[DesignPattern] 프록시 (Proxy)
29 Dec 2023프록시 (Proxy)
다른 객체에 대한 접근을 제어하기 위한 대리자 또는 자리채움자 역할을 하는 객체를 둡니다.
동기
어떤 객체에 대한 접근을 제어하는 한 가지 이유는 실제로 그 객체를 사용할 수 있을 때까지 객체 생성과 초기화에 들어가는 비용 및 시간을 물지 않겠다는 것이다.
이미지와 같은 객체를 생성하는 데는 비용이 많이 들기 때문에, 문서가 읽히는 그 시점에서 모든 내용을 다 읽어올 필요가 없다.
그렇다면 실제 이미지 대역을 맡을 이미지프록시라는 또 다른 객체를 사용하는 것이다. 프록시는 이미지처럼 동작하고, 필요할 때 이미지의 인스턴스를 만들어 낸다.
이미지에 대한 작업에 대한 연산은 실제로 이미지가 인스턴스화 할때 요청을 이미지에 전달하고, 그전에는 프록시가 이미지를 대체해 크기정보를 직접 제공한다.
활용성
프록시 패턴은 단순한 포인터보다는 조금 더 다방면에 활용할 수 있거나 정교한 객체 참조자가 필요한 때 적용할 수 있다.
- 원격지 프록시(remote proxy) 는 서로 다른 주소 공간에 존재하는 객체를 가리키는 대표 객체로, 로컬환경에 위치한다.
- 가상 프록시(vitural proxy) 는 요청이 있을 때만 필요한 고비용 객체를 생성한다.
- 보호용 프록시(protection proxy) 는 원래 객체에 대한 실제 접근을 제어한다. 이는 객체별로 접근 제어 권한이 다를 때 유용하게 사용할 수 있다.
- 스마트 참조자(smart reference) 는 원시 포인터의 대체용 객체로, 실제 객체에 접근이 일어날 때 추가적인 행동을 수행합니다.
구조
여기서 프로그램 실행 중 프록시 구조를 객체 다이어그램으로 나타내면 다음과 같다.
- Proxy
- 실제로 참조할 대상에 대한 참조자를 관리합니다. RealSubject와 Subject 인터페이스가 동일하면 프록시는 Subject에 대한 참조자를 갖습니다.
- Subject와 동일한 인터페이스를 제공하여 실제 대항을 대체할 수 있어야 합니다.
- 실제 대상에 대한 접근을 제어하고 실제 대상의 생성과 삭제를 책임집니다.
- Proxy의 종류에 따라서 각자 목적을 위해 수행합니다.
원격지 프록시: 요청 메시지와 인자를 인코딩 하여 이를 다른 주소 공간에 있는 실제 대상에게 전달합니다.
가상의 프록시: 실제 대상에 대한 추가적 정보를 보유하여 실제 접근을 지연할 수 있도록 해야 합니다.
보호용 프록시: 요청한 대상이 실제 요청할 수 있는 권한이 있는지 확인합니다.
- Subject: RealSubject와 Proxy에 공통적인 인터페이스를 정의하여, RealSubject가 요청되는 곳에 Proxy를 사용할 수 있게 합니다.
- RealSubject: 프록시가 대표하는 실제 객체입니다.
결과
프록시 패턴은 어떤 객체에 접근할 때 추가적인 간접화 통로를 제공한다.
- 원격지 프록시는 객체가 다른 주소 공간에 존재한다는 사실을 숨길 수 있다.
- 가상 프록시는 요구에 따라 객체를 생성하는 등 처리를 최적화할 수 있다.
- 보호용 프록시 및 스마트 참조자는 객체가 접근할 때마다 추가 관리를 책임진다. 객체를 생성할 것인지 삭제할 것인지 관리한다.
장점과 단점
장점
- 기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있다.
- 기존 코드가 해야 하는 일만 유지할 수 있다.
- 기능 추가 및 초기화 지연 등으로 다양하게 활용할 수 있다.
단점
- 코드의 복잡도가 증가한다.
구현
적용전
프록시 패턴을 적용할 예시는 오래 걸리는 작업이라는 가정하에 리팩토링을 진행해 볼것이다.
용량이 큰 게임을 실행하는 서비스에 프록시 패턴을 적용해보자.
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 시키도록 할 수 있을 것이다.