내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.
1. 인터페이스 기반 프록시 - 적용
프록시를 사용하면 기존 코드를 전혀 수정하지 않고 로그 추적 기능을 도입할 수 있다!
로그 추적 기능을 도입해보자!
잠시 V1 버전의 클래스 의존 관계를 살펴보면 다음과 같다.
Controller , Service , Repository 각각 인터페이스에 맞는 프록시 구현체를 추가하면, 의존 관계는 다음과 같다.
Repository의 의존 관계 그림 부분은 생략하였다.
런타임 의존 관계는 다음과 같아진다.
그리고 애플리케이션 실행 시점에 프록시를 사용하도록 의존 관계를 설정해주어야 한다.
이 부분은 빈을 등록하는 설정 파일을 활용하면 된다. (그림에서 리포지토리는 생략했다.)
코드를 통하여 살펴보자.
우선 Repository, Service, Controller에 대한 Proxy부터 만들어보자!
▶ OrderRepositoryInterfaceProxy
@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;
}
}
}
프록시를 만들기 위해 인터페이스를 구현하고 구현한 메서드에 LogTrace 를 사용하는 로직을 추가한다.
프록시를 사용한 덕분에 기존 코드인 OrderRepositoryV1 자체의 코드는 수정되지 않는다.
▶ OrderServiceInterfaceProxy
@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
@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();
}
}
이제 의존관계를 설정할 config 파일을 만들어보자!
▶ InterfaceProxyConfig
@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);
}
}
기존에는 스프링 빈이 orderControlerV1Impl , orderServiceV1Impl 같은 실제 객체를 반환했다.
하지만 이제는 프록시를 사용해야한다.
따라서 프록시를 생성하고 프록시를 실제 스프링 빈 대신 등록한다. 실제 객체는 스프링 빈으로 등록하지 않는다.
또한 Bean으로 실제객체 대신에 프록시 객체를 등록했기 때문에, 앞으로 스프링 빈을 주입 받으면 실제 객체 대신에 프록시 객체가 주입된다.
이때 Bean으로 프록시가 등록된다고 해서 실제객체가 소멸하는 것 은 아니다.
실제객체도 Heap 메모리에 존재한다. 다만 Bean으로 등록된 프록시 들은 Heap 과 Spring Container 둘다에 등록되는것 이다.
스프링 컨테이너에 프록시 객체가 등록된다. 스프링 컨테이너는 이제 실제 객체가 아니라 프록시 객체를 스프링 빈으로 관리한다.
애플리케이션 실행을 위해 다음과 같이 설정해보자!
@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();
}
}
@Bean : 먼저 LogTrace 스프링 빈 추가를 먼저 해주어야 한다.
Config 파일이 아닌, 여기에 등록한 이유는 앞으로 사용할 모든 예제에서 함께 사용하기 때문이다.
@Import(InterfaceProxyConfig.class) : 프록시를 적용한 설정 파일을 사용하자.
실행해보면 정상적으로 로그 추적 기능이 프록시를 통해 작동하는것을 확인할 수 있다.
2. 구체 클래스 기반 프록시 - 예제1
이번에는 인터페이스 없이 구체 클래스만을 가지고 Proxy를 만들어보자!
우선 프록시를 도입하기 전, 기본 코드를 만들자.
▶ ConcreteLogic
@Slf4j
public class ConcreteLogic {
public String operation() {
log.info("ConcreteLogic 실행");
return "data";
}
}
의존성은 다음과 같이 매우 간단한다. 인터페이스가 없기 때문이다.
클라이언트 코드는 다음과 같다.
▶ ConcreteClient
public class ConcreteClient {
private ConcreteLogic concreteLogic;
public ConcreteClient(ConcreteLogic concreteLogic) {
this.concreteLogic = concreteLogic;
}
public void execute() {
concreteLogic.operation();
}
}
이를 사용하는 간단한 테스트 코드는 다음과 같다.
public class ConcreteProxyTest {
@Test
void noProxy() {
ConcreteLogic concreteLogic = new ConcreteLogic();
ConcreteClient client = new ConcreteClient(concreteLogic);
client.execute();
}
}
실행시 다음과 같다.
간단하게 한줄 나오고 끝난다.
다음 단락에서 여기에 Proxy를 적용시켜 보자!
3. 구체 클래스 기반 프록시 - 예제2
자바의 다형성은 인터페이스를 구현하든, 아니면 클래스를 상속하든 상위 타입만 맞으면 다형성이 적용된다.
즉, 인터페이스가 없어도 프록시를 만들수 있다는 뜻이다.
따라서 이번에는 인터페이스가 아니라 클래스를 기반으로 상속을 받아서 프록시를 만들어보겠다.
즉 다음 그림처럼 의존성을 만들 것 이다.
프록시를 ConcreteLogic을 상속하여 만들 것 이다.
▶ TimeProxy
@Slf4j
public class TimeProxy extends ConcreteLogic {
private ConcreteLogic concreteLogic;
public TimeProxy(ConcreteLogic concreteLogic) {
this.concreteLogic = concreteLogic;
}
@Override
public String operation() {
log.info("TimeDecorator 실행");
long startTime = System.currentTimeMillis();
String result = concreteLogic.operation();
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeDecorator 종료 resultTime={}", resultTime);
return result;
}
}
이를 활용하는 테스트 코드는 다음고 같다.
@Test
void addProxy() {
ConcreteLogic concreteLogic = new ConcreteLogic();
TimeProxy timeProxy = new TimeProxy(concreteLogic);
ConcreteClient client = new ConcreteClient(timeProxy);
client.execute();
}
위 코드에서 핵심은 ConcreteClient 의 생성자에 concreteLogic 이 아니라 timeProxy 를 주입하는 부분이다.
ConcreteClient 는 ConcreteLogic타입을 받아 생성자에서 사용한다.
다형성에 의해 ConcreteLogic 에 concreteLogic 도 들어갈 수 있고, ConcreteLogic를 상속한 timeProxy 도 들어갈 수 있다.
실행 결과는 다음과 같다.
실행 결과를 보면 인터페이스가 없어도 클래스 기반의 프록시가 잘 적용된 것을 확인할 수 있다.
이어지는 글에서는 구체 클래스를 사용하는 프록시로 우리의 로그 추적기를 구현해보자!
4. 구체 클래스 기반 프록시 - 적용
이번 단락에서는 구체 클래스만 있는 V2 애플리케이션에 프록시 기능을 적용해보자!
우선 각각의 proxy 코드를 살펴보자
▶ OrderRepositoryConcreteProxy
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.request()");
target.save(itemId);
logTrace.end(status);
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
▶ OrderServiceConcreteProxy
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;
}
}
}
구체 클래스를 상속하는 방식의 Proxy에는 단점이 있다.
자바 기본 문법에 의해 자식 클래스를 생성할 때는 항상 super() 로 부모 클래스의 생성자를 먼저 호출해야 한다.
이 부분을 생략하면 자동으로 기본 생성자가 호출된다.
하지만 부모 클래스인 OrderServiceV2 는 기본 생성자가 없고, 생성자에서 파라미터 1개를 필수로 받는다.
따라서 자식 클래스의 생성자에서는 파라미터를 넣어서 super(null)을 호출해야 한다.
다행이 Proxy는 부모의 기능을 전혀 사용하지 않는다. 따라서 생성자에 null을 넘겨 부모를 초기화 해도 된다.
▶ OrderControllerConcreteProxy
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();
}
}
직전의 Service 와 동일한 문제가 있다.
▶ ConcreteProxyConfig
@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 orderServiceImpl = new OrderServiceV2(orderRepositoryV2(logTrace));
return new OrderServiceConcreteProxy(orderServiceImpl, logTrace);
}
@Bean
public OrderRepositoryV2 orderRepositoryV2(LogTrace logTrace) {
OrderRepositoryV2 repositoryImpl = new OrderRepositoryV2();
return new OrderRepositoryConcreteProxy(repositoryImpl, logTrace);
}
}
이후 다음과 같이 ProxyApplication 을 실행할 때 Config파일을 지정해주면 된다!
@Import(ConcreteProxyConfig.class) // 변경된 config
@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();
}
}
다음 URL로 요청을 보내보자!
http://localhost:8080/v2/request?itemId=hello
실행해보면 클래스 기반 프록시도 잘 동작하는 것을 확인할 수 있다.
5. 인터페이스 기반 프록시와 클래스 기반 프록시
인터페이스 기반 프록시 vs 클래스 기반 프록시
클래스 기반 프록시는 해당 클래스에만 적용할 수 있다.
인터페이스 기반 프록시는 인터페이스만 같으면 모든 곳에 적용할 수 있다.
클래스 기반 프록시는 상속을 사용하기 때문에 몇가지 제약이 있다.
- 부모 클래스의 생성자를 호출해야 한다.(앞서 본 예제)
- 클래스에 final 키워드가 붙으면 상속이 불가능하다.
- 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다.
위 단점만 보면 인터페이스 기반의 Proxy만 사용해야 할 것 같다?
물론 인터페이스 기반으 Proxy가 상속의 단점으로부터 자유로운 것은 사실이다, 또한 역할과 구현을 명확하게 나누기 때문에 더 좋다.
이론적으로는 모든 객체에 인터페이스를 도입해서 역할과 구현을 나누는 것이 좋다.
이렇게 하면 역할과 구현을 나누어서 구현체를 매우 편리하게 변경할 수 있다.
하지만 실제로는 구현을 거의 변경할 일이 없는 클래스도 많다.
인터페이스를 도입하는 것은 구현을 변경할 가능성이 있을 때 효과적인데, 구현을 변경할 가능성이 거의 없는 코드에 무작정 인터페이스를 사용하는 것은 번거롭고 그렇게 실용적이지 않다.
이런곳에는 그냥 구체 클래스를 바로 사용하는것이 더 합리적일수도 있다.
결론 :
실무에서는 프록시를 적용할 때 V1처럼 인터페이스도 있고, V2처럼 구체 클래스도 있다. 따라서 2가지 상황을 모두 대응할 수 있어야 한다.
다만, 지금 우리의 코드는 Proxy를 만들때 너무나 많은 클래스들이 생성된다.
잘 보면 프록시 클래스가 하는 일은 LogTrace 를 사용하는 것인데, 그 로직이 모두 똑같다.
대상 클래스만 다를 뿐이다. 만약 적용해야 하는 대상 클래스가 100개라면 프록시 클래스도 100개를 만들어야한다.
프록시 클래스를 하나만 만들어서 모든 곳에 적용하는 방법은 없을까?
바로 다음에 설명할 동적 프록시 기술이 이 문제를 해결해준다.
'BackEnd > Spring' 카테고리의 다른 글
[Spring] 스프링이 지원하는 프록시 - 1 (0) | 2022.08.14 |
---|---|
[Spring] 동적 프록시 기술 - JDK, CGLIB (0) | 2022.08.13 |
[Spring] 데코레이터 패턴 (0) | 2022.08.10 |
[Spring] 프록시 패턴 (0) | 2022.08.10 |
[Spring] 프록시 패턴과 데코레이터 패턴 예제 (0) | 2022.08.09 |
댓글