프록시 패턴 Proxy Pattern
프록시 패턴
- 특정 객체에 대한 접근을 제어하거나 기능을 추가할 수 있는 패턴
- 초기화 지연, 접근 제어, 로깅, 캐싱 등 다양하게 응용해 사용 할 수 있다.
- 기존 코드의 변경 없이 새로운 기능을 추가할 수 있다.
- 데코레이터 패턴과 거의 유사하다. 가장 큰 차이는 객체 제어에 있다.
- 데코레이터는 행동을 추가한다.
- 프록시는 객체 자체를 제어한다.
프록시를 통한 캐시 구현
- Sum 인터페이스는
int execute()
메서드를 가지고 있다. - HeavyJob은 Sum을 구현한다. 언제나 0.3초를 소모하는 아주 비싼 작업이다.
public interface Sum {
int execute();
}
public class HeavyJob implements Sum {
private final int[] ints;
public HeavyJob(int[] ints) {
this.ints = ints;
}
@Override
public int execute() {
int result = 0;
for (int i : ints) {
result+=i;
}
sleep(300);
return result;
}
private static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
- 아래 로직의 결과는 1,2,3,4,5을 더한 15이다.
- 멱등한 결과를 굳이 반복하여 수행할 필요는 없다. 0.3초 * (요청한 횟수) 만큼 많은 자원을 소모하는 작업이면 더더욱 그렇다.
int[] ints = {1,2,3,4,5};
Sum job = new HeavyJob(ints);
printResult(job); // 0.3 초 소모
printResult(job); // 0.6 초 소모
printResult(job); // 0.9 초 소모
printResult(job); // 1.2 초 소모
- 기존 코드를 수정하지 않고 캐시 프록시를 활용하여 손쉽게 해결 가능하다.
- target의 수행 결과를 cache 필드에 저장한다. 그리고 결과값을 리턴하여 성능 상 유리하다.
- 아래 코드는 멀티스레드를 고려하지 않았다.
public class CacheHeavyJob implements Sum{
private final Sum target;
private Integer cache;
public CacheHeavyJob(Sum target) {
this.target = target;
}
@Override
public int execute() {
if(cache==null){
cache = target.execute();
System.out.println("캐싱 처리 저장!");
}
return cache;
}
}
int[] ints = {1,2,3,4,5};
Sum target = new HeavyJob(ints);
Sum proxy = new CacheHeavyJob(target);
printResult(proxy); // 0.3초
printResult(proxy); // 이하 캐시를 리턴
printResult(proxy);
printResult(proxy);
스프링 AOP
- 프록시나 데코레이터 패턴을 사용하려면 서브 클래스를 작성해야 한다. 코드 관리 및 복잡도가 증가한다.
- 스프링은 자바 리플렉션 기능을 활용한 ProxyFactory를 통하여 프록시 코드 작성의 복잡도를 대폭 줄여준다. 프록시로 사용할 코드 한 개로 여러 개의 객체에 대한 일관된 프록시를 런타임 시점에서 생성하도록 도와준다.
- 스프링 AOP 덕분에 우리는 프로덕션 코드에는 핵심 로직을 구현한다. 공통 관심사는 프록시로 구현하고, 해당 객체를 프록시로 감쌀 수 있게 되었다. SRP를 지킬 수 있다.
- 스프링의 가장 대표적인 프록시는 @Transactional 이다.
- 트랜잭션에 대한 공통 관심사를 비지니스 로직으로부터 분리한다.
- 어너테이션 하나로 같은 커넥션과 같은 트랜잭션을 여러 객체가 같이 사용할 수 있다.