특정 객체에 대한 접근을 제어하거나 기능을 추가할 수 있는 패턴
초기화 지연, 접근 제어, 로깅, 캐싱 등 다양하게 응용해 사용할 수 있다.
간단한 코드로 프록시 패턴에 대해 알아보도록 하자.
public class GameService {
public void startGame() {
System.out.println("이 자리에 오신 여러분을 진심으로 환영합니다.");
}
}
위와 같은 GameService
클래스가 있다고 가정하자.
그런데 다음과 같은 요구사항이 주어졌다고 하자.
이때 사용할 수 있는 방법이 Proxy 패턴인데 2가지의 방법이 있다.
1. 상속을 이용하여 프록시 패턴 구현
2. 인터페이스를 이용하여 프록시 패턴 구현
// Proxy
public class GameServiceInheritanceProxy extends DefaultGameService {
@Override
public void startGame() {
long before = System.currentTimeMillis();
super.startGame();
System.out.println(System.currentTimeMillis() - before);
}
}
// 실행
public class Client {
public static void main(String[] args) {
GameService gameService = new GameServiceInheritanceProxy();
gameService.startGame();
}
}
// 실행 결과
이 자리에 오신 여러분을 진심으로 환영합니다.
0
// 인터페이스
public interface GameService {
void startGame();
}
// 구현체
public class DefaultGameService implements GameService{
@Override
public void startGame() {
System.out.println("이 자리에 오신 여러분을 진심으로 환영합니다.");
}
}
// Proxy
public class GameServiceInterfaceProxy implements GameService{
private GameService gameService;
// startGame 메소드를 실행해야 비로소 초기화를 해주는 방식
@Override
public void startGame() {
long before = System.currentTimeMillis();
if(gameService == null) {
this.gameService = new DefaultGameService();
}
gameService.startGame();
System.out.println(System.currentTimeMillis() - before);
}
}
// 실행
public class Client {
public static void main(String[] args) {
GameService gameService = new GameServiceInterfaceProxy();
gameService.startGame();
}
}
// 실행 결과
이 자리에 오신 여러분을 진심으로 환영합니다.
0
이렇게 간단하게 정적으로 프록시 패턴을 구현하는 것에 대해 알아봤다.
그런데 자바에서는 리플렉션을 이용하여 동적으로 프록시를 구현할 수 있다.
이에 대해 알아보자.
public class ProxyInJava {
public static void main(String[] args) {
ProxyInJava proxyInJava = new ProxyInJava();
proxyInJava.dynamicProxy();
proxyInJava.dynamicProxy2();
}
private void dynamicProxy() {
GameService gameServiceProxy = getGameServiceProxy(new DefaultGameService());
gameServiceProxy.startGame();
}
private void dynamicProxy2() {
GameService gameServiceProxy = initGameServiceProxy();
gameServiceProxy.startGame();
}
// 람다를 이용하여 간단하게 구현
private GameService getGameServiceProxy(GameService target) {
return (GameService) Proxy.newProxyInstance(
this.getClass().getClassLoader(),
new Class[]{GameService.class},
(proxy, method, args) -> {
System.out.println("Hello dynamic proxy");
method.invoke(target, args);
return null;
}
);
}
// 메소드를 실행해야 비로소 초기화되게끔 구현
private GameService initGameServiceProxy() {
return (GameService) Proxy.newProxyInstance(
this.getClass().getClassLoader(),
new Class[]{GameService.class},
new InvocationHandler() {
GameService target;
@Override
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
if(target == null) {
this.target = new DefaultGameService();
}
System.out.println("Hello dynamic proxy");
target.startGame();
return null;
}
}
);
}
}
스프링에서는 JDK Dynamic Proxy와 CGLIB Proxy를 사용한다.
위에서 설명한 동적으로 프록시 패턴 구현한 것이 JDK Dynamic Proxy이다.
Dynamic Proxy는 반드시 Interface가 정의되어야 하고, 이 Interface를 기준으로 Proxy를 생성한다.
따라서 Interface 선언에 대한 강제성이 있다는 단점이 있다.
또한 Reflection을 사용하여 기능을 구현하기 때문에 퍼포먼스 하락의 원인이 되기도 한다.
CGLIB Proxy는 Enhancer
를 바탕으로 Proxy를 구현하는 방식이다.
이 방식은 JDK Dynamic Proxy와는 다르게 Reflection을 사용하지 않고 상속 방식을 이용해서 Proxy화 할 메서드를 오버라이딩 하는 방식이다.
CGLIB는 클래스의 바이트코드를 조작해서 Proxy를 만들기때문에 JDK Dynamic Proxy보다 성능적으로 우세하다. 다만 final 객체 혹은 private 접근자로 된 메소드는 상속이 지우너되지 않기 때문에 제약적인 Proxy 구현에서 단점으로 지적된다.
https://gmoon92.github.io/spring/aop/2019/04/20/jdk-dynamic-proxy-and-cglib.html
https://velog.io/@hanblueblue/Spring-Proxy-1-Java-Dynamic-Proxy-vs.-CGLIB