이 포스팅을 읽기 전에 이전 포스팅 ([김영한 스프링 review] 스프링 핵심 원리 - 고급편 (3) - 프로젝트 준비) 에 있는 예제코드를 미리 준비해주세요.!
import hello.proxy.app.v1.OrderControllerInterface;
import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class OrderControllerInterfaceProxy implements OrderControllerInterface {
private final OrderControllerInterface target;
private final LogTrace logTrace;
@Override
public String request(String itemId) {
TraceStatus status = null;
try {
status = logTrace.begin("OrderController.request()");
//target 호출
String result = target.request(itemId);
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
@Override
public String noLog() {
return target.noLog();
}
}
import hello.proxy.app.v1.OrderServiceInterface;
import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class OrderServiceInterfaceProxy implements OrderServiceInterface {
private final OrderServiceInterface target;
private final LogTrace logTrace;
@Override
public void orderItem(String itemId) {
TraceStatus status = null;
try {
status = logTrace.begin("OrderService.orderItem()");
//target 호출
target.orderItem(itemId);
logTrace.end(status);
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
import hello.proxy.app.v1.OrderRepositoryInterface;
import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class OrderRepositoryInterfaceProxy implements OrderRepositoryInterface {
private final OrderRepositoryInterface target;
private final LogTrace logTrace;
@Override
public void save(String itemId) {
TraceStatus status = null;
try {
status = logTrace.begin("OrderRepository.save()");
//target 호출
target.save(itemId);
logTrace.end(status);
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
import hello.proxy.app.v1.*;
import hello.proxy.config.v1_proxy.interface_proxy.OrderControllerInterfaceProxy;
import hello.proxy.config.v1_proxy.interface_proxy.OrderRepositoryInterfaceProxy;
import hello.proxy.config.v1_proxy.interface_proxy.OrderServiceInterfaceProxy;
import hello.proxy.trace.logtrace.LogTrace;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class InterfaceProxyConfig {
@Bean
public OrderControllerV1 orderController(LogTrace logTrace) {
OrderControllerV1Impl controllerImpl = new OrderControllerV1Impl(orderService(logTrace));
return new OrderControllerInterfaceProxy(controllerImpl, logTrace);
}
@Bean
public OrderServiceV1 orderService(LogTrace logTrace) {
OrderServiceV1Impl serviceImpl = new OrderServiceV1Impl(orderRepository(logTrace));
return new OrderServiceInterfaceProxy(serviceImpl, logTrace);
}
@Bean
public OrderRepositoryV1 orderRepository(LogTrace logTrace) {
OrderRepositoryV1Impl repositoryImpl = new OrderRepositoryV1Impl();
return new OrderRepositoryInterfaceProxy(repositoryImpl, logTrace);
}
}
// 빈 중복등록을 피하기 위해, 기존 컨피그를 주석처리한다.
// @Import({AppV1Config.class, AppV2Config.class})
@Import(InterfaceProxyConfig.class)
@SpringBootApplication(scanBasePackages = "{로직 패키지}")
public class ProxyApplication {
public static void main(String[] args) {
SpringApplication.run(ProxyApplication.class, args);
}
// 앞으로 사용할 예제에서 계속 사용하기 위해 여기에 빈 등록함
@Bean
public LogTrace logTrace() {
return new ThreadLocalLogTrace();
}
}
http://localhost:8080/v1/request?itemId=hello
[65b39db2] OrderController.request()
[65b39db2] |-->OrderService.orderItem()
[65b39db2] | |-->OrderRepository.save()
[65b39db2] | |<--OrderRepository.save() time=1002ms
[65b39db2] |<--OrderService.orderItem() time=1002ms
[65b39db2] OrderController.request() time=1003ms
아래 코드를 보면, 생성자에서 super(null)을 호출하는 것을 볼 수 있다.
이는 클래스 기반 프록시의 단점인데, 자바에서는 자식 클래스를 생성할 때 항상 super()를 통해 부모 생성자를 호출해야 한다. (생략 시 기본생성자)
하지만 부모 클래스인 OrderServiceConcrete는 기본 생성자가 없기 때문에 명시적으로 생성자를 호출해줘야한다. -> super(null)
프록시는 부모 객체의 기능을 사용하지 않기 때문에 super(null) 을 입력해도 무방하다. 인터페이스 기반 프록시는 이런 고민을 하지 않아도 된다.
import hello.proxy.app.v2.OrderControllerConcrete;
import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;
public class OrderControllerConcreteProxy extends OrderControllerConcrete {
private final OrderControllerConcrete target;
private final LogTrace logTrace;
public OrderControllerConcreteProxy(OrderControllerConcrete target, LogTrace logTrace) {
super(null);
this.target = target;
this.logTrace = logTrace;
}
@Override
public String request(String itemId) {
TraceStatus status = null;
try {
status = logTrace.begin("OrderController.request()");
//target 호출
String result = target.request(itemId);
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
import hello.proxy.app.v2.OrderServiceConcrete;
import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;
public class OrderServiceConcreteProxy extends OrderServiceConcrete {
private final OrderServiceConcrete target;
private final LogTrace logTrace;
public OrderServiceConcreteProxy(OrderServiceConcrete target, LogTrace logTrace) {
super(null);
this.target = target;
this.logTrace = logTrace;
}
@Override
public void orderItem(String itemId) {
TraceStatus status = null;
try {
status = logTrace.begin("OrderService.orderItem()");
//target 호출
target.orderItem(itemId);
logTrace.end(status);
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
import hello.proxy.app.v2.OrderRepositoryConcrete;
import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;
public class OrderRepositoryConcreteProxy extends OrderRepositoryConcrete {
private final OrderRepositoryConcrete target;
private final LogTrace logTrace;
public OrderRepositoryConcreteProxy(OrderRepositoryConcrete target, LogTrace logTrace) {
this.target = target;
this.logTrace = logTrace;
}
@Override
public void save(String itemId) {
TraceStatus status = null;
try {
status = logTrace.begin("OrderRepository.save()");
//target 호출
target.save(itemId);
logTrace.end(status);
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
import hello.proxy.app.v2.OrderControllerConcrete;
import hello.proxy.app.v2.OrderRepositoryConcrete;
import hello.proxy.app.v2.OrderServiceConcrete;
import hello.proxy.config.v1_proxy.concrete_proxy.OrderControllerConcreteProxy;
import hello.proxy.config.v1_proxy.concrete_proxy.OrderRepositoryConcreteProxy;
import hello.proxy.config.v1_proxy.concrete_proxy.OrderServiceConcreteProxy;
import hello.proxy.trace.logtrace.LogTrace;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ConcreteProxyConfig {
@Bean
public OrderControllerConcrete orderControllerConcrete(LogTrace logTrace) {
OrderControllerConcrete controllerImpl = new OrderControllerConcrete(orderServiceConcrete(logTrace));
return new OrderControllerConcreteProxy(controllerImpl, logTrace);
}
@Bean
public OrderServiceConcrete orderServiceConcrete(LogTrace logTrace) {
OrderServiceConcrete serviceImpl = new OrderServiceConcrete(orderRepositoryConcrete(logTrace));
return new OrderServiceConcreteProxy(serviceImpl, logTrace);
}
@Bean
public OrderRepositoryConcrete orderRepositoryConcrete(LogTrace logTrace) {
OrderRepositoryConcrete repositoryImpl = new OrderRepositoryConcrete();
return new OrderRepositoryConcreteProxy(repositoryImpl, logTrace);
}
}
//@Import({AppV1Config.class, AppV2Config.class})
//@Import(InterfaceProxyConfig.class)
@Import(ConcreteProxyConfig.class)
@SpringBootApplication(scanBasePackages = "{로직 패키지명}")
public class ProxyApplication {
public static void main(String[] args) {
SpringApplication.run(ProxyApplication.class, args);
}
@Bean
public LogTrace logTrace() {
return new ThreadLocalLogTrace();
}
}
프록시를 사용하여, 원본 로직에 손을 대지 않고 로그찍기 기능을 적용할 수 있었다.
인터페이스 기반이나, 클래스 기반이나 모두 프록시를 적용할 수 있었다.
다만, 클래스 기반 프록시는 상속의 단점을 모두 가져간다.
인터페이스 기반 프록시는 인터페이스만 같다면 어디에든 적용할 수 있다.
(인터페이스를 구현하는 하위 클래스에도 적용할 수 있다.)
반면에, 클래스 기반 프록시는 해당 클래스에만 적용할 수 있다.
인터페이스 기반 프록시에도 단점이 존재하는데,
지금까지 알아본 프록시의 가장 큰 단점은, 적용할 구체클래스, 인터페이스마다 매번
프록시 클래스를 만들어줘야 한다는 것이다.
우리는 실행 전/후로 로그를 찍는 기능 1개만 공통적으로 적용하는데, 이를 위해 OrderControllerInterfaceProxy, OrderServiceInterfaceProxy, OrderRepositoryInterfaceProxy, OrderContollerConcreteProxy, OrderServiceConcreteProxy, OrderRepositoryConcreteProxy 이렇게 6개를 만든것 처럼 말이다.
이는 이후에 다룰 동적 프록시 기술을 통해 해결할 수 있다.
다음 시간에는 동적 프록시의 근간이 되는 리플렉션에 대해 알아본다.