본문 바로가기
ComputerScience/Design Pattern

Proxy Pattern

by 규난 2023. 3. 13.
728x90

Proxy Pattern 이란?

특정 객체에 접근하기 전에 proxy 객체를 통해 접근 제어(권한 체크, 캐싱, 지연 로딩) 및 기능을 추가할 수 있는 패턴입니다.

즉, 클라이언트가 사용하려는 객체를 직접 사용하는 것이 아니라 proxy를 거쳐서 사용하는 패턴입니다.

 

객체에서 프록시가 되려면, 클라이언트는 서버(요청을 처리하는 객체)에게 요청을 한 것인지, 프록시에게 요청을 한 것인지 몰라야 합니다. 즉 서버와 프록시 객체는 같은 인터페이스를 사용해야 합니다. 이렇게 되면 클라이언트는 서버 객체를 프록시 객체로 변경해도 클라이언트의 코드를 변경하지 않고(OCP) 새로운 기능 추가 및 접근 제어를 할 수 있게 됩니다.

 

proxy pattern을 사용하면 사용하려는 객체에 접근제어를 한다던가 생성하는데 많은 resource가 드는 instance라면 application이 구동할 때 미리 instance를 만들어두지 않고 객체가 실제로 쓰일 때 만드는 lazy loding을 적용할 수 있습니다.

 

디자인 패턴 공부 중 프록시 패턴과 데코레이터 패턴 개념이 굉장히 비슷해서 많이 헷갈렸는데 GoF 디자인 패턴에서는 이 둘을 의도(intent)에 따라서 구분하고 있습니다. (스프링의 프록시는 GoF에 나오는 프록시 패턴 + 데코레이터 패턴을 합쳐서 만든 것 같은 느낌이 듭니다..)

프록시 패턴 : 접근 제어가 목적

데코레이터 패턴 : 새로운 기능을 추가하는 목적

 

Proxy Pattern의 장점

  • 기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있습니다. (OCP)
  • 기존 코드는 단일의 책임만을 가질 수 있습니다. (SRP)
  • 기능 추가 및 초기화 지연, 캐싱 등으로 다양하게 활용할 수 있습니다.

Proxy Pattern의 단점

  • 코드의 복잡도가 증가할 수 있습니다.
  • 응답속도가 느려질 수 있습니다.

Proxy Pattern 예제

public class Main {

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

    void startGame();
}


public class DefaultGameService implements GameService {

    @Override
    public void startGame() {
        System.out.println("start game!!");
    }
}


public class GameServiceProxy implements GameService {

    private GameService gameService;

    @Override
    public void startGame() {
        long before = System.currentTimeMillis();

        if (Objects.isNull(this.gameService)) {
            this.gameService = new DefaultGameService();
        }


        long after = System.currentTimeMillis();

        System.out.println("total runtime : " + (after - before));
    }
}

위 코드 예시를 보면 클라이언트인 Main class에서 실제 게임을 실행하는 DefaultGameService를 호출하는 것이 아니라

GameServiceProxy의 인스턴스를 생성 후 proxy의 startGame을 호출하여 proxy 객체에서 게임이 실행하는데 걸리는 시간 계산과

실제로 DefaultGameService가 쓰이는 시점에 DefaultGameService 인스턴스를 생성하는 것을 볼 수 있습니다. (lazy loading)

이렇게 proxy pattern을 적용하게 되면 실제 사용하려는 객체에 접근하기 전 접근 제어 및 객체의 초기화 지연 등을 다양하게 적용할 수 있습니다.

 

proxy pattern에서 해야 하는 비슷한 기능들, 예를 들면 메소드 마다 실행 완료까지의 걸리는 시간을 체크할 때 위 코드 예시처럼 정적으로 proxy 객체를 만들면 proxy 객체마다 실제 메소드가 실행되기 전과 후에 시간을 체크해 계산하는 로직을 다 넣어주어야 합니다. 

 

더 쉽게 이해할 수 있도록 코드로 예시를 보여드리도록 하겠습니다.

위에서 만든 게임이 원래 로그인 없이 모든 사용자가 이용 가능한 게임이었다가 추가 요구 사항이 들어와 로그인을 한 유저만 게임을 할 수 있도록 변경하고 로그인 완료까지 걸리는 시간도 체크해달라는 요구 사항까지 있었다고 가정을 해보도록 하겠습니다.

public interface LoginService {

    void login();
}

public class LoginServiceImpl implements LoginService {

    @Override
    public void login() {
        System.out.println("login complete!!");
    }
}

public class LoginServiceProxy implements LoginService {

    private LoginService loginService;

    @Override
    public void login() {
        long before = System.currentTimeMillis();

        if (Objects.isNull(this.loginService)) {
            this.loginService = new LoginServiceImpl();
        }

        this.loginService.login();

        long after = System.currentTimeMillis();

        System.out.println("total runtime : " + (after - before));
    }
}

위 코드에서 볼 수 있듯이 GameServiceProxy startGame 메소드 코드와 대부분 중복이 됩니다.

이처럼 간단한 예제에서는 별문제가 없어 보이지만 나중에 서비스가 커지게 되면 이러한 중복 코드가 엄청나게 많아지게 될 겁니다.

 

그래서 java에서는 java.lang.reflect.Proxy 패키지에서 제공하는 API를 이용하여 class나 method의 메타정보를 획득하여(reflection) 컴파일 시점이 아니라 런타임 시점에 동적으로 proxy 인스턴스를 만들 수 있는 Dynamic Proxy를 제공합니다.

 

java.lang.reflect.Proxy 패키지를 이용한 Dynamic Proxy 예제

package proxy;

import java.lang.reflect.Proxy;

public class Main {

    public static void main(String[] args) {
    	// Proxy.newProxyInstance() 첫 번째 인자는 class loader
        // 두 번째 인자는 동적으로 생성되는 프록시가 구현해야하는 인터페이스 타입
        // 세 번째는 InvocationHandler 타입의 핸들러
        LoginService loginService = (LoginService) Proxy.newProxyInstance(LoginService.class.getClassLoader(),
                new Class[]{LoginService.class}, new DynamicProxy(new LoginServiceImpl()));
        loginService.login();
        GameService gameService = (GameService) Proxy.newProxyInstance(GameService.class.getClassLoader(),
                new Class[]{GameService.class}, new DynamicProxy(new DefaultGameService()));
        gameService.startGame();
    }
}
package proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Objects;

public class DynamicProxy implements InvocationHandler {

    private Object target;

    public DynamicProxy(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("new dynamic proxy instance");
        long before = System.currentTimeMillis();

        // method invoke는 첫 번째 인자 target 인스턴스를 받아
        // 인스턴스 안에 있는 메소드 실행을 담당
        Object result = method.invoke(this.target, args);

        long after = System.currentTimeMillis();

        System.out.println("total runtime : " + (after - before));
        return result;

    }
}

이렇게 java.lang.reflect.Proxy 패키지를 이용한 Dynamic Proxy를 활용하면

정적으로 proxy 객체를 만들 때 보다 중복 코드가 줄어들고 런타임 시 proxy 객체를 생성할 수 있습니다.

하지만 런타임 시 동적으로 proxy 객체를 생성하면 정적으로 생성했을 때 보다 실행 시간이 좀 느리다는 단점이 있습니다.

또한 타켓의 타입은 class가 아닌 무조건 interface로 인자를 넣어줘야 합니다. interface 말고 class를 넣게 되면 IllegalArgumentException 예외가 발생하게 됩니다. 

 

이러한 Dynamic Proxy기능을 Spring Framwork에서는 AOP로 제공을하게 되는데 이 부분에 대해서는 다음 포스트에서 다루어보도록 하겠습니다.

728x90

'ComputerScience > Design Pattern' 카테고리의 다른 글

Adaptor Pattern  (0) 2023.03.06
Facade Pattern  (0) 2023.02.26
Strategy Pattern  (0) 2023.02.20
디자인 패턴 - 디자인 원칙  (0) 2023.02.14
Builder Pattern  (0) 2023.02.01