About Proxy (feat: GoF 강의)

PPakSSam·2022년 1월 14일
1

프록시(Proxy) 패턴

특정 객체에 대한 접근을 제어하거나 기능을 추가할 수 있는 패턴
초기화 지연, 접근 제어, 로깅, 캐싱 등 다양하게 응용해 사용할 수 있다.

간단한 코드로 프록시 패턴에 대해 알아보도록 하자.

public class GameService {

    public void startGame() {
        System.out.println("이 자리에 오신 여러분을 진심으로 환영합니다.");
    }
}

위와 같은 GameService 클래스가 있다고 가정하자.
그런데 다음과 같은 요구사항이 주어졌다고 하자.

  • GameService의 코드는 건들지 말것
  • startGame() 메소드의 실행시간을 구해서 출력할 것

이때 사용할 수 있는 방법이 Proxy 패턴인데 2가지의 방법이 있다.
1. 상속을 이용하여 프록시 패턴 구현
2. 인터페이스를 이용하여 프록시 패턴 구현

상속을 이용하여 프록시 패턴 구현

// Proxy
public class GameServiceInheritanceProxy extends DefaultGameService {

    @Override
    public void startGame() {
        long before = System.currentTimeMillis();
        super.startGame();
        System.out.println(System.currentTimeMillis() - before);
    }
}

// 실행
public class Client {

    public static void main(String[] args) {
        GameService gameService = new GameServiceInheritanceProxy();
        gameService.startGame();
    }
}

// 실행 결과
이 자리에 오신 여러분을 진심으로 환영합니다.
0

인터페이스를 이용하여 프록시 패턴 구현

// 인터페이스
public interface GameService {
    
    void startGame();
}

// 구현체
public class DefaultGameService implements GameService{
    
    @Override
    public void startGame() {
        System.out.println("이 자리에 오신 여러분을 진심으로 환영합니다.");
    }
}

// Proxy
public class GameServiceInterfaceProxy implements GameService{

    private GameService gameService;

    // startGame 메소드를 실행해야 비로소 초기화를 해주는 방식
    @Override
    public void startGame() {
        long before = System.currentTimeMillis();
        if(gameService == null) {
            this.gameService = new DefaultGameService();
        }
        gameService.startGame();
        System.out.println(System.currentTimeMillis() - before);
    }
}

// 실행
public class Client {

    public static void main(String[] args) {

        GameService gameService = new GameServiceInterfaceProxy();
        gameService.startGame();
    }
}

// 실행 결과
이 자리에 오신 여러분을 진심으로 환영합니다.
0

이렇게 간단하게 정적으로 프록시 패턴을 구현하는 것에 대해 알아봤다.
그런데 자바에서는 리플렉션을 이용하여 동적으로 프록시를 구현할 수 있다.
이에 대해 알아보자.

동적으로 프록시 패턴 구현

public class ProxyInJava {

    public static void main(String[] args) {

        ProxyInJava proxyInJava = new ProxyInJava();
        proxyInJava.dynamicProxy();
        proxyInJava.dynamicProxy2();
    }

    private void dynamicProxy() {
        GameService gameServiceProxy = getGameServiceProxy(new DefaultGameService());
        gameServiceProxy.startGame();
    }

    private void dynamicProxy2() {
        GameService gameServiceProxy = initGameServiceProxy();
        gameServiceProxy.startGame();
    }
	
    
    // 람다를 이용하여 간단하게 구현
    private GameService getGameServiceProxy(GameService target) {
        return (GameService) Proxy.newProxyInstance(
                this.getClass().getClassLoader(),
                new Class[]{GameService.class},
                (proxy, method, args) -> {
                    System.out.println("Hello dynamic proxy");
                    method.invoke(target, args);
                    return null;
                }
        );
    }
	
    // 메소드를 실행해야 비로소 초기화되게끔 구현
    private GameService initGameServiceProxy() {
        return (GameService) Proxy.newProxyInstance(
                this.getClass().getClassLoader(),
                new Class[]{GameService.class},
                new InvocationHandler() {

                    GameService target;

                    @Override
                    public Object invoke(Object proxy, Method method, 
                    			Object[] args) throws Throwable {
                    
                        if(target == null) {
                            this.target = new DefaultGameService();
                        }
                        System.out.println("Hello dynamic proxy");
                        target.startGame();
                        return null;
                    }
                }
        );
    }
}

프록시 패턴의 장점과 단점

  • 장점
    • 기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있다.
    • 기존 코드가 해야 하는 일만 유지할 수 있다.
    • 기능 추가 및 초기화 지연등으로 다양하게 활용할 수 있다.


  • 단점
    • 코드의 복잡도가 증가한다.

스프링에서의 프록시 패턴

스프링에서는 JDK Dynamic ProxyCGLIB Proxy를 사용한다.

JDK Dynamic Proxy

위에서 설명한 동적으로 프록시 패턴 구현한 것이 JDK Dynamic Proxy이다.
Dynamic Proxy는 반드시 Interface가 정의되어야 하고, 이 Interface를 기준으로 Proxy를 생성한다.
따라서 Interface 선언에 대한 강제성이 있다는 단점이 있다.
또한 Reflection을 사용하여 기능을 구현하기 때문에 퍼포먼스 하락의 원인이 되기도 한다.

CGLIB Proxy

CGLIB ProxyEnhancer를 바탕으로 Proxy를 구현하는 방식이다.
이 방식은 JDK Dynamic Proxy와는 다르게 Reflection을 사용하지 않고 상속 방식을 이용해서 Proxy화 할 메서드를 오버라이딩 하는 방식이다.
CGLIB는 클래스의 바이트코드를 조작해서 Proxy를 만들기때문에 JDK Dynamic Proxy보다 성능적으로 우세하다. 다만 final 객체 혹은 private 접근자로 된 메소드는 상속이 지우너되지 않기 때문에 제약적인 Proxy 구현에서 단점으로 지적된다.

더 공부할 부분

https://gmoon92.github.io/spring/aop/2019/04/20/jdk-dynamic-proxy-and-cglib.html
https://velog.io/@hanblueblue/Spring-Proxy-1-Java-Dynamic-Proxy-vs.-CGLIB

profile
성장에 대한 경험을 공유하고픈 자발적 경험주의자

0개의 댓글