Blog
강의 리뷰
백기선 GoF 디자인 패턴
Structural Patterns
Proxy

프록시 패턴 (Proxy Pattern)

특정한 객체나 오퍼레이션에 접근하기 전에 프록시 객체를 거쳐서 접근하게 하는 패턴이다. 예를 들자면 사장을 만나기 위해서 직접 만나는게 아니라 비서를 거쳐서 만나는 것과 같다.

구현

아래와 같이 Proxy를 구현해서 최초의 Client 코드와 DefaultGameService의 구현체는 어떠한 변경도 없이 새로운 기능을 추가할 수 있다. Client 코드에서 GameService 객체에 DefaultGameService가 아닌 GameServiceProxy를 생성하게 되면 GameServiceProxy의 startGame() 메소드가 호출되어 DefaultGameService의 startGame() 메소드를 호출하게 된다.

public class GameServiceProxy implements GameService{
    private GameService gameService;
 
    public void startGame() {
        long before= System.currentTimeMillis();
 
        // Lazy Initialization 해서 실제로 사용될 때까지 객체를 생성하지 않음
        if(gameService == null) {
            this.gameService = new DefaultGameService();
        }
        gameService.startGame();
        System.out.println("소요시간: " + (System.currentTimeMillis() - before) + "ms");
    }
}

Reflection을 활용한 동적 프록시

Reflection의 Proxy 클래스를 활용하면 프록시를 객체를 간편하게 생성할 수 있는데 Spring AOP에서도 이러한 방식을 사용한다.

public class GameServiceProxyInJava {
    public static void main(String[] args) {
        GameServiceProxyInJava proxyInJava = new GameServiceProxyInJava();
        proxyInJava.dynamicProxy();
    }
 
    private void dynamicProxy() {
        GameService gameService = getGameServiceProxy(new DefaultGameService());
    }
 
    // 이 코드가 실행될 때 Proxy Instance를 생성하게 된다.
    private GameService getGameServiceProxy(GameService target) {
        return (GameService) Proxy.newProxyInstance(this.getClass()
                        .getClassLoader(), new Class[]{GameService.class},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // target을 args를 넣어서 실행하겠다.
                        method.invoke(target, args);
                        return null;
                    }
                }
        );
    }
}

Spring Proxy

Spring에서는 다음과 같이 Aspect@Around를 사용해서 프록시를 구현할 수 있다.

@Service
public class GameService {
    public void startGame() {
        System.out.println("Game started");
    }
}

적용하는 상황

  1. 초기화 지연 : 객체가 매우 크거나 복잡한 경우, 객체를 미리 생성하는게 아니라 필요할 때 생성하는 방식으로 사용한다.
  2. 보안 : 객체에 대한 접근을 제어하고 싶을 때 사용한다.
  3. 캐싱 : 어떤 오퍼레이션을 거쳐서 데이터를 가져오는데 프록시 안에 캐싱해둔 데이터가 있다면 오퍼레이션까지 가지도 않고 리턴을 하는것이다.

장점과 단점

  • 장점
    • 기존의 코드를 수정하지 않고도 새로운 기능을 추가할 수 있다. (OCP 원칙)
    • 기존 코드가 해야 하는 일만 유지할 수 있다. (SRP)
    • 기능 추가 및 초기화 지연, 캐싱 등으로 다양하게 활용할 수 있다.
  • 단점
    • 코드의 복잡도가 증가한다.