프록시 패턴(Proxy Pattern)
특정 객체에 대한 접근을 제어하거나 기능을 추가할 수 있는 패턴
- 초기화 지연, 접근 제어, 로깅, 캐싱 등 다양하게 응용해 사용 할 수 있다
프록시 패턴 특징
- 원래 하려던 기능을 수행하며 그외의 부가적인 작업(로깅, 인증, 네트워크 통신 등)을 수행하기에 좋다.
- 비용이 많이 드는 연산(DB 쿼리, 대용량 파일 등)을 실제로 필요한 시점에 수행할 수 있다.
- 사용자 입장에서는 프록시 객체나 실제 객체나 사용법은 유사하므로 사용성이 좋다.
프록시 패턴의 종류
가상프록시
꼭 필요로 하는 시점까지 객체의 생성을 연기하고, 해당 객체가 생성된 것 처럼 동작하도록 만들고 싶을 때 사용하는 패턴이다. 프록시 클래스에서 작은 단위의 작업을 처리하고 리소스가 많이 요구되는 작업들이 필요할 경우만 주체 클래스를 사용하도록 구현한다.
원격프록시
원격 객체에 대한 접근을 제어 로컬 환경에 존재하며, 원격 객체에 대한 대변자 역할을 하는 객체 서로 다른 주소 공간에 있는 객체에 대해 마치 같은 주소 공간에 있는 것 처럼 동작하게 하는 패턴이다.
보호프록시
주체 클래스에 대한 접근을 제어하기 위한 경우에 객체에 대한 접근 권한을 제어하거나 객체마다 접근 권한을 달리하고 싶을 경우 사용하는 패턴으로 프록시 클래스에서 클라이언트가 주체 클래스에 대한 접근을 허용할지 말지 결정하도록 할 수 있다.
프록시 패턴 적용 전
Client 가 startGame 이 실행되고 종료되기까지 얼마나 시간이 걸리는지 알기 위해서는 Client 코드의 main 시작 부분과 startGame 마지막 부분에 시간을 재면 된다.
public class Client {
public static void main(String[] args) {
GameService gameService = new GameService();
gameService.startGame();
}
}
public class GameService {
public void startGame() {
System.out.println("게임을 시작합니다.");
}
}
프록시 패턴 적용 후
방법 1
GameService 를 전혀 손대지 않고 측정하는 방법을 살펴보자.
(여기에선 시간을 고의적으로 지연시키기 위해 sleep 을 사용한 것이다.)
public class Client {
public static void main(String[] args) throws InterruptedException {
GameService gameService = new GameServiceProxy(); //프록시 사용
gameService.startGame();
}
}
public class GameService {
public void startGame() throws InterruptedException {
System.out.println("게임을 시작합니다.");
Thread.sleep(1000L);
}
}
public class GameServiceProxy extends GameService {
@Override
public void startGame() throws InterruptedException {
long before = System.currentTimeMillis();
super.startGame();
System.out.println(System.currentTimeMillis() - before);
}
}
Service 클래스를 상속받는 ServiceProxy(프록시 클래스) 를 만들어 startGame 메서드를 오버라이드 하였고, 이 곳에 시간 측정 코드를 추가하였다.
이 방법은 기존코드를 전혀 변경하지 않고 프록시 패턴을 적용하는 방법이였다.
방법2
이번에는 아래 프록시 패턴 클래스 다이어그램을 적용해보자.
Client 를 보면 프록시 안에 사용할 Service 객체를 넣어주고 있다.
public class Client {
public static void main(String[] args) {
// Client 가 DefaultGameService 를 쓰기 위해서는 프록시를 거쳐야함.
GameService gameService = new GameServiceProxy(new DefaultGameService());
gameService.startGame();
}
}
DefaultGameService 는 코드를 더 유연하게 하기 위해 GameService 인터페이스를 정의하고 구현하였다.
// Subject interface
public interface GameService {
void startGame();
}
// Real Subject interface
public class DefaultGameService implements GameService {
@Override
public void startGame() {
System.out.println("게임을 시작합니다!");
}
}
프록시는 이전 데코레이터 패턴에서 본 것 처럼 GameService 를 가지고 있고, 이를 구현하고 있다.
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);
}
}
프록시의 startGame 메서드를 보면 수행시간을 측정하는 로직과 Client 한테 받은 GameService 타입 객체의 startGame 메서드를 실행시키고 있다. 이렇게 하면 시간 측정외에도 다양한 로직을 추가할 수 있고, GameService 도 유연하게 변경할 수 있다.
또한 프록시 객체 내에서 GameService 를 결정하게 하여 지연 초기화(Lazy Initiallization)를 적용 할 수 있다.
아래 코드를 살펴보자.
public class GameServiceProxy implements GameService {
private GameService gameService;
@Override
public void startGame() {
long before = System.currentTimeMillis();
// Lazy Initiallization
// 지연 로딩 외에도 특정 로직을 삽입하여 gameService 를 원하는 객체로 넣을 수 있다.
if(this.gameService == null) this.gameService = new DefaultGameService();
gameService.startGame();
System.out.println(System.currentTimeMillis() - before);
}
}
public class Client {
public static void main(String[] args) {
// Client 가 DefaultGameService 를 쓰기 위해서는 프록시를 거쳐야함.
GameService gameService = new GameServiceProxy();
gameService.startGame();
}
}
장점
- 기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있다
- 기존 코드가 해야 하는 일만 유지할 수 있다
- 기능 추가 및 초기화 지연 등으로 다양하게 활용할 수 있다
단점
- 코드의 복잡도가 증가한다