지난 포스팅에서는 프록시에 대해 알아보았다.
프록시는 일종의 대리자로서 실체 객체 대신에 호출이 되어 부가기능을 수행하고, 실제 객체를 대신 호출해주는 역할을 한다.
프록시의 장점은 핵심 로직은 전혀 수정하지 않고 부가 기능을 추가할 수 있다는 것이다.
그럼 기존에 봤던 로그 추적기에 프록시를 적용하면 코드가 어떻게 바뀌는지 확인해보자.
컨트롤러와 서비스를 이용해 의존 관계가 어떻게 형성되는지 확인해보자.
@RequestMapping
@ResponseBody
public interface OrderControllerV1 {
// 로그 적용
@GetMapping("/v1/request")
String request(@RequestParam("itemId") String itemId);
// 로그 미적용
@GetMapping("v1/no-log")
String noLog();
}
public class OrderControllerV1Impl implements OrderControllerV1 {
private final OrderServiceV1 orderService;
public OrderControllerV1Impl(OrderServiceV1 orderService) {
this.orderService = orderService;
}
@Override
public String request(String itemId) {
orderService.orderItem(itemId);
return "ok";
}
@Override
public String noLog() {
return "ok";
}
}
@RequiredArgsConstructor
public class OrderControllerInterfaceProxy implements OrderControllerV1 {
private final OrderControllerV1 target;
private final LogTrace logTrace;
@Override
public String request(String itemId) {
TraceStatus status = null;
try {
status = logTrace.begin("OrderController.request()");
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();
}
}
public interface OrderServiceV1 {
void orderItem(String itemId);
}
public class OrderServiceV1Impl implements OrderServiceV1 {
private final OrderRepositoryV1 orderRepository;
public OrderServiceV1Impl(OrderRepositoryV1 orderRepository) {
this.orderRepository = orderRepository;
}
@Override
public void orderItem(String itemId) {
orderRepository.save(itemId);
}
}
@RequiredArgsConstructor
public class OrderServiceInterfaceProxy implements OrderServiceV1 {
private final OrderServiceV1 target;
private final LogTrace logTrace;
@Override
public void orderItem(String itemId) {
TraceStatus status = null;
try {
status = logTrace.begin("OrderService.orderItem()");
target.orderItem(itemId);
logTrace.end(status);
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
@Configuration
public class InterfaceProxyConfig {
@Bean
public OrderControllerV1 orderController(LogTrace trace) {
// OrderControllerV1Impl에 orderService의 프록시를 주입
OrderControllerV1Impl controllerImpl = new OrderControllerV1Impl(orderService(trace));
return new OrderControllerInterfaceProxy(controllerImpl, trace);
}
@Bean
public OrderServiceV1 orderService(LogTrace trace) {
// OrderServiceV1Impl에 orderRepository의 프록시를 주입
OrderServiceV1Impl serviceImpl = new OrderServiceV1Impl(orderRepository(trace));
return new OrderServiceInterfaceProxy(serviceImpl, trace);
}
}
여기까지는 인터페이스를 구현하고 있어 지난 포스팅에서 봤던 방식과 다르지 않다.
하지만 따로 인터페이스가 존재하지 않는 어플리케이션도 많은데 이런 경우에는 프록시를 어떻게 적용할 수 있을까?
@Slf4j
@RequestMapping
@ResponseBody
public class OrderControllerV2 {
private final OrderServiceV2 orderService;
public OrderControllerV2(OrderServiceV2 orderService) {
this.orderService = orderService;
}
@GetMapping("/v2/request")
public String request(String itemId) {
orderService.orderItem(itemId);
return "ok";
}
@GetMapping("/v2/no-log")
public String noLog() {
return "ok";
}
}
public class OrderControllerConcreteProxy extends OrderControllerV2 {
private final OrderControllerV2 target;
private final LogTrace logTrace;
public OrderControllerConcreteProxy(OrderControllerV2 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()");
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();
}
}
public class OrderServiceV2 {
private final OrderRepositoryV2 orderRepository;
public OrderServiceV2(OrderRepositoryV2 orderRepository) {
this.orderRepository = orderRepository;
}
public void orderItem(String itemId) {
orderRepository.save(itemId);
}
}
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;
}
@Override
public void orderItem(String itemId) {
TraceStatus status = null;
try {
status = logTrace.begin("OrderService.orderItem()");
target.orderItem(itemId);
logTrace.end(status);
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
@Configuration
public class ConcreteProxyConfig {
@Bean
public OrderControllerV2 orderControllerV2(LogTrace trace) {
OrderControllerV2 controllerImpl = new OrderControllerV2(orderServiceV2(trace));
return new OrderControllerConcreteProxy(controllerImpl,trace);
}
@Bean
public OrderServiceV2 orderServiceV2(LogTrace trace) {
OrderServiceV2 serviceImpl = new OrderServiceV2(orderRepositoryV2(trace));
return new OrderServiceConcreteProxy(serviceImpl,trace);
}
}
인터페이스 기반 프록시는 인터페이스만 같다면 프록시를 적용할 수 있다.
클래스 기반 프록시는 상속에 따른 제약이 몇가지 있다.
인터페이스의 가장 큰 장점은 구현체 변경이 편리하다는 것이다. 하지만 구현체를 변경할 가능성이 적은 곳에서 굳이 인터페이스를 생성해서 사용하는 것은 번거롭다. 따라서 상황에 적합한 방법을 사용하자.
이번 포스팅에서는 인터페이스 기반으로 프록시를 생성하는 방법과 클래스 기반으로 프록시를 생성하는 방법에 대해 알아보았다.
인터페이스를 사용할 경우 인터페이스를 구현해 프록시를 생성하면 되고 클래스를 사용할 경우 상속을 이용해 프록시를 생성하면 된다. 스프링에는 프록시를 빈으로 등록하고, 각 프록시에 실제 객체를 주입 받는다. 또 각 실제 객체에는 다시 프록시를 주입해준다.
그런데 여기서 드러나는 문제가 있다.
프록시를 적용하는 클래스마다 프록시 클래스를 새로 생성해야 하는 것이다. 그만큼 반복되는 코드가 늘어나는 것이다.
그렇다면 하나의 프록시 클래스만 만들어서 모든 클래스에 적용하는 방법은 없을까?
다음 포스팅에서 알아보자.