Design Pattern

ν”„λ‘μ‹œ νŒ¨ν„΄ 정리

devJK93 2025. 4. 1.

πŸ“ ν”„λ‘μ‹œλž€ 무엇인가?

ν”„λ‘μ‹œ(Proxy)λž€ ν΄λΌμ΄μ–ΈνŠΈκ°€ μ„œλ²„μ— 직접 μš”μ²­ν•˜μ§€ μ•Šκ³ , 쀑간에 λŒ€λ¦¬ 객체λ₯Ό 톡해 κ°„μ ‘μ μœΌλ‘œ μš”μ²­μ„ μˆ˜ν–‰ν•˜λŠ” ꡬ쑰이닀. μ΄λ•Œ μ€‘κ°„μ˜ λŒ€λ¦¬ 객체가 λ°”λ‘œ 'ν”„λ‘μ‹œ'이닀. ν”„λ‘μ‹œλŠ” ν΄λΌμ΄μ–ΈνŠΈμ™€ μ„œλ²„ 사이에 μœ„μΉ˜ν•˜μ—¬ μš”μ²­μ„ κ°€λ‘œμ±„κ±°λ‚˜, 뢀가적인 μž‘μ—…μ„ μˆ˜ν–‰ν•˜κ±°λ‚˜, μš”μ²­μ„ μ œμ–΄ν•  수 μžˆλ‹€.

 

일반적으둜 ν΄λΌμ΄μ–ΈνŠΈλŠ” μžμ‹ μ΄ μš”μ²­ν•˜λŠ” λŒ€μƒμ΄ μ‹€μ œ μ„œλ²„μΈμ§€, ν”„λ‘μ‹œμΈμ§€λ₯Ό μ•Œμ§€ λͺ»ν•œλ‹€. λ”°λΌμ„œ ν”„λ‘μ‹œλŠ” μ„œλ²„μ™€ λ™μΌν•œ μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•΄μ•Ό ν•œλ‹€. 이둜 인해 ν΄λΌμ΄μ–ΈνŠΈλŠ” ν”„λ‘μ‹œλ“  μ„œλ²„λ“  λ™μΌν•œ λ°©μ‹μœΌλ‘œ ν˜ΈμΆœν•  수 있으며, ꡬ쑰가 λ³€κ²½λ˜μ–΄λ„ ν΄λΌμ΄μ–ΈνŠΈ μ½”λ“œλŠ” μ „ν˜€ 영ν–₯을 λ°›μ§€ μ•ŠλŠ”λ‹€.

μ„œλ²„μ™€ ν”„λ‘μ‹œλŠ” 같은 μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„

πŸ—οΈ ν”„λ‘μ‹œ νŒ¨ν„΄μ˜ ꡬ쑰

ν”„λ‘μ‹œ νŒ¨ν„΄μ˜ κΈ°λ³Έ κ΅¬μ‘°λŠ” λ‹€μŒκ³Ό κ°™λ‹€.

  • Client: μš”μ²­μ„ λ³΄λ‚΄λŠ” 주체이닀.
  • Proxy: μ„œλ²„ λŒ€μ‹  μš”μ²­μ„ λ°›μ•„ μ²˜λ¦¬ν•˜κ±°λ‚˜, μ„œλ²„μ—κ²Œ μ „λ‹¬ν•˜λŠ” 쀑간 객체이닀.
  • RealSubject (Server): μ‹€μ œ λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ μ²˜λ¦¬ν•˜λŠ” μ„œλ²„ 객체이닀.

ν”„λ‘μ‹œ νŒ¨ν„΄

이 μ„Έ κ°μ²΄λŠ” λ‹€μŒκ³Ό 같은 μΈν„°νŽ˜μ΄μŠ€ ꡬ쑰λ₯Ό κ°–λŠ”λ‹€.

public interface Subject {
    String operation();
}

public class RealSubject implements Subject {
    public String operation() {
        return "μ‹€μ œ 객체 호좜 κ²°κ³Ό";
    }
}

public class Proxy implements Subject {
    private RealSubject realSubject;

    public Proxy(RealSubject realSubject) {
        this.realSubject = realSubject;
    }

    public String operation() {
        // λΆ€κ°€ κΈ°λŠ₯ μˆ˜ν–‰
        System.out.println("ν”„λ‘μ‹œμ—μ„œ μΆ”κ°€ μž‘μ—… μˆ˜ν–‰");
        return realSubject.operation();
    }
}
  

μ΄λ ‡κ²Œ κ΅¬μ„±λ˜λ©΄ ν΄λΌμ΄μ–ΈνŠΈλŠ” μ•„λž˜μ²˜λŸΌ μ‚¬μš©ν•  수 μžˆλ‹€.

public class Client {
    private Subject subject;

    public Client(Subject subject) {
        this.subject = subject;
    }

    public void execute() {
        String result = subject.operation();
        System.out.println("κ²°κ³Ό: " + result);
    }
}
  

이 κ΅¬μ‘°μ—μ„œ ν΄λΌμ΄μ–ΈνŠΈλŠ” Proxyλ₯Ό μ£Όμž…λ°›μ•„λ„, RealSubjectλ₯Ό μ£Όμž…λ°›μ•„λ„ μ „ν˜€ μ•Œ 수 μ—†μœΌλ©°, μ½”λ“œμ˜ μˆ˜μ • 없이 λ™μΌν•˜κ²Œ λ™μž‘ν•œλ‹€.

ν”„λ‘μ‹œ νŒ¨ν„΄κ³Ό λ°μ½”λ ˆμ΄ν„° νŒ¨ν„΄

GOF λ””μžμΈ νŒ¨ν„΄μ—μ„œλŠ” ν”„λ‘μ‹œ νŒ¨ν„΄κ³Ό λ°μ½”λ ˆμ΄ν„° νŒ¨ν„΄μ„ κ΅¬λΆ„ν•œλ‹€. λ‘˜ λ‹€ κ΅¬μ‘°μ μœΌλ‘œλŠ” μœ μ‚¬ν•˜μ§€λ§Œ, μ˜λ„(intent)κ°€ λ‹€λ₯΄λ‹€.

λ°μ½”λ ˆμ΄ν„° νŒ¨ν„΄

1. ν”„λ‘μ‹œ νŒ¨ν„΄ - μ ‘κ·Ό μ œμ–΄ λͺ©μ 

ν”„λ‘μ‹œ νŒ¨ν„΄μ€ 보톡 접근을 μ œμ–΄ν•˜κΈ° μœ„ν•΄ μ‚¬μš©λœλ‹€.

  • κΆŒν•œμ— λ”°λ₯Έ μ ‘κ·Ό 차단
  • κ²°κ³Όλ₯Ό μΊμ‹±ν•΄μ„œ μ„±λŠ₯ ν–₯상
  • μ§€μ—° λ‘œλ”©

μ˜ˆμ‹œ: μΊμ‹œ ν”„λ‘μ‹œ

public class CacheProxy implements Subject {
    private Subject target;
    private String cache;

    public CacheProxy(Subject target) {
        this.target = target;
    }

    public String operation() {
        if (cache == null) {
            cache = target.operation();
        }
        return cache;
    }
}
  

ν•œ 번만 μ„œλ²„μ— μ ‘κ·Όν•˜κ³ , μ΄ν›„μ—λŠ” μΊμ‹œλœ κ²°κ³Όλ₯Ό λ¦¬ν„΄ν•œλ‹€.

2. λ°μ½”λ ˆμ΄ν„° νŒ¨ν„΄ - κΈ°λŠ₯ ν™•μž₯ λͺ©μ 

λ°μ½”λ ˆμ΄ν„° νŒ¨ν„΄μ€ κΈ°μ‘΄ κΈ°λŠ₯에 μƒˆλ‘œμš΄ κΈ°λŠ₯을 μΆ”κ°€ν•˜λŠ” 데 μ‚¬μš©λœλ‹€. κ΅¬μ‘°λŠ” ν”„λ‘μ‹œμ™€ 거의 λ™μΌν•˜μ§€λ§Œ, λͺ©μ μ΄ λ‹€λ₯΄λ‹€.

μ˜ˆμ‹œ: λ©”μ‹œμ§€ λ³€κ²½ λ°μ½”λ ˆμ΄ν„°

public class MessageDecorator implements Subject {
    private Subject target;

    public MessageDecorator(Subject target) {
        this.target = target;
    }

    public String operation() {
        String result = target.operation();
        return "[λ°μ½”λ ˆμ΄μ…˜] " + result;
    }
}
  

ν”„λ‘μ‹œλ₯Ό μ—°μ†μ μœΌλ‘œ μ—°κ²°ν•˜λŠ” 체인 ν˜•νƒœλ‘œλ„ κ°€λŠ₯ν•˜λ‹€.

Subject realSubject = new RealSubject();
Subject decorator1 = new MessageDecorator(realSubject);
Subject decorator2 = new TimeDecorator(decorator1);

Client client = new Client(decorator2);
client.execute();
  

이처럼 λ°μ½”λ ˆμ΄ν„°λŠ” 체인을 κ΅¬μ„±ν•΄μ„œ 점점 κΈ°λŠ₯을 μΆ”κ°€ν•΄ λ‚˜κ°ˆ 수 μžˆλ‹€. ν”„λ‘μ‹œ μ—­μ‹œ λ§ˆμ°¬κ°€μ§€λ‘œ ν”„λ‘μ‹œ 체인을 ꡬ성할 수 μžˆλ‹€.

ν”„λ‘μ‹œ νŒ¨ν„΄μ˜ 핡심 정리

  • ν”„λ‘μ‹œλŠ” μ„œλ²„μ™€ λ™μΌν•œ μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•΄μ•Ό ν•œλ‹€.
  • ν”„λ‘μ‹œλ₯Ό λ„μž…ν•΄λ„ ν΄λΌμ΄μ–ΈνŠΈ μ½”λ“œλŠ” λ³€κ²½ 없이 λ™μž‘ν•΄μ•Ό ν•œλ‹€.
  • ν”„λ‘μ‹œλŠ” 체인 ν˜•νƒœλ‘œ μ—¬λŸ¬ 개 μ—°κ²°ν•  수 μžˆλ‹€.
  • μ ‘κ·Ό μ œμ–΄λ‚˜ λΆ€κ°€ κΈ°λŠ₯ μΆ”κ°€ λ“±μ˜ 역할을 μˆ˜ν–‰ν•  수 μžˆλ‹€.
  • ν”„λ‘μ‹œλŠ” μŠ€ν”„λ§ AOPλ‚˜ ν”„λ‘μ‹œ 기반 DI κ΅¬ν˜„μ—μ„œ 맀우 μ€‘μš”ν•˜λ‹€.

μŠ€ν”„λ§μ—μ„œμ˜ μ‹€μ œ ν”„λ‘μ‹œ κ΅¬ν˜„ μ˜ˆμ‹œ

μ•„λž˜λŠ” μŠ€ν”„λ§μ—μ„œ μΈν„°νŽ˜μ΄μŠ€ 기반으둜 ν”„λ‘μ‹œλ₯Ό κ΅¬ν˜„ν•œ μ½”λ“œμ΄λ‹€.

@RequiredArgsConstructor
public class OrderServiceInterfaceProxy implements OrderServiceV1 {
    private final OrderServiceV1 target;
    private final LogTrace logTrace;

    public void orderItem(String itemId) {
        TraceStatus status = null;
        try {
            status = logTrace.begin("OrderService.orderItem()");
            target.orderItem(itemId);
            logTrace.end(status);
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }
}
  

이런 λ°©μ‹μœΌλ‘œ OrderController, OrderRepository도 ν”„λ‘μ‹œ κ΅¬ν˜„μ²΄λ₯Ό λ§Œλ“€μ–΄, κΈ°μ‘΄ κ΅¬ν˜„μ²΄λ₯Ό κ°μ‹ΈλŠ” λ°©μ‹μœΌλ‘œ κΈ°λŠ₯을 ν™•μž₯ν•  수 μžˆλ‹€.


πŸ“Œ JDK 동적 ν”„λ‘μ‹œ 기술

ν”„λ‘μ‹œ κΈ°μˆ μ„ ν™œμš©ν•˜μ—¬ κΈ°μ‘΄ μ½”λ“œλ₯Ό μˆ˜μ •ν•˜μ§€ μ•Šκ³  λΆ€κ°€ κΈ°λŠ₯을 μΆ”κ°€ν•  수 μžˆμ—ˆμ§€λ§Œ, λ­”κ°€ ν•΄κ²°ν•΄μ•Όν•  뢀뢄이 λ‚¨μ•„μžˆλŠ” 것 처럼 보인닀.
같은 ν˜Ήμ€ μ•„μ£Ό μœ μ‚¬ν•œ κΈ°λŠ₯을 μˆ˜ν–‰ν•˜λŠ” ν”„λ‘μ‹œλΌλ„ ν”„λ‘μ‹œλ₯Ό μ μš©ν•˜κΈ° μœ„ν•΄ 적용 λŒ€μƒμ˜ 숫자 만큼 λ§Žμ€ ν”„λ‘μ‹œ 클래슀λ₯Ό 생산해야 ν•œλ‹€λŠ” 점이닀.

οΉ‘ 적용 λŒ€μƒμ΄ 1μ–΅κ°œμ˜ 클래슀라면 ν”„λ‘μ‹œ ν΄λž˜μŠ€λ„ 1μ–΅κ°œλ₯Ό 생상해야 ν•œλ‹€.

 

동적 ν”„λ‘μ‹œλ₯Ό μ΄ν•΄ν•˜λ €λ©΄ μžλ°” λ¦¬ν”Œλ ‰μ…˜ κΈ°μˆ μ— λŒ€ν•œ 이해가 μ„ ν–‰λ˜μ–΄μ•Ό ν•œλ‹€.

λ¦¬ν”Œλ ‰μ…˜ κΈ°μˆ μ„ μ‚¬μš©ν•˜λ©΄ ν΄λž˜μŠ€λ‚˜ λ©”μ„œλ“œμ˜ λ©”타정보λ₯Ό λŸ°νƒ€μž„μ— λ™μ μœΌλ‘œ νšλ“ν•˜κ³ , μ½”λ“œλ„ λ™μ μœΌλ‘œ ν˜ΈμΆœν•  μˆ˜ μžˆλ‹€.

λŸ°νƒ€μž„μ—μ„œ ν΄λž˜μŠ€λ‚˜ λ©”μ„œλ“œμ˜ 메타정보λ₯Ό μ‚¬μš©ν•΄μ„œ λ™μ μœΌλ‘œ ν˜ΈμΆœν•˜λŠ” λ©”μ„œλ“œλ₯Ό λ³€κ²½ν•  μˆ˜ μžˆλ‹€. 

 

πŸ“ μžλ°” λ¦¬ν”Œλ ‰μ…˜ 예제 μ½”λ“œ

@Test
void reflection1() throws Exception {
    //클래슀 정보
    Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
    
    Hello target = new Hello();
    //callA λ©”μ„œλ“œ 정보
    Method methodCallA = classHello.getMethod("callA");
    Object result1 = methodCallA.invoke(target);
    log.info("result1={}", result1);
    
    //callB λ©”μ„œλ“œ 정보
    Method methodCallB = classHello.getMethod("callB");
    Object result2 = methodCallB.invoke(target);
    log.info("result2={}", result2);
}

@Slf4j
static class Hello {

    public String callA() {
    log.info("callA");
    return "A";
    }
    
    public String callB() {
    log.info("callB");
    return "B";
    }
    
}

 

JDK 동적 ν”„λ‘μ‹œ κΈ°μˆ μ€ 이 λ¦¬ν”Œλ ‰μ…˜ κΈ°μˆ μ„ μ΄μš©ν•˜μ—¬ κ°œλ°œμžκ°€ 직접 ν”„λ‘μ‹œλ₯Ό λ§Œλ“€μ§€ μ•Šμ•„λ„ λ˜λ„λ‘ ν•΄μ£ΌλŠ”λ°, λŸ°νƒ€μž„μ— ν”„λ‘μ‹œ 객체λ₯Ό λ™μ μœΌλ‘œ λ§Œλ“€μ–΄ μ£ΌκΈ° λ•Œλ¬Έμ΄λ‹€.

 

πŸ“ 예제λ₯Ό 톡해 μ•Œμ•„λ³΄μž

AInterface

package hello.proxy.jdkdynamic.code;

public interface AInterface {
	String call();
}

 

AInterface κ΅¬ν˜„μ²΄

package hello.proxy.jdkdynamic.code;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class AImpl implements AInterface {
    @Override
    public String call() {
        log.info("A 호좜");
        return "a";
    }
}

 

BInterface

package hello.proxy.jdkdynamic.code;

public interface BInterface {
	String call();
}

 

BInterface κ΅¬ν˜„μ²΄

package hello.proxy.jdkdynamic.code;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class BImpl implements BInterface {
    @Override
    public String call() {
        log.info("B 호좜");
        return "b";
    }
}


JDK 동적 ν”„λ‘μ‹œκ°€ μ œκ³΅ν•˜λŠ” InvocationHandler (InvocationHandler을 κ΅¬ν˜„ν•΄μ„œ Proxy에 λ§€κ°œλ³€μˆ˜λ‘œ λ„£μ–΄μ€˜μ•Ό ν•œλ‹€.)

이게 JDK 동적 ν”„λ‘μ‹œμ— μ μš©ν•  곡톡 λ‘œμ§μ΄λ‹€.

 

package java.lang.reflect;

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable;
}
package hello.proxy.jdkdynamic.code;

import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

@Slf4j
public class TimeInvocationHandler implements InvocationHandler {

    private final Object target;
    
    public TimeInvocationHandler(Object target) {
	    this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        log.info("TimeProxy μ‹€ν–‰");
        long startTime = System.currentTimeMillis();
        
        Object result = method.invoke(target, args);
        
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("TimeProxy μ’…λ£Œ resultTime={}", resultTime);
        
        return result;
    }
}

- `TimeInvocationHandler` 은 `InvocationHandler` μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•œλ‹€. μ΄λ ‡κ²Œν•΄μ„œ JDK 동적 프둝
μ‹œμ— μ μš©ν•  κ³΅ν†΅ λ‘œμ§μ„ κ°œλ°œν•  μˆ˜ μžˆλ‹€.
- `Object target` : 동적 ν”„λ‘μ‹œκ°€ ν˜ΈμΆœν•  λŒ€μƒ
- `method.invoke(target, args)` : λ¦¬ν”Œλ ‰μ…˜μ„ μ‚¬μš©ν•΄μ„œ `target` μΈμŠ€ν„΄μŠ€μ˜ λ©”μ„œλ“œλ₯Ό μ‹€ν–‰ν•œλ‹€. `args`
λŠ” λ©”μ„œλ“œ ν˜ΈμΆœμ‹œ λ„˜κ²¨μ€„ μΈμˆ˜μ΄λ‹€.

package hello.proxy.jdkdynamic;

import hello.proxy.jdkdynamic.code.*;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Proxy;

@Slf4j
public class JdkDynamicProxyTest {
    @Test
    void dynamicA() {
        AInterface target = new AImpl();
        TimeInvocationHandler handler = new TimeInvocationHandler(target);
        
        AInterface proxy = (AInterface) Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[] {AInterface.class}, handler);
        proxy.call();
        
        log.info("targetClass={}", target.getClass());
        log.info("proxyClass={}", proxy.getClass());
    }
    
    @Test
    void dynamicB() {
        BInterface target = new BImpl();
        TimeInvocationHandler handler = new TimeInvocationHandler(target);
        
        BInterface proxy = (BInterface) Proxy.newProxyInstance(BInterface.class.getClassLoader(), new Class[] {BInterface.class}, handler);
        proxy.call();
        
        log.info("targetClass={}", target.getClass());
        log.info("proxyClass={}", proxy.getClass());
    }
}

 

직접 ν”„λ‘μ‹œμ™€ JDK 동적 ν”„λ‘μ‹œ 비ꡐ

νŠΉμ • νŒ¨ν„΄μ˜ URl만 동적 ν”„λ‘μ‹œλ₯Ό μ μš©ν•˜κ³  싢을 경우

package hello.proxy.config.v2_dynamicproxy.handler;
import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;
import org.springframework.util.PatternMatchUtils;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class LogTraceFilterHandler implements InvocationHandler {

  private final Object target;
  private final LogTrace logTrace;
  private final String[] patterns;
  
  public LogTraceFilterHandler(Object target, LogTrace logTrace, String[] patterns) {
    this.target = target;
    this.logTrace = logTrace;
    this.patterns = patterns;
  }
  
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  
	//λ©”μ„œλ“œ 이름 ν•„ν„°
    String methodName = method.getName();
    if (!PatternMatchUtils.simpleMatch(patterns, methodName)) {
      return method.invoke(target, args);
    }
    
    TraceStatus status = null;
    
    try {
      String message = method.getDeclaringClass().getSimpleName() + "."
          + method.getName() + "()";
      status = logTrace.begin(message);
	  //둜직 호좜
      Object result = method.invoke(target, args);
      logTrace.end(status);
      return result;
    } catch (Exception e) {
      logTrace.exception(status, e);
      throw e;
    }
  }
}
package hello.proxy.config.v2_dynamicproxy;

import hello.proxy.app.v1.*;
import hello.proxy.config.v2_dynamicproxy.handler.LogTraceFilterHandler;
import hello.proxy.trace.logtrace.LogTrace;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Proxy;

@Configuration
public class DynamicProxyFilterConfig {

  private static final String[] PATTERNS = {"request*", "order*", "save*"};
  
  @Bean
  public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
    OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
    OrderControllerV1 proxy = (OrderControllerV1)
        Proxy.newProxyInstance(OrderControllerV1.class.getClassLoader(),
            new Class[]{OrderControllerV1.class},
            new LogTraceFilterHandler(orderController, logTrace, PATTERNS)
        );
    return proxy;
  }
  
  @Bean
  public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
    OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
    OrderServiceV1 proxy = (OrderServiceV1)
        Proxy.newProxyInstance(OrderServiceV1.class.getClassLoader(),
            new Class[]{OrderServiceV1.class},
            new LogTraceFilterHandler(orderService, logTrace, PATTERNS)
        );
    return proxy;
  }
  
  @Bean
  public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
    OrderRepositoryV1 orderRepository = new OrderRepositoryV1Impl();
    OrderRepositoryV1 proxy = (OrderRepositoryV1)
        Proxy.newProxyInstance(OrderRepositoryV1.class.getClassLoader(),
            new Class[]{OrderRepositoryV1.class},
            new LogTraceFilterHandler(orderRepository, logTrace, PATTERNS)
        );
    return proxy;
  }
  
}

λŒ“κΈ€