앞서 다룬 인터페이스의 기반의 프록시 뿐만 아니라, 구체클래스 기반의 프록시를 적용하는 방법에 대해 학습을 이어간다.
상속을 통한 프록시 적용
결론부터 말하자면, 인터페이스가 존재하지 않아도 '상속'을 통해 프록시 적용이 가능하다.
코드 예시를 통해 살펴보자.
상속을 통한 프록시 적용 예시 코드
프록시 적용 이전 예시
먼저, ConcreteLogic이며, 인터페이스가 존재하지 않는 구체클래스이다.(ex, 서비스 로직)
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ConcreteLogic {
public String operation() {
log.info("ConcreteLogic 실행");
return "data";
}
}
그리고 이를 사용하는 클라이언트 코드이다. (ex, 컨트롤러)
public class ConcreteClient {
private ConcreteLogic concreteLogic;
public ConcreteClient(ConcreteLogic concreteLogic) {
this.concreteLogic = concreteLogic;
}
public void execute() {
concreteLogic.operation();
}
}
이를 조합하여, 실행하려면 다음과 같은 코드의 형태이다(스프링 컨테이너의 역할)
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();
}
}
클라이언트가 직접 필요한 클래스에 요청을 보내 필요한 기능을 사용하는 형태이다.
이제 구체 클래스에 프록시를 적용하기 위해 상속을 사용하는 방법에 대해서 정리하고 적용해보자.
먼저, 자바의 다형성은 인터페이스, 클래스 가릴 것 없이 상위 타입이 맞으면 적용 가능하다.
즉, 인터페이스를 상위 타입으로 활용하는 것처럼 클래스의 상속을 통해 상위 타입으로 프록시를 적용할 수 있다는 것이다.
프록시 적용 예시
그럼 위의 예시 코드에 상속을 통해 프록시를 적용해보자.
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 클래스를 상속 받은 클래스로, 시간을 측정하는 프록시 기능을 지니고 있다.
그리고 이제 해당 클래스를 사용하여 프록시를 적용한다.
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();
}
// 추가된 로직
@Test
void addProxy() {
ConcreteLogic concreteLogic = new ConcreteLogic();
TimeProxy timeProxy = new TimeProxy(concreteLogic);
ConcreteClient client = new ConcreteClient(timeProxy);
client.execute();
}
}
추가된 addProxy()를 보면, 클라이언트가 noProxy와 달리 timeProxy 객체를 주입받아 사용하고 있다는 점이다.
ConcreteClient는 ConcreteLogic 객체를 필드 멤버로 사용하고 있으며, timeProxy의 상위타입은 ConcreteLogic 객체이기에 주입이 가능한 것이다.
상속을 통한 App 프록시 적용 코드
실제 구체 클래스기반의 application 코드에 상속을 사용하여 프록시를 적용해보자.
앞서 인터페이스기반의 프록시 적용과 크게 다른 것이 없다.
먼저, 레포지토리 구현 클래스를 상속하여 만든 프록시 객체이다.( OrderRepositoryV2 클래스를 상속받아 만든 프록시 객체이다 )
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 호출
target.save(itemId);
logTrace.end(status);
}
}
}
이어서 서비스 구현 클래스를 상속하여 만든 프록시 객체이다.( OrderServiceV2 클래스를 상속받아 만든 프록시 객체이다 )
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); // 부모 생성자 호출이 필요하며, 오버라이딩하여 기능을 사용하기에 변수는 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;
}
}
}
그리고 컨트롤러 구현 클래스를 상속하여 만든 프록시 객체이다.
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()");
//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();
}
}
이제 스프링 컨테이너의 설정까지 진행해보자.
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);
}
}
그리고 어플리케이션 실행 파일에 설정을 변경해준다.
//@Import({AppV1Config.class, AppV2Config.class})
//@Import(InterfaceProxyConfig.class)
@Import(ConcreteProxyConfig.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();
}
}
이후 실행하여 해당 컨트롤러에서 제대로 출력되는 모습까지 살펴볼 수 있다.

이처럼 자바의 다형성을 통해 구체 클래스에도 상속 기능을 활용하여 프록시를 적용할 수 있음을 살펴보았다.
'Spring > SpringCore - advance' 카테고리의 다른 글
| 스프링 핵심원리( 심화 ) - 동적 프록시(리플렉션) (0) | 2026.01.26 |
|---|---|
| 스프링 핵심원리( 심화 ) - 인터페이스 기반 프록시 (0) | 2025.11.07 |
| 스프링 핵심원리( 심화 ) - 프록시 패턴과 데코레이터 패턴(2) (0) | 2025.08.23 |
| 스프링 핵심원리( 심화 ) - 프록시 패턴과 데코레이터 패턴(1) (2) | 2025.08.10 |
| 스프링 핵심원리( 심화 ) - 로그 추적기와 디자인패턴( 템플릿 콜백 패턴 ) (0) | 2025.05.30 |
댓글