DEV ℧ Developer Diary

[DesignPattern] 프록시 (Proxy)

프록시 (Proxy)

다른 객체에 대한 접근을 제어하기 위한 대리자 또는 자리채움자 역할을 하는 객체를 둡니다.

동기

어떤 객체에 대한 접근을 제어하는 한 가지 이유는 실제로 그 객체를 사용할 수 있을 때까지 객체 생성과 초기화에 들어가는 비용 및 시간을 물지 않겠다는 것이다.

이미지와 같은 객체를 생성하는 데는 비용이 많이 들기 때문에, 문서가 읽히는 그 시점에서 모든 내용을 다 읽어올 필요가 없다.

그렇다면 실제 이미지 대역을 맡을 이미지프록시라는 또 다른 객체를 사용하는 것이다. 프록시는 이미지처럼 동작하고, 필요할 때 이미지의 인스턴스를 만들어 낸다.

이미지에 대한 작업에 대한 연산은 실제로 이미지가 인스턴스화 할때 요청을 이미지에 전달하고, 그전에는 프록시가 이미지를 대체해 크기정보를 직접 제공한다.

활용성

프록시 패턴은 단순한 포인터보다는 조금 더 다방면에 활용할 수 있거나 정교한 객체 참조자가 필요한 때 적용할 수 있다.

  1. 원격지 프록시(remote proxy) 는 서로 다른 주소 공간에 존재하는 객체를 가리키는 대표 객체로, 로컬환경에 위치한다.
  2. 가상 프록시(vitural proxy) 는 요청이 있을 때만 필요한 고비용 객체를 생성한다.
  3. 보호용 프록시(protection proxy) 는 원래 객체에 대한 실제 접근을 제어한다. 이는 객체별로 접근 제어 권한이 다를 때 유용하게 사용할 수 있다.
  4. 스마트 참조자(smart reference) 는 원시 포인터의 대체용 객체로, 실제 객체에 접근이 일어날 때 추가적인 행동을 수행합니다.

구조

프록시 구조1

여기서 프로그램 실행 중 프록시 구조를 객체 다이어그램으로 나타내면 다음과 같다.

프록시 구조2

  • Proxy
    • 실제로 참조할 대상에 대한 참조자를 관리합니다. RealSubject와 Subject 인터페이스가 동일하면 프록시는 Subject에 대한 참조자를 갖습니다.
    • Subject와 동일한 인터페이스를 제공하여 실제 대항을 대체할 수 있어야 합니다.
    • 실제 대상에 대한 접근을 제어하고 실제 대상의 생성과 삭제를 책임집니다.
    • Proxy의 종류에 따라서 각자 목적을 위해 수행합니다.

원격지 프록시: 요청 메시지와 인자를 인코딩 하여 이를 다른 주소 공간에 있는 실제 대상에게 전달합니다.
가상의 프록시: 실제 대상에 대한 추가적 정보를 보유하여 실제 접근을 지연할 수 있도록 해야 합니다.
보호용 프록시: 요청한 대상이 실제 요청할 수 있는 권한이 있는지 확인합니다.

  • Subject: RealSubject와 Proxy에 공통적인 인터페이스를 정의하여, RealSubject가 요청되는 곳에 Proxy를 사용할 수 있게 합니다.
  • RealSubject: 프록시가 대표하는 실제 객체입니다.

결과

프록시 패턴은 어떤 객체에 접근할 때 추가적인 간접화 통로를 제공한다.

  1. 원격지 프록시는 객체가 다른 주소 공간에 존재한다는 사실을 숨길 수 있다.
  2. 가상 프록시는 요구에 따라 객체를 생성하는 등 처리를 최적화할 수 있다.
  3. 보호용 프록시 및 스마트 참조자는 객체가 접근할 때마다 추가 관리를 책임진다. 객체를 생성할 것인지 삭제할 것인지 관리한다.

장점과 단점

장점

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

단점

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

구현

적용전

프록시 패턴을 적용할 예시는 오래 걸리는 작업이라는 가정하에 리팩토링을 진행해 볼것이다.

용량이 큰 게임을 실행하는 서비스에 프록시 패턴을 적용해보자.

GameService는 게임을 실행 후 동작하는 로직, Client는 게임을 실행하는 로직을 나타낸다.

GameService

public class GameService {
    public void startGame() {
        System.out.println("게임을 실행해 보도록 하겠습니다.");
    }
}

Client

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

적용후

먼저 Class로 되어있는 GameService 인터페이스로 변경해준다. GameService에 대한 수정이 힘들때 상속으로 프록시를 구현할 수 있지만, 인터페이스로 구현하는 것이 테스트코드 작성이 용이하고, 유연한 코드가 된다. 그리고 인터페이스를 구현하는 기본 구현체 DefaultGameService를 생성해준다.

GameService

public interface GameService {
    void startGame();
}

DefaultGameService

public class DefaultGameService implements GameService{
    @Override
    public void startGame() {
        System.out.println("게임을 시작합니다.");
    }
}

GameServiceProxy는 GameService의 프록시로 프록시 내부에서는 GameService를 구현한 DefaultGameService를 사용하게 된다.

정리하자면 DefaultGameService를 사용하기 위해서는 GameServiceProxy를 거쳐야만 로직을 실행할 수 있게 된다.

GameServiceProxy

public class GameServiceProxy implements GameService{

    private GameService gameService;

    public GameServiceProxy(GameService gameService) {
        this.gameService = gameService;
    }

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

이제 Client에서 DefaultGameService를 인자로 받은 프록시 객체를 실행할 수 있다.

Client

public class Client {

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

이렇게 된다면 DefaultGameService는 원래 하던 일인 게임을 실행한다. 에 집중을 할 수 있게되고, 그외 예를들어 권한체크나, 실행시간 기록과 같은 부가기능은 GameServiceProxy를 통해 붙여 나갈 수 있다.

추가적으로 GameServiceProxy 내부의 startGame 메서드가 return 타입을 가지고 있다면 기존의 DefaultGameService에서 반환하는 값과 다른 값을 return 시키도록 할 수 있을 것이다.