본문 바로가기
Computer Sience/Desgin Pattern

[Design Pattern] 프록시(Proxy) 패턴 + 다이나믹 프록시

by 제우제우 2024. 10. 30.

프록시(Proxy) 패턴

특정 객체에 대한 접근을 제어하거나 기능을 추가할 수 있는 패턴

초기화 지연, 접근 제어, 로깅, 캐싱 등 다양하게 응용하여 사용할 수 있다.

프록시(Proxy) 패턴 before

public class GameService {
    public void gameService1(){
        System.out.println("gameService1");
    }
    public void gameService2(){
        System.out.println("gameService2");
    }
}

public class Client {
    public static void main(String[] args) {
        GameService gameService = new GameService();
        gameService.gameService1();
        gameService.gameService2();
    }
}

 

비즈니스 로직인 gameService1 메소드와 gameService2 메소드의 동작 시간을 체킹 해달라는 요구사항이 내려왔다. 

gameService1 메소드와 gameService2의 메소드는 변경하지 않아야 한다. 

클라이언트 코드 또한 변경하지 않아야 한다. 

 

만약 변경이 가능하더라도 이런 2개의 메소드가 아닌 수많은 메소드의 동작 시간을 체킹 하는데 메소드 호출 앞뒤로 시간을 체크하는 로직이 들어가면 순수 클라이언트 코드와 시간을 체킹 하는 로직이 섞인다. 

 

또한 만약 다시 동작 시간을 체크하지 말라고 하면?

수많은 로직을 변경해야 한다. 

이는 순수 노동이자 ocp가 지켜지지 않는다. 

 

이를 proxy 패턴을 적용해서 클라이언트 코드와 기존 서비스 코드는 변경하지 않고 호출 시간을 체크하도록 해보겠다.

프록시(Proxy) 패턴 after

상속을 통한 프록시 

GameService 

public class GameService {
    public void gameService1() {
        System.out.println("gameService1");
        sleep();
    }
    public void gameService2(){
        System.out.println("gameService2");
        sleep();
    }
    private static void sleep(){
        try {
            Thread.sleep(1000L);
        }
        catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

너무 빨리 끝나서 Thread.sleep(1000L) 코드 추가 

 

GameServiceProxy

public class GameServiceProxy extends GameService{
    @Override
    public void gameService1() {
        long start = System.currentTimeMillis();
        super.gameService1();
        System.out.println(System.currentTimeMillis() - start);
    }
    @Override
    public void gameService2() {
        long start = System.currentTimeMillis();
        super.gameService2();
        System.out.println(System.currentTimeMillis() - start);
    }
}

GameServiceProxy는 리얼 서브젝트인 GameService의 메소드를 호출하기 전에 호출되는 대리인(proxy)이다.

리얼 서브젝트의 메소드 호출 메소드 앞 뒤로 시간을 체킹해서 출력한다.  

 

클라이언트 코드 

public class Client {
    public static void main(String[] args) {
        GameService gameService = new GameServiceProxy();
        gameService.gameService1();
        gameService.gameService2();
    }
}

출력 
gameService1
1008
gameService2
1002

클라이언트는 이제 GameService를 사용하는 게 아니라 GameServiceProxy를 사용한다.

출력 결과를 보면 걸리는 시간이 잘 나온다. 

이렇게 상속을 사용해서 클라이언트 코드와 기존 서비스 코드의 변경 없이 걸리는 시간 로직을 추가해 봤다.

인터페이스를 통한 프록시 

 

GameService 인터페이스 

public interface GameService {
    void startService1();
    void startService2();
}

 

DefaultGameService 인터페이스 

public class DefaultGameService implements GameService{
    @Override
    public void startService1() {
        System.out.println("gameService1");
        sleep();
    }
    @Override
    public void startService2() {
        System.out.println("gameService2");
        sleep();
    }
    private static void sleep(){
       try {
           Thread.sleep(1000L);
       }
       catch (InterruptedException e){
           e.printStackTrace();
       }
   }
}

기존 로직을 가지는 클래스

 

GameServiceProxy

@RequiredArgsConstructor
public class GameServiceProxy implements GameService{
    private final GameService target;
    @Override
    public void startService1() {
        long start = System.currentTimeMillis();
        target.startService1();
        System.out.println(System.currentTimeMillis() - start);
    }
    @Override
    public void startService2() {
        long start = System.currentTimeMillis();
        target.startService2();
        System.out.println(System.currentTimeMillis() - start);
    }
}

내부에서 GameService 타입의 target을 가진다.

이 target은 real subject이고 이 예제에서는 DefaultGameService를 의미한다. 

 

클라이언트 코드

public class Client {
    public static void main(String[] args) {
        GameService gameService = new GameServiceProxy(new DefaultGameService());
        gameService.startService1();
        gameService.startService2();
    }
}

클라이언트는 프록시를 통해서 DefaultGameService API를 사용하게 된다. 

출력은 동일하다. 

 

남은 문제점 

모든 클래스 / 모든 메소드에 시간 체크를 하려면 인터페이스를 사용하던 상속을 하던 프록시 클래스를 만들고

메소드를 정의해야 한다. 

하지만 시간 체크 로직은 동일하다. 

실제 메소드 호출 앞뒤로 시간 체크만 한다. 

이런 문제점은 다이나믹 프록시(동적 프록시)기술을 사용하면 해결된다. 

동적 프록시 적용 

TimeInvocationHandler

public class TimeInvocationHandler implements InvocationHandler {
    private Object target;
    public TimeInvocationHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = method.invoke(target, args);
        System.out.println(System.currentTimeMillis() - start);
        return result;
    }
}

TimeInvocationHandler는 InvocationHandler를 구현한다. 

 

프록시 생성 & 프록시 사용 코드 

public class Client {
    public static void main(String[] args) {
        GameService target = new DefaultGameService();
        TimeInvocationHandler handler = new TimeInvocationHandler(target);

        // 동적 Proxy 인스턴스 생성
        GameService proxy = (GameService) Proxy.newProxyInstance
                (GameService.class.getClassLoader(), new Class[]{GameService.class}, handler);

        proxy.startService1();
        proxy.startService2();

        // Proxy API
        System.out.println(proxy.getClass()); // class jdk.proxy1.$Proxy0
        System.out.println(Proxy.isProxyClass(proxy.getClass())); // true
    }
}

// 출력 
gameService1
1006
gameService2
1014
0
class jdk.proxy1.$Proxy0
true

이렇게 동적 프록시 기술을 사용하면 개발자가 직접 프록시 클래스를 만들어줄 필요 없이 

런타임에 동적으로 프록시 클래스가 생성된다. 

 

동적 프록시 기술 정리

 

Spring/Spring8-Advanced/proxy/Section5 동적 프록시 기술 at main · jeus1998/Spring

Records everything related to spring and spring. Contribute to jeus1998/Spring development by creating an account on GitHub.

github.com

프록시(Proxy) 패턴 정리

특정 객체에 대한 접근을 제어하거나 기능을 추가할 수 있는 패턴

 

장점

기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있다

기존 코드가 해야 하는 일만 유지할 수 있다

기능 추가 및 초기화 지연 등으로 다양하게 활용할 수 있다

 

단점

코드의 복잡도가 증가한다

프록시(Proxy) 패턴 적용 사례

자바

다이나믹 프록시, java.lang.reflect.Proxy

 

스프링

스프링 AOP

참고자료

백기선님 디자인 패턴 강의

 

코딩으로 학습하는 GoF의 디자인 패턴 강의 | 백기선 - 인프런

백기선 | 디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를

www.inflearn.com