22.09.30
구체 클래스 기반 프록시 - 예제1
인터페이스 없는 구체 클래스( ConcreteLogic )에 프록시 도입하기


구체 클래스 기반 프록시 - 예제2
클래스 기반 프록시 도입
자바의 다형성은 인터페이스를 구현하든, 아니면 클래스를 상속하든 상위 타입만 맞으면 다형성이 적용된다. 그래서 이번에는 인터페이스가 아니라 클래스를 기반으로 상속을 받아서 프록시를 만들어보자.
=> 부모 클래스 참조 변수는 자식 클래스 객체를 참조할 수 있다.


시간 측정 프록시 ( TimeProxy )
@Slf4j
public class TimeProxy extends ConcreteLogic{
private ConcreteLogic concreteLogic;
public TimeProxy(ConcreteLogic concreteLogic) {
this.concreteLogic = concreteLogic;
}
@Override
public String operation() {
log.info("TimeDecorator 실행");
long startTime = System.currentTimeMillis();
String result = concreteLogic.operation();
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeDecorator 종료, resultTime={}", resultTime);
return result;
}
}
- 인터페이스가 아니라 클래스인 ConcreteLogic 를 상속 받아서 만든다.
- 시간 측정 로직 operation() 안에서 부모의 operation() 을 호출하여 결과를 반환한다.
ConcreteProxyTest
public class ConcreteProxyTest {
@Test
void noProxyTest(){
ConcreteLogic concreteLogic = new ConcreteLogic();
ConcreteClient client = new ConcreteClient(concreteLogic);
client.execute();
}
@Test
void addProxyTest(){
ConcreteLogic concreteLogic = new ConcreteLogic();
TimeProxy timeProxy = new TimeProxy(concreteLogic);
ConcreteClient client = new ConcreteClient(timeProxy);
client.execute();
}
}
- ConcreteClient 의 생성자에 concreteLogic 이 아니라 timeProxy 를 주입한다.
- ConcreteClient 는 ConcreteLogic 을 의존하는데, 다형성에 의해 ConcreteLogic 에 concreteLogic 도 들어갈 수 있고, timeProxy 도 들어갈 수 있다.
ConcreteLogic에 할당할 수 있는 객체
ConcreteLogic = concreteLogic (본인과 같은 타입을 할당)
ConcreteLogic = timeProxy (자식 타입을 할당)
구체 클래스 기반 프록시 - 적용
구체 클래스만 있는 V2 애플리케이션에 프록시 기능을 적용
OrderServiceConcreteProxy
public class OrderServiceConcreteProxy extends OrderServiceV2 {
private final OrderServiceV2 target;
private final LogTrace logTrace;
public OrderServiceConcreteProxy(OrderServiceV2 target, LogTrace logTrace) {
super(null);
this.target = target;
this.logTrace = logTrace;
}
'상속' 을 통해 프록시를 구현한다.
클래스 기반 프록시의 단점
- super(null): 자바 기본 문법에 의해 자식 클래스를 생성할 때는 항상 super() 로 부모 클래스의 생성자를 호출해야 한다. 이 부분을 생략하면 자동으로 기본 생성자( super() )가 호출된다. 그런데 부모 클래스인 OrderServiceV2 는 기본 생성자가 없고, 생성자에서 파라미터 1개( OrderRepositoryV2 )를 필수로 받는다. 따라서 파라미터를 넣어서 super(..) 를 호출해야 한다.
- 프록시는 부모 객체의 기능을 사용하지 않기 때문에 super(null) 을 입력해도 된다.
- 인터페이스 기반 프록시는 이런 고민을 하지 않아도 된다.
스프링 빈 등록 설정 ( ConcreteProxyConfig )
@Configuration
public class ConcreteProxyConfig {
@Bean
public OrderRepositoryV2 orderRepositoryV2(LogTrace trace){
OrderRepositoryV2 orderRepositoryImpl = new OrderRepositoryV2();
return new OrderRepositoryConcreteProxy(orderRepositoryImpl, trace);
}
@Bean
public OrderServiceV2 orderServiceV2(LogTrace trace){
OrderServiceV2 orderServiceImpl = new OrderServiceV2(orderRepositoryV2(trace));
return new OrderServiceConcreteProxy(orderServiceImpl, trace);
}
@Bean
public OrderControllerV2 orderControllerV2(LogTrace trace){
OrderControllerV2 orderControllerImpl = new OrderControllerV2(orderServiceV2(trace));
return new OrderControllerConcreteProxy(orderControllerImpl, trace);
}
}
인터페이스 기반 프록시와 유사하다.
인터페이스 기반 프록시와 클래스 기반 프록시
프록시
프록시를 사용한 덕분에 원본 코드를 전혀 변경하지 않고, V1, V2 애플리케이션에 LogTrace 기능을 적용할 수 있었다.
인터페이스 기반 프록시 vs 클래스 기반 프록시
인터페이스가 없어도 클래스 기반으로 프록시를 생성할 수 있다.
클래스 기반 프록시는 해당 클래스에만 적용할 수 있다. 인터페이스 기반 프록시는 인터페이스만 같으면 모든 곳에 적용할 수 있다.
클래스 기반 프록시는 상속을 사용하기 때문에 몇가지 제약이 있다.
- 부모 클래스의 생성자를 호출해야 한다.(앞서 본 예제)
- 클래스에 final 키워드가 붙으면 상속이 불가능하다.
- 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다.
인터페이스 기반의 프록시는 상속이라는 제약에서 자유롭다. 프로그래밍 관점에서도 인터페이스를 사용하는 것이 역할과 구현을 명확하게 나누기 때문에 더 좋다.
인터페이스 기반 프록시의 단점은 인터페이스가 필요하다는 그 자체이다. 인터페이스가 없으면 인터페이스 기반 프록시를 만들 수 없다.
참고: 인터페이스 기반 프록시는 캐스팅 관련해서 단점이 있는데, 이 내용은 강의 뒷부문에서 설명...
이론적으로는 모든 객체에 인터페이스를 도입해서 역할과 구현을 나누는 것이 좋다. 이렇게 하면 구현체를 매우 편리하게 변경할 수 있다.
하지만 실제로는 구현을 거의 변경할 일이 없는 클래스도 많다.
인터페이스를 도입하는 것은 구현을 변경할 가능성이 있을 때 효과적인데, 구현을 변경할 가능성이 거의 없는 코드에 무작정 인터페이스를 사용하는 것은 번거롭고 그렇게 실용적이지 않다. 이런곳에는 실용적인 관점에서 인터페이스를 사용하지 않고 구체 클래스를 바로 사용하는 것이 좋다.
(물론 인터페이스를 도입하는 다양한 이유가 있다. 여기서 핵심은 인터페이스가 항상 필요하지는 않다는 것이다.)
너무 많은 프록시 클래스
지금까지 프록시를 사용해서 기존 코드를 변경하지 않고, 로그 추적기라는 부가 기능을 적용할 수 있었다. 그런데 문제는 프록시 클래스를 너무 많이 만들어야 한다는 점이다.
잘 보면 프록시 클래스가 하는 일은 LogTrace 를 사용하는 것인데, 그 로직이 모두 똑같다. 대상 클래스만 다를 뿐이다.
프록시 클래스를 하나만 만들어서 모든 곳에 적용하는 방법은 없을까?
-> 동적 프록시 기술