proxy라는 새로운 프로젝트를 생성하였습니다.
다양한 상황에서 프록시 사용법을 이해하기 위해
예제는 크게 3가지 상황으로 만들었습니다.
OrderRepositoryV1
package hello.proxy.app.v1;
public interface OrderRepositoryV1 {
void save(String itemId);
}
OrderRepositoryV1Impl
package hello.proxy.app.v1;
public class OrderRepositoryV1Impl implements OrderRepositoryV1 {
@Override
public void save(String itemId) {
//저장 로직
if (itemId.equals("ex")) {
throw new IllegalStateException("예외 발생!");
}
sleep(1000);
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
OrderServiceV1
package hello.proxy.app.v1;
public interface OrderServiceV1 {
void orderItem(String itemId);
}
OrderServiceV1Impl
package hello.proxy.app.v1;
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);
}
}
OrderControllerV1
package hello.proxy.app.v1;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@RequestMapping //스프링은 @Controller 또는 @RequestMapping이 있어야 스프링 컨트롤러로 인식
@ResponseBody
public interface OrderControllerV1 {
@GetMapping("/v1/request")
String request(@RequestParam("itemId") String itemId);
@GetMapping("/v1/no-log")
String noLog();
}
@RequestMapping
: 스프링MVC는 타입에 @Controller
또는 @RequestMapping
애노테이션이 있어야 스프링 컨트롤러로 인식한다. 그리고 스프링 컨트롤러로 인식해야, HTTP URL이 매핑되고 동작한다. 이 애노테이션은 인터페이스에 사용해도 된다.@ResponseBody
: HTTP 메시지 컨버터를 사용해서 응답한다. 이 애노테이션은 인터페이스에 사용해도 된다.OrderControllerV1Impl
package hello.proxy.app.v1;
import lombok.extern.slf4j.Slf4j;
@Slf4j
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";
}
}
AppV1Config
package hello.proxy.config;
import hello.proxy.app.v1.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppV1Config {
@Bean
public OrderControllerV1 orderControllerV1() {
return new OrderControllerV1Impl(orderServiceV1());
}
@Bean
public OrderServiceV1 orderServiceV1() {
return new OrderServiceV1Impl(orderRepositoryV1());
}
@Bean
public OrderRepositoryV1 orderRepositoryV1() {
return new OrderRepositoryV1Impl();
}
}
package hello.proxy;
import hello.proxy.config.AppV1Config;
import hello.proxy.config.AppV2Config;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;
@Import(AppV1Config.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app") //주의
public class ProxyApplication {
public static void main(String[] args) {
SpringApplication.run(ProxyApplication.class, args);
}
}
@Import(AppV1Config.class)
: 클래스를 스프링 빈으로 등록한다. 여기서는 AppV1Config.class
를 스프링 빈으로 등록한다. 일반적으로 @Configuration
같은 설정 파일을 등록할 때 사용하지만, 스프링 빈을 등록할 때도 사용할 수 있다.@Configuration
은 내부에 @Component
애노테이션을 포함하고 있어서 컴포넌트 스캔의 대상이 된다. 따라서 컴포넌트 스캔에 의해 hello.proxy.config
위치의 설정 파일들이 스프링 빈으로 자동 등록 되지 않도록 컴포넌스 스캔의 시작 위치를 scanBasePackages=hello.proxy.app
로 설정해야 한다.OrderRepositoryV2
package hello.proxy.app.v2;
public class OrderRepositoryV2 {
public void save(String itemId) {
//저장 로직
if (itemId.equals("ex")) {
throw new IllegalStateException("예외 발생!");
}
sleep(1000);
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
OrderServiceV2
package hello.proxy.app.v2;
public class OrderServiceV2 {
private final OrderRepositoryV2 orderRepository;
public OrderServiceV2(OrderRepositoryV2 orderRepository) {
this.orderRepository = orderRepository;
}
public void orderItem(String itemId) {
orderRepository.save(itemId);
}
}
OrderControllerV2
package hello.proxy.app.v2;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@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";
}
}
@Controller
를 사용하지 않고, @RequestMapping
애노테이션을 사용했다. 그 이유는 @Controller
를 사용하면 자동 컴포넌트 스캔의 대상이 되기 때문이다. 여기서는 컴포넌트 스캔을 통한 자동 빈 등록이 아니라 수동 빈 등록을 하는 것이 목표다. 따라서 컴포넌트 스캔과 관계 없는 @RequestMapping
를 타입에 사용했다.AppV2Config
package hello.proxy.config;
import hello.proxy.app.v2.OrderControllerV2;
import hello.proxy.app.v2.OrderRepositoryV2;
import hello.proxy.app.v2.OrderServiceV2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppV2Config {
@Bean
public OrderControllerV2 orderControllerV2() {
return new OrderControllerV2(orderServiceV2());
}
@Bean
public OrderServiceV2 orderServiceV2() {
return new OrderServiceV2(orderRepositoryV2());
}
@Bean
public OrderRepositoryV2 orderRepositoryV2() {
return new OrderRepositoryV2();
}
}
ProxyApplication
package hello.proxy;
import hello.proxy.config.AppV1Config;
import hello.proxy.config.AppV2Config;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;
@Import({AppV1Config.class, AppV2Config.class})
@SpringBootApplication(scanBasePackages = "hello.proxy.app") //주의
public class ProxyApplication {
public static void main(String[] args) {
SpringApplication.run(ProxyApplication.class, args);
}
}
OrderRepositoryV3
package hello.proxy.app.v3;
import org.springframework.stereotype.Repository;
@Repository
public class OrderRepositoryV3 {
public void save(String itemId) {
//저장 로직
if (itemId.equals("ex")) {
throw new IllegalStateException("예외 발생!");
}
sleep(1000);
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
OrderServiceV3
package hello.proxy.app.v3;
import org.springframework.stereotype.Service;
@Service
public class OrderServiceV3 {
private final OrderRepositoryV3 orderRepository;
public OrderServiceV3(OrderRepositoryV3 orderRepository) {
this.orderRepository = orderRepository;
}
public void orderItem(String itemId) {
orderRepository.save(itemId);
}
}
OrderControllerV3
package hello.proxy.app.v3;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
public class OrderControllerV3 {
private final OrderServiceV3 orderService;
public OrderControllerV3(OrderServiceV3 orderService) {
this.orderService = orderService;
}
@GetMapping("/v3/request")
public String request(String itemId) {
orderService.orderItem(itemId);
return "ok";
}
@GetMapping("/v3/no-log")
public String noLog() {
return "ok";
}
}
@RestController
, @Service
, @Repository
애노테이션을 가지고 있기 때문에 컴포넌트 스캔의 대상이 된다.기존 요구사항에 다음 요구사항이 추가
이 문제를 해결하려면 프록시(Proxy)의 개념을 먼저 이해해야 한다.
클라이언트와 서버
직접 호출과 간접 호출
서버와 프록시가 같은 인터페이스 사용
Client -> Server
에서 Client -> Proxy
로 객체 의존관계를 변경해도 클라이언트 코드를 전혀 변경하지 않아도 된다. 클라이언트 입장에서는 변경 사실 조차 모른다.프록시의 주요 기능
GOF 디자인 패턴
둘다 프록시를 사용하는 방법이지만 GOF 디자인 패턴에서는 이 둘을 의도(intent)에 따라서 프록시 패턴과 데코레이터 패턴으로 구분한다.
프록시와 프록시 패턴은 다르다.
먼저 프록시 패턴을 도입하기 전 코드를 아주 단순하게 만들어보자.
Subject
package hello.proxy.pureproxy.proxy.code;
public interface Subject {
String operation();
}
RealSubject
package hello.proxy.pureproxy.proxy.code;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class RealSubject implements Subject{
@Override
public String operation() {
log.info("실제 객체 호출");
sleep(1000);
return null;
}
private void sleep(int millis) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
ProxyPatternTest
package hello.proxy.pureproxy.proxy;
import hello.proxy.pureproxy.proxy.code.ProxyPatternClient;
import hello.proxy.pureproxy.proxy.code.RealSubject;
import org.junit.jupiter.api.Test;
public class ProxyPatternTest {
@Test
void noProxyTest() {
RealSubject realSubject = new RealSubject();
ProxyPatternClient client = new ProxyPatternClient(realSubject);
client.execute();
client.execute();
client.execute();
}
}
client.execute()
을 3번 호출하면 1초 씩 걸려서 값을 조회한다.
프록시 패턴을 적용해보자.
CacheProxy
package hello.proxy.pureproxy.proxy.code;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CacheProxy implements Subject{
private Subject target;
private String cacheValue;
public CacheProxy(Subject target) {
this.target = target;
}
@Override
public String operation() {
log.info("프록시 호출");
if(cacheValue == null){
cacheValue = target.operation();
}
return cacheValue;
}
}
Subject
인터페이스를 구현해야 한다.private Subject target
: 클라이언트가 프록시를 호출하면 프록시가 최종적으로 실제 객체를 호출해야 한다. 따라서 내부에 실제 객체의 참조를 가지고 있어야 한다. 이렇게 프록시가 호출하는 대상을 target
이라 한다.operation()
: 구현한 코드를 보면 cacheValue
에 값이 없으면 실제 객체(target
)를 호출해서 값을 구한다. 그리고 구한 값을 cacheValue
에 저장하고 반환한다. 만약 cacheValue
에 값이 있으면 실제 객체를 전혀 호출하지 않고, 캐시 값을 그대로 반환한다. 따라서 처음 조회 이후에는 캐시(cacheValue
)에서 매우 빠르게 데이터를 조회할 수 있다.ProxyPatternTest 코드 추가
@Test
void cacheProxyTest() {
Subject realSubject = new RealSubject();
Subject cacheProxy = new CacheProxy(realSubject);
ProxyPatternClient client = new ProxyPatternClient(cacheProxy);
client.execute();
client.execute();
client.execute();
}
오류
이렇게 나오면 안된다. 3번 다 호출하고 있다.
RealSubject
@Override
public String operation() {
log.info("실제 객체 호출");
sleep(1000);
// return null; // 오류
return "data";
}
"data"
를 리턴하니 오류가 해결됬다.호출하면 다음과 같이 처리된다.
1. client의 cacheProxy 호출 cacheProxy에 캐시 값이 없다. realSubject를 호출, 결과를 캐시에 저장 (1초)
2. client의 cacheProxy 호출 cacheProxy에 캐시 값이 있다. cacheProxy에서 즉시 반환 (0초)
3. client의 cacheProxy 호출 cacheProxy에 캐시 값이 있다. cacheProxy에서 즉시 반환 (0초)
RealSubject
코드와 클라이언트 코드를 전혀 변경하지 않고, 프록시를 도입해서 접근 제어를 했다는 점이다.먼저 데코레이터 패턴을 도입하기 전 코드를 아주 단순하게 만들어보자.
Component
package hello.proxy.pureproxy.decorator.code;
public interface Component {
String operation();
}
RealComponent
package hello.proxy.pureproxy.decorator.code;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class RealComponent implements Component {
@Override
public String operation() {
log.info("RealComponent 실행");
return "data";
}
}
RealComponent
는Component
인터페이스를 구현한다.operation()
: 단순히 로그를 남기고 "data"
문자를 반환한다.DecoratorPatternClient
package hello.proxy.pureproxy.decorator.code;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class DecoratorPatternClient {
private Component component;
public DecoratorPatternClient(Component component) {
this.component = component;
}
public void execute(){
String result = component.operation();
log.info("result={}", result);
}
}
Component
인터페이스를 의존한다.execute()
를 실행하면 component.operation()
을 호출하고, 그 결과를 출력한다.DecoratorPatternTest
package hello.proxy.pureproxy.decorator;
import hello.proxy.pureproxy.decorator.code.Component;
import hello.proxy.pureproxy.decorator.code.DecoratorPatternClient;
import hello.proxy.pureproxy.decorator.code.RealComponent;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
@Slf4j
public class DecoratorPatternTest {
@Test
void noDecorator() {
Component realComponent = new RealComponent();
DecoratorPatternClient client = new DecoratorPatternClient(realComponent);
client.execute();
}
}
앞서 프록시 패턴에서 설명한 내용과 유사하다.
부가 기능 추가
이번에는 프록시를 활용해서 부가 기능을 추가해보자. 이렇게 프록시로 부가 기능을 추가하는 것을 데코레이터 패턴이라 한다.
MessageDecorator
package hello.proxy.pureproxy.decorator.code;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MessageDecorator implements Component{
private Component component;
public MessageDecorator(Component component) {
this.component = component;
}
@Override
public String operation() {
log.info("MessageDecorator 실행");
String result = component.operation();
String decoResult = "데코***" + result + "***데코";
log.info("MessageDecorator 꾸미기 적용 전={}, 적용 후={}",result,decoResult);
return decoResult;
}
}
DecoratorPatternTest 코드 추가
@Test
void decorator1() {
Component realComponent = new RealComponent();
Component messageDecorator = new MessageDecorator(realComponent);
DecoratorPatternClient client = new DecoratorPatternClient(messageDecorator); // 의존관계 주입
client.execute();
}
MessageDecorator
가 RealComponent
를 호출하고 반환한 응답 메시지를 꾸며서 반환한 것을 확인할 수 있다.TimeDecorator
package hello.proxy.pureproxy.decorator.code;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class TimeDecorator implements Component{
private Component component;
public TimeDecorator(Component component) {
this.component = component;
}
@Override
public String operation() {
log.info("-----TimeDecorator 실행-----");
long startTime = System.currentTimeMillis();
String result = component.operation();
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}ms", resultTime);
log.info("-----TimeDecorator 종료-----");
return result;
}
}
DecoratorPatternTest 코드 추가
@Test
void decorator2() {
Component realComponent = new RealComponent();
Component messageDecorator = new MessageDecorator(realComponent);
Component timeDecorator = new TimeDecorator(messageDecorator); // 추가 주입
DecoratorPatternClient client = new DecoratorPatternClient(timeDecorator);
client.execute();
}
TimeDecorator
가 MessageDecorator
를 실행하고 실행 시간을 측정해서 출력한 것을 확인할 수 있다.GOF 데코레이터 패턴
여기서 생각해보면 Decorator
기능에 일부 중복이 있다. 꾸며주는 역할을 하는 Decorator
들은 스스로 존재할 수 없다. 항상 꾸며줄 대상이 있어야 한다. 따라서 내부에 호출 대상인 component
를 가지고 있어야 한다. 그리고 component
를 항상 호출해야 한다. 이 부분이 중복이다. 이런 중복을 제거하기 위해 component
를 속성으로 가지고 있는 Decorator
라는 추상 클래스를 만드는 방법도 고민할 수 있다.
프록시 패턴 vs 데코레이터 패턴
의도(intent)
인터페이스와 구현체가 있는 V1 App에 지금까지 학습한 프록시를 도입해서 LogTrace
를 사용해보자.
프록시를 사용하면 기존 코드를 전혀 수정하지 않고 로그 추적 기능을 도입할 수 있다.
먼저 기존에 V1 클래스 의존 관계를 살펴보자
V1 런타임 객체 의존 관계
V1 프록시 의존 관계 추가
V1 프록시 런타임 객체 의존 관계
OrderRepositoryInterfaceProxy
package hello.proxy.config.v1_proxy.interface_proxy;
import hello.proxy.app.v1.OrderRepositoryV1;
import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;
import lombok.RequiredArgsConstructor;
@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.request()");
target.save(itemId);
logTrace.end(status);
}catch (Exception e){
logTrace.exception(status, e);
throw e;
}
}
}
OrderServiceInterfaceProxy
package hello.proxy.config.v1_proxy.interface_proxy;
import hello.proxy.app.v1.OrderServiceV1;
import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;
import lombok.RequiredArgsConstructor;
@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;
}
}
}
OrderControllerInterfaceProxy
package hello.proxy.config.v1_proxy.interface_proxy;
import hello.proxy.app.v1.OrderControllerV1;
import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;
import lombok.RequiredArgsConstructor;
@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();
}
}
InterfaceProxyConfig
package hello.proxy.config.v1_proxy;
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 {
// OrderControllerInterfaceProxy -> OrderControllerImpl -> OrderServiceInterfaceProxy -> serviceImpl -> OrderRepositoryInterfaceProxy -> repositoryImpl
@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);
}
}
V1 프록시 런타임 객체 의존 관계 설정
orderControlerV1Impl
,orderServiceV1Impl
같은 실제 객체를 반환했다.OrderServiceInterfaceProxy
는 내부에 실제 대상 객체인 OrderServiceV1Impl
을 가지고 있다.스프링 컨테이너 - 프록시 적용 전
스프링 컨테이너 - 프록시 적용 후
ProxyApplication 변경
//@Import({AppV1Config.class, AppV2Config.class})
@Import(InterfaceProxyConfig.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app") //주의
public class ProxyApplication {
public static void main(String[] args) {
SpringApplication.run(ProxyApplication.class, args);
}
@Bean
public LogTrace logTrace() {
return new ThreadLocalLogTrace();
}
}
애플리케이션 실행 로그 확인
이번에는 구체 클래스에 프록시를 적용하는 방법을 학습해보자.
ConcreteLogic
package hello.proxy.pureproxy.concreteproxy.code;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ConcreteLogic {
public String operation() {
log.info("ConcreteLogic 실행");
return "data";
}
}
ConcreteClient
package hello.proxy.pureproxy.concreteproxy.code;
public class ConcreteClient {
private ConcreteLogic concreteLogic;
public ConcreteClient(ConcreteLogic concreteLogic) {
this.concreteLogic = concreteLogic;
}
public void execute() {
concreteLogic.operation();
}
}
ConcreteProxyTest
package hello.proxy.pureproxy.concreteproxy;
import hello.proxy.pureproxy.concreteproxy.code.ConcreteClient;
import hello.proxy.pureproxy.concreteproxy.code.ConcreteLogic;
import org.junit.jupiter.api.Test;
public class ConcreteProxyTest {
// 프록시 없는 버전
@Test
void noProxy() {
ConcreteLogic concreteLogic = new ConcreteLogic();
ConcreteClient client = new ConcreteClient(concreteLogic);
client.execute();
}
}
TimeProxy
package hello.proxy.pureproxy.concreteproxy.code;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class TimeProxy extends ConcreteLogic{
private ConcreteLogic realLogic;
public TimeProxy(ConcreteLogic realLogic){
this.realLogic = realLogic;
}
@Override
public String operation(){
log.info("TimeDecorator 실행");
long startTime = System.currentTimeMillis();
String result = realLogic.operation();
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeDecorator 종료 resultTime={}", resultTime);
return result;
}
}
TimeProxy
프록시는 시간을 측정하는 부가 기능을 제공한다. 그리고 인터페이스가 아니라 클래스인 ConcreteLogic
를 상속 받아서 만든다.ConcreteLogic에 할당할 수 있는 객체
ConcreteLogic = concreteLogic
(본인과 같은 타입을 할당)ConcreteLogic = timeProxy
(자식 타입을 할당)ConcreteProxyTest - 코드 추가
// 프록시 이용
@Test
void addProxy() {
ConcreteLogic concreteLogic = new ConcreteLogic();
TimeProxy timeProxy = new TimeProxy(concreteLogic); // 의존관계 주입
ConcreteClient client = new ConcreteClient(timeProxy);
client.execute();
}
자바 언어에서 다형성은 인터페이스나 클래스를 구분하지 않고 모두 적용된다. 해당 타입과 그 타입의 하위 타입은 모두 다형성의 대상이 된다.
OrderRepositoryConcreteProxy
package hello.proxy.config.v1_proxy.concrete_proxy;
import hello.proxy.app.v2.OrderRepositoryV2;
import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;
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.save(itemId);
logTrace.end(status);
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
OrderRepositoryV2
클래스를 상속 받아서 프록시를 만든다.OrderServiceConcreteProxy
package hello.proxy.config.v1_proxy.concrete_proxy;
import hello.proxy.app.v2.OrderServiceV2;
import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;
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;
}
}
}
OrderServiceV2
클래스를 상속 받아서 프록시를 만든다.super(null)
을 입력해도 된다. (자식 클래스를 생성할 때는 항상 super()
로 부모 클래스의 생성자를 호출해야 된다.)OrderControllerConcreteProxy
package hello.proxy.config.v1_proxy.concrete_proxy;
import hello.proxy.app.v2.OrderControllerV2;
import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;
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;
}
}
}
ConcreteProxyConfig
package hello.proxy.config.v1_proxy;
import hello.proxy.app.v2.OrderControllerV2;
import hello.proxy.app.v2.OrderRepositoryV2;
import hello.proxy.app.v2.OrderServiceV2;
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 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);
}
}
ProxyApplication
//@Import({AppV1Config.class, AppV2Config.class})
//@Import(InterfaceProxyConfig.class)
@Import(ConcreteProxyConfig.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app") //주의
public class ProxyApplication {
애플리케이션 실행 로그 확인
프록시
프록시를 사용한 덕분에 원본 코드를 전혀 변경하지 않고, V1, V2 애플리케이션에 LogTrace
기능을 적용할 수 있었다.
인터페이스 기반 프록시 vs 클래스 기반 프록시
참고
김영한: 스프링 핵심 원리 - 고급편(인프런)
Github - https://github.com/b2b2004/Spring_ex