⬜섹션4 프록시 패턴과 데코레이터 패턴
◼️예제 프로젝트 만들기
🟢예제 3가지 상황
- v1 - 인터페이스와 구현 클래스 - 스프링 빈으로 수동 등록
- v2 - 인터페이스 없는 구체 클래스 - 스프링 빈으로 수동 등록
- v3 - 컴포넌트 스캔으로 스프링 빈 자동 등록
🟢스프링 부트 3.0 전후 차이
스프링 부트 3.0 미만
@RequestMapping //스프링은 @Controller 또는 @RequestMapping 이 있어야 스프링 컨트롤러로 인식
@ResponseBody
public interface OrderControllerV1 {}
스프링 부트 3.0 이상
@RestController //스프링은 @Controller, @RestController가 있어야 스프링 컨트롤러로 인식
public interface OrderControllerV1 {}
◼️요구사항 추가
🟢목표: 원본 코드를 전혀 수정하지 않고, 로그 추적기를 도입
◼️프록시, 프록시 패턴, 데코레이터 패턴 - 소개
클라이언트는 서버에 필요한 것을 요청하고, 서버는 클라이언트의 요청을 처리한다. 프록시(Proxy)는 일종의 대리자로, 대리자를 통해 간접적으로 서버에 요청할 수 있다.
🟢프록시 주요 기능
- 프록시 패턴: 접근 제어
- 권한에 따른 접근 차단
- 캐싱,지연 로딩
- 데코레이터 패턴: 부가 기능 추가
- 원래 서버가 제공하는 기능에 더해서 부가 기능을 수행한다.
- 예) 요청 값이나, 응답 값을 중간에 변형한다.
- 예) 실행 시간을 측정해서 추가 로그를 남긴다.
◼️프록시 패턴 - 예제 코드1
프록시 패턴 핵심은 RealSubject 코드와 클라이언트 코드를 전혀 변경하지 않고, 중간에서 프록시를 도입해서 접근 제어를 했다는 점이다. 또한 클라이언트 코드는 변경이 없다.
🔶프록시 패턴(캐시) 적용 코드
@Slf4j
public class RealSubject implements Subject{
@Override
public String operation() {
log.info("실제 객체 호출");
sleep(1000);
return "data";
}
private void sleep(int millis) {
try{
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ProxyPatternClient {
private Subject subject;
public ProxyPatternClient(Subject subject) {
this.subject = subject;
}
public void execute() {
subject.operation();
}
}
@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;
}
}
public class ProxyPatternTest {
@Test
void noProxyTest() {
RealSubject realSubject = new RealSubject();
ProxyPatternClient client = new ProxyPatternClient(realSubject);
client.execute();
client.execute();
client.execute();
}
@Test
void cacheProxyTest() {
RealSubject realSubject = new RealSubject();
Subject cacheProxy = new CacheProxy(realSubject);
ProxyPatternClient client = new ProxyPatternClient(cacheProxy);
client.execute();
client.execute();
client.execute();
}
}
◼️데코레이터 패턴 - 예제 코드1
🔶데코레이터 패턴(부가 기능) 적용 코드
@Slf4j
public class RealComponent implements Component{
@Override
public String operation() {
log.info("RealComponent 실행");
return "data";
}
}
@Slf4j
public class DecoratorPatternClient {
private Component component; // 클라이언트는 단순히 Component 인터페이스를 의존
public DecoratorPatternClient(Component component) {
this.component = component;
}
public void execute() {
String result = component.operation();
log.info("result={}",result);
}
}
@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;
}
}
@Test
void decorator2() {
Component realComponent = new RealComponent();
Component messageDecorator = new MessageDecorator(realComponent);
Component timeDecorator = new TimeDecorator(messageDecorator);
DecoratorPatternClient client = new DecoratorPatternClient(timeDecorator);
client.execute();
}
◼️프록시 패턴과 데코레이터 패턴 정리
Decorator 코드 중복 제거를 위해 Component를 속성으로 가지고 있는 Decorator 추상 클래스를 만들 수도 있다.
🟢디자인 패턴은 결국 "목적 & 의도"가 중요하다.
프록시 패턴의 의도: 다른 개체에 대한 접근을 제어하기 위해 대리자를 제공
데코레이터 패턴의 의도: 객체에 추가 책임(기능)을 동적으로 추가하고, 기능 확장을 위한 유연한 대안 제공
◼️인터페이스 기반 프록시 - 적용
V1(인터페이스 + 구현체)에 프록시를 도입하면 기존 코드를 수정하지 않고 로그 추적 기능을 도입할 수 있다.
기존엔 위와 같지만, 중간에 프록시가 껴서 실제 호출 원본 리포지토리 참조를 가지고 중간다리 역할을 한다.
프록시를 실제 스프링 빈 대신 등록하고, 실제 객체는 스프링 빈으로 등록하지 않는다.따라서 실제 객체 대신에 프록시 객체가 주입된다. 프록시는 내부에 실제 객체를 참조하고 있다.
즉 프록시 객체는 스프링 컨테이너가 관리하고 자바 힙 메모리에도 올라가지만, 실 객체는 힙 메모리에 올라가도 스프링 컨테이너가 관리하지 않는다.
🔶V1 프록시 적용 일부 예시
@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;
}
}
}
@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({AppV1Config.class, AppV2Config.class}) // 클래스(@Configuration 설정파일)를 스프링 빈으로 등록
@Import(InterfaceProxyConfig.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app.v3") //주의
public class ProxyApplication {
public static void main(String[] args) {
SpringApplication.run(ProxyApplication.class, args);
}
@Bean
public LogTrace logTrace(){
return new ThreadLocalLogTrace();
}
}
◼️구체 클래스 기반 프록시 예제
V2(인터페이스X 실구현체만 있을때)에 프록시를 적용해보자. 자바의 다형성을 이용해 상속을 통해 프록시를 적용해 볼 수 있다.
인터페이스 대신 구체 클래스를 기반으로 프록시를 만드는것을 제외하고는 기존과 같다.
🔶구체 클래스 프록시 적용 코드
@Slf4j
public class ConcreteLogic {
public String operation() {
log.info("ConcreteLogic 실행");
return "data";
}
}
public class ConcreteClient {
private ConcreteLogic concreteLogic; //ConcreteLogic, TimeProxy 모두 주입 가능
public ConcreteClient(ConcreteLogic concreteLogic) {
this.concreteLogic = concreteLogic;
}
public void execute() {
concreteLogic.operation();
}
}
@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;
}
}
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);
// concreteLogic 이 아니라 timeProxy 를 주입
ConcreteClient client = new ConcreteClient(timeProxy);
client.execute();
}
}
◼️구체 클래스 기반 프록시 - 적용
🔶구체 클래스 기반 프록시 적용 코드 일부 예시
// 인터페이스가 아닌 클래스를 상속받아 프록시를 만든다.
public class OrderServiceConcreteProxy extends OrderServiceV2 {
private final OrderServiceV2 target;
private final LogTrace logTrace;
public OrderServiceConcreteProxy(OrderServiceV2 target, LogTrace logTrace) {
/*
* 자식 클래스를 생성할 때는 항상 super()로 부모 클래스 생성자를 호출해야 한다.
* 이를 생략하면 기본 생성자가 호출된다.
* OrderService 는 기본생성자가 없고 파라미터 1개를 필수로 받기때문에 파라미터를 넣어 호출해준다.
* */
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;
}
}
}
@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}) // 클래스(@Configuration 설정파일)를 스프링 빈으로 등록
//@Import(InterfaceProxyConfig.class)
@Import(ConcreteProxyConfig.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app.v3") //주의
public class ProxyApplication {
public static void main(String[] args) {
SpringApplication.run(ProxyApplication.class, args);
}
@Bean
public LogTrace logTrace(){
return new ThreadLocalLogTrace();
}
}
◼️인터페이스 기반 프록시 vs 클래스 기반 프록시
인터페이스 도입은 구현을 변경할 가능성이 있을때 효과적이다.
구현 변경 가능성이 거의 없으면 구체 클래스를 바로 사용하는 것이 낫다.
인터페이스 기반 프록시
- 장점:
- 인터페이스만 같다면 다양한 클래스에 적용 가능하다.
- 상속 제약이 없어 유연하게 사용할 수 있다.
- 역할과 구현을 명확히 분리할 수 있어, 프로그래밍 관점에서 좋다.
- 단점:
- 인터페이스가 필요하다. (+캐스팅관련문제..추후다룸)
클래스 기반 프록시
- 장점: 인터페이스가 없어도 클래스 자체로 프록시를 생성 가능하다.
- 단점: 상속을 사용하기 때문에, 부모 클래스의 생성자를 호출해야 하고, final 키워드가 붙은 클래스나 메서드는 오버라이딩할 수 없다.
이로써 코드변경없이 로그 추적기라는 부가기능을 추가했다. 근데 문제는...
LogTrace를 사용하기위해 코드중복,로직이 같은 프록시 클래스를 많이 만들어야한다.
적용대상클래스가 100개라면 100개를 만들어야한다...
이같은 문제는 동적 프록시 기술을 통해 해결 가능하다!
'Spring' 카테고리의 다른 글
[Spring] 스프링 핵심 원리 - 고급편 섹션6 스프링이 지원하는 프록시 (0) | 2024.08.04 |
---|---|
[Spring] 스프링 핵심 원리 - 고급편 섹션5 동적 프록시 기술 (0) | 2024.08.04 |
[Spring] 스프링 핵심 원리 - 고급편 섹션3 템플릿 메서드 패턴과 콜백 패턴 (2) | 2024.07.24 |
[Spring] 스프링 핵심 원리 - 고급편 섹션2 쓰레드 로컬 - ThreadLocal (0) | 2024.07.19 |
[Spring] 스프링 핵심 원리 - 고급편 섹션1 예제 만들기 (0) | 2024.07.17 |