앞서, 프록시의 역할과 기능 및 사용법에 대해서 알아보았다.
이제, 앞서 만들었던 v1(인터페이스가 있는 구현 클래스)에 프록시를 적용하여 LogTrace 기능을 활용하는 방법에 대해서 알아보자.
즉, 프록시를 적용하여 기존 코드를 전혀 수정하지 않으면서 로그 추적 기능을 도입하는 방법에 대해서 구체적으로 파악해보자는 것이다.
V1 App 의존관계
v1 App의 기본 의존 관계(클래스 의존)와 런타임시 객체 인스턴스의 의존 관계는 아래의 그림과 같다.
V1 App 기존 의존관계
V1 기본 클래스 의존관계

V2 런타임 객체 의존 관계

여기에 로그 추적 기능을 담당하는 프록시를 추가하면 다음과 같은 형태를 지닌다.
V1 App 프록시 도입 클래스 의존관계
V1 프록시 도입 기본 클래스 의존관계

V1 프록시 도입 런타임 객체 의존관계

프록시를 도입하여 클라이언트 → [ 컨트롤러 프록시 → 실제 컨트롤러 ] → [ 서비스 프록시 → 실제 서비스 ] 와 같은 형태를 지니게 된다.
이제 V1 인터페이스와 Impl 구현체를 사용하는 의존관계에서 프록시 객체를 도입한 형태를 적용하기 위한 과정을 살펴보자.
먼저, Repository 인터페이스 기반의 프록시를 만든다.
public interface OrderRepositoryV1 {
void save(String itemId);
}
@RequiredArgsConstructor
public class OrderRepositoryInterfaceProxy implements OrderRepositoryV1 {
// real repository object
private final OrderRepositoryV1 target;
private final LogTrace logTrace;
@Override
public void save(String itemId) {
TraceStatus status = null;
try {
status = logTrace.begin("OrderRepository.request()");
//target call
target.save(itemId);
logTrace.end(status);
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
프록시를 구성하기 위해서 인터페이스를 구현하는 메서드에 LogTrace를 사용하는 로직을 추가하였다.
이는 실제 구현체인 Impl 객체에 로직을 전부 추가하는 수고로움을 제거한 것이다.
이어서, Service와 Controller의 로직 구성도 살펴보자.
public interface OrderServiceV1 {
void orderItem(String 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;
}
}
}
@RequestMapping//스프링은 @Controller 또는 @RequestMapping 이 있어야 스프링 컨트롤러로 인식
@ResponseBody
public interface OrderControllerV1 {
@GetMapping("/v1/request")
String request(@RequestParam("itemId") String itemId);
@GetMapping("/v1/no-log")
String noLog();
}
@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();
}
}
위와 같은 소스 코드를 통해 컨트롤러 - 서비스 - 레포지토리 각각의 인터페이스와 해당 인터페이스를 구현한 프록시 구현체를 구성하였다.
이제 구현체들을 스프링에 Bean으로 등록하여 런타임에 프록시 객체가 사용될 수 있도록 해아한다.
@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);
}
}
@Import(InterfaceProxyConfig.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app") //주의
public class ProxyApplication {
public static void main(String[] args) {
SpringApplication.run(ProxyApplication.class, args);
}
// LogTrace Bean 등록
@Bean
public LogTrace logTrace() {
return new ThreadLocalLogTrace();
}
}
지금까지 인터페이스 기반의 프록시 객체를 만들어 구현체의 로직 변경없이 적용하는 구성의 로직이며,
스프링에 수동 빈 등록하여 [ 프록시 호출 → 로깅 → 구현체 호출 ]의 흐름을 갖추었다.
그리고 스프링 컨테이너 내부에는 프록시 적용 전후로 다음과 같은 변화를 가진다.


프록시를 적용하고 난 뒤, 스프링 컨테이너에 프록시 객체가 등록되며 관리된다.
그리고 실제 객체는 스프링 컨테이너와 상관없으며, 프록시 객체를 통해 참조되어 사용된다.
내부적으로 프록시 객체는 스프링 컨테이너가 관리하며 자바 힙 메모리에 할당되어 사용된다. 반면 실제 객체는 자바 힙 메모리에 올라가지만 스프링 컨테이너의 관리를 받지는 않는다.
[65b39db2] OrderController.request()
[65b39db2] |-->OrderService.orderItem()
[65b39db2] | |-->OrderRepository.save()
[65b39db2] | |<--OrderRepository.save() time=1002ms
[65b39db2] |<--OrderService.orderItem() time=1002ms
[65b39db2] OrderController.request() time=1003ms
실행 결과 로그도 잘 동작하는 모습을 확인할 수도 있다.
따라서, 원본 코드를 수정하지 않으며 로그 추적기를 적용하고 특정 메소드는 로그를 출력하지 않는 기능의 요구사항을 적용하였다.
그 방법들 중 인터페이스 기반의 구현 클래스인 AppV1 적용하는 방법에 대해 알아보았으며, 프록시와 DI가 사용되어 내부적으로 발생하는 스프링의 구성까지 정리했다.
다음에는 이어서 인터페이스가 없는 구체 클래스 기반의 구성에서 프록시를 적용하는 방법에 대해 알아보자.
'Spring > SpringCore - advance' 카테고리의 다른 글
| 스프링 핵심원리( 심화 ) - 동적 프록시(리플렉션) (0) | 2026.01.26 |
|---|---|
| 스프링 핵심원리( 심화 ) - 구체클래스 기반 프록시 (0) | 2026.01.14 |
| 스프링 핵심원리( 심화 ) - 프록시 패턴과 데코레이터 패턴(2) (0) | 2025.08.23 |
| 스프링 핵심원리( 심화 ) - 프록시 패턴과 데코레이터 패턴(1) (2) | 2025.08.10 |
| 스프링 핵심원리( 심화 ) - 로그 추적기와 디자인패턴( 템플릿 콜백 패턴 ) (0) | 2025.05.30 |
댓글