인터페이스 기반 프록시 적용
- 인터페이스를 기반으로 프록시를 적용한다면 아래와 같은 구조가 된다.
- 위의 그림처럼 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);
}
}
- 다음과 같이 인터페이스가 아닌 구체 클래스를 기반으로 프록시를 만들고 의존관계를 설정해준다.
정리
- 인터페이스 기반의 프록시는 상속이라는 제약에서 자유기 때문에 구체 클래스 방식보다 좋다.
- 프로그래밍 관점에서도 인터페이스를 사용하는 것이 역할과 구현을 명확하게 나누기 때문에 더 좋다.
- 하지만 무작정 인터페이스 방식이 좋은 것은 아니다. -> 인터페이스를 도입하는 것은 구현을 변경할 가능성이 있을 때 효과적 때문에 변할 가능성이 거의 없다면 구현 클래스 방식이 더 좋은 경우도 있다.
References
- 김영한님의 스프링 고급편
'객체지향' 카테고리의 다른 글
프록시 패턴과 데코레이터 패턴 (0) | 2022.08.06 |
---|---|
생성 패턴 (디자인 패턴) (0) | 2022.05.15 |
행위 패턴 (디자인 패턴) (0) | 2022.05.15 |
구조패턴 (디자인 패턴) (0) | 2022.05.15 |
클래스 다이어그램 (0) | 2022.04.23 |