프록시(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
이렇게 동적 프록시 기술을 사용하면 개발자가 직접 프록시 클래스를 만들어줄 필요 없이
런타임에 동적으로 프록시 클래스가 생성된다.
프록시(Proxy) 패턴 정리
특정 객체에 대한 접근을 제어하거나 기능을 추가할 수 있는 패턴
장점
기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있다
기존 코드가 해야 하는 일만 유지할 수 있다
기능 추가 및 초기화 지연 등으로 다양하게 활용할 수 있다
단점
코드의 복잡도가 증가한다
프록시(Proxy) 패턴 적용 사례
자바
다이나믹 프록시, java.lang.reflect.Proxy
스프링
스프링 AOP
참고자료
'Computer Sience > Desgin Pattern' 카테고리의 다른 글
[Design Pattern] 커맨드(Command) 패턴 (1) | 2024.10.31 |
---|---|
[Design Pattern] 책임 연쇄(Chain of Responsibility) 패턴 (1) | 2024.10.31 |
[Design Pattern] 플라이웨이트(Flyweight) 패턴 (1) | 2024.10.30 |
[Design Pattern] 퍼사드(Facade) 패턴 (1) | 2024.10.28 |
[Design Pattern] 데코레이터 (Decorator) 패턴 (0) | 2024.10.28 |