위의 그림처럼 Controller와 Service를 인터페이스로 구현하고, 실제 구현체와 같은 인터페이스를 구현하도록 프록시를 만든다.
이렇게 되면 런타임 시점에는 아래와 같이 의존 관계가 설정된다/
구현
Repository, RepositoryProxy
public class OrderRepositoryV1Impl implements OrderRepositoryV1 {
@Override
public void save(String itemId) {
//... 생략
}
}
@RequiredArgsConstructor
public class OrderRepositoryInterfaceProxy implements OrderRepositoryV1 {
private final OrderRepositoryV1 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;
}
}
}
위와 같이 OrderRepositoryV1를 구현하도록 OrderRepositoryInterfaceProxy를 구현한다.
또한 실제 구현체(target)를 주입 받을 수 있도록 OrderRepositoryV1타입의 target을 변수로 가지고 있는다.
로그를 남기고 실제 구현체를 실행시키는 것을 확인할 수 있다.
이렇게되면, 실제 구현체를 건드리지 않고도 로그를 남기는 기능을 추가할 수 있다.
Service, ServiceProxy
@RequiredArgsConstructor
public class OrderServiceV1Impl implements OrderServiceV1 {
private final OrderRepositoryV1 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 호출
target.orderItem(itemId);
logTrace.end(status);
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
OrderServiceInterfaceProxy도 위와 같은 방식으로 프록시를 구현한다.
Controller, ControllerProxy
@RequiredArgsConstructor
public class OrderControllerV1Impl implements OrderControllerV1 {
private final OrderServiceV1 orderService;
@Override
public String request(String itemId) {
orderService.orderItem(itemId);
return "ok";
}
@Override
public String noLog() {
return "ok";
}
}
@RequiredArgsConstructor
public class OrderControllerV1Impl implements OrderControllerV1 {
private final OrderServiceV1 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()"); //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();
}
}
Config
@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);
}
}
위와 같이 의존 관계를 설정해 주어야 한다.
orderRepository는 실제 구현체를 반환하는 것이 아니라 프록시를 반환한다.
OrderRepositoryV1Impl(실제 구현체)는 빈으로 등록되지 않지만, 프록시에 포함 되어 있기 때문에 프록시를 통해서 호출할 수 있다.
프록시 객체는 스프링 컨테이너가 관리하고 자바 힙 메모리에도 올라간다. 반면에 실제 객체는 자바 힙 메모리에는 올라가지만 스프링 컨테이너가 관리하지는 않는다.
따라서 스프링 컨테이너에는 아래와 같이 빈이 등록된다.
구체 클래스 기반 프록시
인터페이스가 아닌 경우라도 프록시를 적용할 수 있다.
Repository, RepositoryProxy
public class OrderRepositoryV2 {
public void save(String itemId) {
//...생략
}
}
public class OrderRepositoryConcreteProxy extends OrderRepositoryV2 {
private final OrderRepositoryV2 target;
private final LogTrace logTrace;
public OrderRepositoryConcreteProxy(OrderRepositoryV2 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;
}
}
}
OrderRepositoryV2를 상속받아 OrderRepositoryConcreteProxy를 만든다.
실제 구현체를 주입을 받을 수 있도록 OrderRepositoryV2 타입의 target을 변수로 가지고 있는다.
구현 방식은 인터페이스 방식과 거의 비슷하다.
Service, ServiceProxy
@RequiredArgsConstructor
public class OrderServiceV2 {
private final OrderRepositoryV2 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 호출
target.orderItem(itemId);
logTrace.end(status);
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
인터페이스 방식과 차이점은 생성자에서 super(null)을 담아 줘야한다는 것이다.
java 문법상 super(null)를 생략하면 super()가 기본으로 호출 되기 때문이다.
이 부분이 구체 클래스 방식의 단점이다.
Controller, ControllerProxy
@Slf4j
@RequestMapping
@ResponseBody
@RequiredArgsConstructor
public class OrderControllerV2 {
private final OrderServiceV2 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()"); //target 호출
String result = target.request(itemId);
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
OrderControllerConcreteProxy 도 마찬가지로 OrderControllerV2를 상속받으며, OrderControllerV2 타입이 target을 변수로 가지고 있다.
또한, super(null)을 호출했다.
프록시는 부모 객체의 기능을 사용하지 않기 때문에 super(null) 을 입력해도 된다.
Config
@Configuration
public class ConcreteProxyConfig {
@Bean
public OrderControllerV2 orderControllerV2(LogTrace logTrace) {
OrderControllerV2 controllerImpl = new OrderControllerV2(orderServiceV2(logTrace));
return new OrderControllerConcreteProxy(controllerImpl, logTrace);
}
@Bean
public OrderServiceV2 orderServiceV2(LogTrace logTrace) {
OrderServiceV2 serviceImpl = new OrderServiceV2(orderRepositoryV2(logTrace));
return new OrderServiceConcreteProxy(serviceImpl, logTrace);
}
@Bean
public OrderRepositoryV2 orderRepositoryV2(LogTrace logTrace) {
OrderRepositoryV2 repositoryImpl = new OrderRepositoryV2();
return new OrderRepositoryConcreteProxy(repositoryImpl, logTrace);
}
}
다음과 같이 인터페이스가 아닌 구체 클래스를 기반으로 프록시를 만들고 의존관계를 설정해준다.
정리
인터페이스 기반의 프록시는 상속이라는 제약에서 자유기 때문에 구체 클래스 방식보다 좋다.
프로그래밍 관점에서도 인터페이스를 사용하는 것이 역할과 구현을 명확하게 나누기 때문에 더 좋다.
하지만 무작정 인터페이스 방식이 좋은 것은 아니다. -> 인터페이스를 도입하는 것은 구현을 변경할 가능성이 있을 때 효과적 때문에 변할 가능성이 거의 없다면 구현 클래스 방식이 더 좋은 경우도 있다.