일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 자바의 정석 기초편 ch1
- 자바의 정석 기초편 ch9
- 자바의 정석 기초편 ch5
- 스프링 mvc2 - 타임리프
- 스프링 mvc1 - 서블릿
- 스프링 mvc2 - 로그인 처리
- 타임리프 - 기본기능
- 자바의 정석 기초편 ch14
- 스프링 mvc2 - 검증
- 자바의 정석 기초편 ch13
- jpa - 객체지향 쿼리 언어
- jpa 활용2 - api 개발 고급
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch11
- 게시글 목록 api
- 코드로 시작하는 자바 첫걸음
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch8
- @Aspect
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch7
- 자바의 정석 기초편 ch4
- 자바의 정석 기초편 ch6
- 자바의 정석 기초편 ch3
- 2024 정보처리기사 수제비 실기
- 자바의 정석 기초편 ch12
- 자바의 정석 기초편 ch2
- 스프링 입문(무료)
- 스프링 db1 - 스프링과 문제 해결
- 2024 정보처리기사 시나공 필기
- Today
- Total
나구리의 개발공부기록
프록시 패턴과 데코레이터 패턴, 프로젝트 생성 및 예제 프로젝트 만들기, 요구사항 추가, 프록시/프록시 패턴/데코레이터 패턴 소개, 프록시 패턴 - 예제, 데코레이터 패턴 - 예제, 프록시 패턴과 데코레이터 패턴 정리 본문
프록시 패턴과 데코레이터 패턴, 프로젝트 생성 및 예제 프로젝트 만들기, 요구사항 추가, 프록시/프록시 패턴/데코레이터 패턴 소개, 프록시 패턴 - 예제, 데코레이터 패턴 - 예제, 프록시 패턴과 데코레이터 패턴 정리
소소한나구리 2024. 11. 7. 16:58출처 : 인프런 - 스프링 핵심 원리 - 고급편 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 프로젝트 생성 및 예제 프로젝트 만들기
1) 프로젝트 생성
(1) 프로젝트 생성 및 설정 변경
- 강의에서 제공된 프로젝트를 복사해서 사용
- 자바 : 17
- 스프링 부트 : 3.3.5
- dependency-management : 1.1.6
- gradle : 8.10.2
2) 예제 프로젝트 v1
- 여기에서의 v1, v2, v3는 버전업이 되는 것은 아니고 실무에서 마주하게 되는 3가지 상황에 대한 예제라고 보면 되며 v1, v2, v3의 패키지를 각각 만들어서 코드들을 작성
- v1 - 인터페이스와 구현 클래스가 있고 스프링 빈으로 수동으로 직접 등록하는 상황
(1) OrderRepositoryV1, OrderRepositoryV1Impl
- 로그 추적기를 만들었던 Repository의 구조와 동일한 구조로 각각의 인터페이스와 인터페이스를 구현한 구현체클래스를 생성
package hello.proxy.app.v1;
public interface OrderRepositoryV1 {
void save(String itemId);
}
package hello.proxy.app.v1;
@Slf4j
public class OrderRepositoryV1Impl implements OrderRepositoryV1{
@Override
public void save(String itemId) {
// 저장 로직
if (itemId.equals("ex")) {
throw new IllegalStateException("예외 발생!");
}
sleep(1000);
}
// Thread.sleep을 try - catch하는 sleep()메서드 구현도 동일
}
(2) OrderServiceV1, OrderServiceV1Impl
- 서비스코드들도 동일함
package hello.proxy.app.v1;
public interface OrderServiceV1 {
void orderItem(String itemId);
}
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);
}
}
(3) OrderControllerV1 - 인터페이스
- 스프링 컨트롤러로 인식시키기 위해 @RestController 애노테이션을 입력, 해당 애노테이션은 인터페이스에 사용해도 됨
- @RestController는 내부에 @ResponseBody를 가지고 있어 HTTP 메시지 컨버터를 사용해서 응답함
- 인터페이스에서는 @RequestParam("itemId")을 생략하면 ItemId 단어를 컴파일 이후 자바 버전에 따라 인식하지 못할 수 있기 때문에 생략하지 말고 꼭 넣어주어야 하며 클래스에서 적용할 때는 생략해도 대부분 잘 지원함
package hello.proxy.app.v1;
@RestController
public interface OrderControllerV1 {
@GetMapping("/v1/request")
String request(@RequestParam("itemId") String itemId);
@GetMapping("/v1/no-log")
String noLog();
}
**참고
- 이번 예제처럼 실무에서 컨트롤러를 인터페이스로 만드는 경우는 거의 없음
- 강의에서는 스프링 부트 2.x 버전을 사용했기에 스프링 MVC가 @Controller나 @RequestMapping 애노테이션이 있어야 스프링 컨트롤러로 인식하지만 지금 실습하는 환경은 스프링 부트 3.x 버전이므로 @Controller나 @RestController 애노테이션이 있어야 스프링 컨트롤러로 인식함
- 여기서는 REST컨트롤러로 사용할 것이기 때문에 @RestController를 적용함
(4) OrderControllerV1Impl
- OrderControllerV1 인터페이스를 구현한 클래스이며 로그를 찍는 @Slf4j가 여기에 붙어있음
- 보통 컨트롤러에 컨트롤러 관련 애노테이션이 있었지만 인터페이스에 모두 적용되어 있고 클래스는 매핑 메서드가 실제 동작하도록 구현만하고 있음
package hello.proxy.app.v1;
@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";
}
}
(5) AppV1Config
- @Configuration과 @Bean 애노테이션으로 설정 클래스를 싱글톤 스프링 빈으로 수동 등록
- Controller, Service, Repository를 의존관계 주입
package hello.proxy.config;
@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();
}
}
(6) ProxyApplication - 수정
- @Import로 설정 클래스를 스프링 빈으로 등록, 일반적으로 @Configuration같은 설정 파일을 등록할 때 사용하지만 스프링 빈을 등록할 때도 사용할 수 있음
- @scanBasePackages 옵션으로 컴포넌트 스캔을 시작할 위치를 ~.app.v3 패키지와 그 하위패키지만 컴포넌트 스캔의 대상이 되도록 지정
- app.v1과 v2는 직접 빈으로 등록하는 예제이고 v3가 컴포넌트 스캔을 사용도록 하는 예제이기 때문에 이렇게 구성하였으며 이런 사용법을 알고있으면 실무에서 도움이 될 때가 있음
** 주의
- v1, v2패키지의 컨트롤러에서 사용하는 @RestController는 내부에 @Component를 가지고있기 때문에 컴포넌트 스캔의 대상이됨과 동시에 빈도 수동으로 직접 등록하게되면 스프링 컨테이너에서 등록시 충돌 오류가 발생함
@Import(AppV1Config.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app.v3")
public class ProxyApplication {
// ... 기존 코드 동일 생략
}
(7) 실행해보기
- 코드를 적용하고 컨트롤러에 매핑된 각 url로 접속해보면 정상적으로 예제코드가 동작하는 것을 확인할 수 있음
- v1/request에는 요청파라미터로 item에 값을 입력해주어야 정상 동작함
3) 예제 프로젝트 v2
- v2 - 인터페이스가 없는 구체 클래스만 있고 스프링 빈으로 수동으로 직접 등록하는 상황
- 즉 인터페이스가 없는 Controller, Service, Repository를 스프링 빈으로 수동 등록
(1) OrderRepositoryV2
- 패키지의 위치와 클래스 명만 다르고 로직은 OrderRepositoryV1Impl과 완전히 똑같음
package hello.proxy.app.v2;
public class OrderRepositoryV2 {
// OrderRepositoryV1Impl과 완전히 동일한 로직
}
(2) OrderServiceV2
- 마찬가지로 패키지와 클래스명, 그리고 생성자로 주입받은 객체가 OrderRepositoryV2라는 것만 다르고 나머지는 동일함
package hello.proxy.app.v2;
public class OrderServiceV2 {
// OrderRepositoV2를 선언하고 생성자로 주입받는 것만 다르고 나머지는 동일
}
(3) OrderControllerV2
- OrderControllerV1의 애노테이션과 OrderControllerV1Impl의 컨트롤러 구현코드를 합쳐놓음
- 생성자로 의존관계 주입을 받는 코드를 OrderServiceV2로 변경하고 @GetMapping의 정보를 /v2/request와 /v2/no-log로 변경
package hello.proxy.app.v2;
@Slf4j
@RestController
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";
}
}
(4) AppV2Config 추가 및 ProxyApplication 수정
- AppV2Config는 AppV1Config의 설정 그대로 V1의 인터페이스와 V1Impl 클래스 코드들을 전부 V2의 코드들로 수정하여 스프링 빈으로 등록
- ProxyApplication에서는 @Import에 {} 배열을 사용하여 AppV1Config와 AppV2Config를 모두 등록하도록 설정
- 각 설정파일의 스프링 빈으로 등록되는 이름이 각각 다르기때문에 충돌 오류가 발생하지 않음
- 애플리케이션을 실행해보면 정상적으로 V1과 마찬가지로 정상적으로 동작함
@Import({AppV1Config.class, AppV2Config.class})
@SpringBootApplication(scanBasePackages = "hello.proxy.app.v3") //주의
public class ProxyApplication {
// ... 코드 생략
}
4) 예제 프로젝트 v3
- v3 - 컴포넌트 스캔으로 스프링 빈을 자동 등록하는 상황
- 예제를 간소화하기위해 전부 구체클래스로 적용
(1) OrderRepositoryV3
- 기존 코드와 구현 동작이 모두 동일한 OrderRepositoryV3에 @Repository 애노테이션을 적용
@Repository
public class OrderRepositoryV3 {
// 코드 동일
}
(2) OrderServiceV3
- @Service 적용, OrderRepositoryV3 주입
@Service
public class OrderServiceV3 {
private final OrderRepositoryV3 orderRepository;
// 생성자 및 기능 동일
}
(3) OrderControllerV3
- V2와 완전히 동일하며 OrderServiceV3를 주입받는 코드만 변경하면 됨
(4) 실행 테스트
- 컴포넌트 스캔으로 @RestController, @Service, @Repository를 자동으로 조회하기 때문에 수동으로 빈을 등록하지 않아도 정상적으로 동작이 되어 v1, v2, v3 모두 동작하는 예제 코드가 완성되었음
2. 요구사항 추가
1) 로그 추적기에 요구사항을 추가
(1) 로그추적기의 문제점과
- 로그 추적기에 쓰레드 로컬과 템플릿 콜백 패턴까지 적용하면서 기존의 요구사항을 모두 만족하고 코드의 최적화까지 하는 로그 추적기를 만들었으나 결과적으로 로그 추적기를 실제 애플리케이션에 적용해야 할 때 기존 코드를 많이 수정해야함
- 로그를 남기고 싶은 클래스가 수백개라면 수백개의 클래스의 원본 코드를 변경해야한다는 것 자체가 개발자에게는 큰 부담임
(2) 요구사항 추가
- 원본 코드를 전혀 수정하지 않고 로그 추적기를 적용
- 특정 메서드는 로그를 출력하지 않는 기능이 있어야 함(보안상으로 일부 로그를 출력하지 않아야 한다고 가정)
- 인터페이스가 있는 구현클래스에도(v1), 인터페이스가 없는 구체 클래스에도(v2), 컴포넌트 스캔 대상에도(v3) 모두 적용할 수 있어야 함
(3) 문제 해결 과제
- 가장 어려운 문제는 원본 코드를 전혀 수정하지 않고, 로그 추적기를 도입하는 것인데 이 문제를 해결하려면 프록시(Proxy)의 개념을 먼저 이해해야함
3. 프록시, 프록시 패턴, 데코레이터 패턴 - 소개
1) 프록시
(1) 클라이언트와 서버
- 프록시의 개념을 알기 위해서는 클라이언트와 서버의 개념을 알아야함
- 클라이언트와 서버 라고하면 보통 클라이언트는 개인컴퓨터 서버는 서버 컴퓨터를 생각할텐데 클라이언트와 서버의 개념은 상당히 넓게 사용됨
- 클라이언트는 의뢰인이라는 뜻이고 서버는 서비스나 상품을 제공하는 사람이나 물것을 뜻함
- 즉, 클라이언트는 서버에 필요한 것이고 서버는 클라이언트의 요청을 처리하는 것임
- 이러한 개념을 컴퓨터 네트워크에 도입하면 클라이언트는 웹 브라우저가 되고 요청을 처리하는 서버는 웹 서버가 되며 객체에 도입하면 요청하는 객체는 클라이언트가 되고 요청을 처리하는 객체는 서버가 됨
(2) 직접 호출과 간접 호출
- 클라이언트와 서버 개념에서 일반적으로는 클라이언트가 서버를 직접 호출하고 처리 결과를 직접 받는데 이것을 직접 호출이라함
- 그러나 클라이언트가 요청한 결과를 서버에 직접 요청하는 것이 아니라 어떤 대리자를 통해 간접적으로 서버에 요청할 수 있는데 이 대리자를 영어로 프록시(Proxy)라고함
(3) 대체 가능, 서버와 프록시가 같은 인터페이스를 사용
- 프록시는 아무 객체가 될 수 있는 것은 아니며 객체에서 프록시가 되려면 클라이언트는 서버에게 요청한 것인지 프록시에게 요청을 한 것인지 조차 몰라야함
- 즉, 서버와 프록시는 같은 인터페이스를 사용해야하며 클라이언트가 사용하는 서버 객체를 프록시 객체로 변경해도 클라이언트 코드를 변경하지 않고 동작할 수 있어야 함
- 클라이언트는 서버 인터페이스에만 의존하고 서버와 프록시가 같은 인터페이스를 사용하기에 DI를 사용해서 대체가 가능함
- 런타임(애플리케이션 실행)시점에 클라이언트 객체에 DI를 사용해서 클라이언트가 서버에서 프록시로 객체 의존관계를 변경해도 클라이언트 코드를 전혀 변경하지 않아도 되며 클라이언트 입장에서는 변경된 사실조차 모름
- DI를 사용하면 클라이언트 코드의 변경 없이 유연하게 프록시를 주입할 수 있음
(4) 프록시의 주요기능
- 프록시 객체가 중간에 있으면 크게 접근 제어와 부가 기능 추가를 수행할 수 있음
- 접근제어: 권한에 따른 접근 차단, 캐싱, 지연 로딩
- 부가 기능 추가 : 원래 서버가 제공하는 기능에 대해서 부가 기능을 수행
- 부가 기능 추가 예시 - 요청 값이나 응답 값을 중간에 변형, 실행 시간을 측정해서 추가 로그 시간을 남김 등
- 프록시가 또 다른 프록시를 호출하는 프록시 체인의 구조로 적용할 수 있으며 클라이언트는 대리자에게 요청이 가면 그 이후의 요청은 모르고 결과만 나에게 반환되면 됨
(5) GOF 디자인 패턴
- 둘다 프록시를 사용하는 방법이지만 GOF 디자인 패턴에서는 이 둘의 의도(intent)에 따라서 프록시 패턴과 데코레이터 패턴으로 구분
- 프록시 패턴 : 접근 제어가 목적
- 데코레이터 패턴 : 새로운 기능 추가가 목적
- 둘다 프록시를 사용하지만 의도가 다르다는 점이 핵심이며 용어가 프록시 패턴이라고해서 이 패턴만 프록시를 사용하는 것이 아니라 데코레이터 패턴도 프록시를 사용함
** 참고
- 프록시 라는 개념은 클라이언트 서버라는 큰 개념안에서 자연스럽게 발생할 수 있음
- 프록시는 객체 안에서의 개념도 있고 웹 서버에서의 프록시도 있기에 객체 안에서 객체로 구현되어있는지, 웹 서버로 구현되어 있는지의 규모의 차이가 있을 뿐, 근본적인 역할은 같음
4. 프록시 패턴 - 예제 코드
1) 예제1 - 프록시 사용 X
(1) 관계도
(2) Subject 인터페이스와 Subject를 구현하는 RealSubject 구현체
- 예제 코드는 모두 test의 하위 패키지에 pureproxy.proxy.code의 패키지 구조를 만들어서 적용
- Subject인터페이스는 operation()이라는 기능 하나만 가지고있음
- RealSubject는 Subject를 구현하여 실제 객체 호출이라는 로그를 출력 후 1초 대기하고 반환하도록 operation()메서드를 구현
- 즉 데이터를 DB나 외부에서 조회하는데 1초가 걸린다고 가정하여 호출할 때마다 시스템에 큰 부하를 주는 데이터 조회라고 가정함
package hello.proxy.pureproxy.proxy.code;
public interface Subject {
String operation();
}
package hello.proxy.pureproxy.proxy.code;
@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();
}
}
}
(3) ProxyPatternClient
- Subject인터페이스에 의존하고 Subject를 호출하는 클라이언트 코드
- execute()를 실행하면 subject.operation()를 호출함
package hello.proxy.pureproxy.proxy.code;
public class ProxyPatternClient {
private Subject subject;
public ProxyPatternClient(Subject subject) {
this.subject = subject;
}
public void execute() {
subject.operation();
}
}
(4) ProxyPatternTest 작성 및 실행
- 아직 프록시를 적용하지 않는 테이스케이스로 클라이언트를 생성할 때 생성한 RealSubject객체를 의존관계 주입
- 클라이언트가 execute()메서드 요청을 3번 연속하도록 작성 후 실행해보면 작성한 코드의 동작대로 1초에 한번씩 로그가 출력됨
package hello.proxy.pureproxy.proxy;
public class ProxyPatternTest {
@Test
void noProxyTest() {
RealSubject realSubject = new RealSubject();
ProxyPatternClient client = new ProxyPatternClient(realSubject);
client.execute();
client.execute();
client.execute();
}
}
(5) 개선점
- 그런데 이 데이터가 한번 조회할 때 변하지 않는 데이터라면 어딘가에 보관해두고 이미 조회한 데이터를 사용하는 것이 성능상에는 분명히 좋을텐데, 이런 것을 캐시라고 함
- 프록시 패턴의 주요 기능은 접근 제어인데 캐시도 접근 자체를 제어하는 기능 중 하나이며 이를 통해 성능개선을 할 수 있음
2) 예제1 - 프록시 패턴 적용
(1) 관계도
- 런타임 시점에 클라이언트가 프록시를 의존함
(2) CacheProxy 추가
- Subject 인터페이스를 구현하는 CacheProxy클래스를 생성
- 클라이언트가 프록시를 호출하면 프록시가 최종적으로 실제 객체를 호출해야하기에 내부에 실제 객체의 참조를 가지고 있어야함
- 프록시가 호출하는 대상을 target이라고 하기에 실제객체 타입으로 target 변수를 선언
- operation() 메서드에서 cacheValue에 값이 없으면 실제 객체의 operation()메서드를 호출해서 값을 반환하고 cacheValue에 값이 있으면 cacheValue실제 객체를 전혀 호출하지 않고 캐시 값을 그대로 반환함
- 즉, 처음 조회 이후에는 캐시에서 매우 빠르게 데이터를 조회할 수 있게됨
package hello.proxy.pureproxy.proxy.code;
@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가 없으면 실제 객체의 메서드를 호출
cacheValue = target.operation();
}
return cacheValue; // 있으면 cacheValue값을 출력
}
}
(3) ProxyPatternTest 테스트 추가 및 실행
- cacheProxyTest()를 추가하여 프록시를 사용하는 테스트케이스를 생성
- 클라이언트는 CacheProxy를 CachProxy는 RealSubject를 의존관계 주입을 받으면서 client -> cacheProxy -> realsubject 런타임 객체 의존관계가 완성됨
- client가 execute()를 3번 연속으로 호출하면 실제 객체가아닌 캐시 프록시를 호출하게 되면서 최초에만 실제 객체를 호출하여 1초의 소요시간이 걸리고 그다음 호출 2번은 프록시에서 바로 값을 꺼내오는 결과가 출력됨
@Test
void cacheProxyTest() {
RealSubject realSubject = new RealSubject();
CacheProxy cacheProxy = new CacheProxy(realSubject);
ProxyPatternClient client = new ProxyPatternClient(cacheProxy);
client.execute();
client.execute();
client.execute();
}
(4) 정리
- 결과적으로 캐시 프록시를 도입하기 전에는 3초가 걸렸지만, 캐시 프록시 도입 이후에는 최초에 한번만 1초가 걸렸고 이후에는 거의 즉시 반환하였음
- 프록시 패턴의 핵심은 RealSubject코드와 클라이언트 코드를 전혀 변경하지 않았고 프록시를 도입하여 접근 제어를 했다는 점임
- 클라이언트 코드의 변경 없이 자유롭게 프록시를 넣고 뺄 수 있으며 실제 클라이언트 입장에서는 프록시 객체가 주입이 되었는지 실제 객체가 주입 되었는지 알지 못함
5. 데코레이터 패턴 - 예제 코드
1) 예제1 - 데코레이터 패턴 사용 X
(1) 관계도
(2) Component인터페이스와 RealComponent 구현체
- test하위에 생성하였던 pureproxy하위에 decorator.code 패키지를 생성하여 코드를 작성
- 로직은 프록시 패턴 테스트와 거의 동일하며 sleep()메서드만 없음
package hello.proxy.pureproxy.decorator.code;
public interface Component {
String operation();
}
package hello.proxy.pureproxy.decorator.code;
@Slf4j
public class RealComponent implements Component {
@Override
public String operation() {
log.info("RealComponent 실행");
return "data";
}
}
(3) DecoratorPatternClient
- 클라이언트코드는 단순히 Component 인터페이스를 의존하고 execute()메서드를 실행하면 component.operation()메서드를 실행하고 그 결과를 출력함
package hello.proxy.pureproxy.decorator.code;
@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);
}
}
(4) DecoratorPatternTest
- 데코레이터 패턴이 적용되지 않는 테스트케이스의 클라이언트로 execute()메서드를 호출
- 클라이언트가 실제객체인 RealComponent를 주입받아 execute()메서드를 호출하고 있으며 앞에서 했던 프록시 패턴에서의 프록시를 적용하지 않은 코드와 유사한 구조의 어렵지 않은 코드임
package hello.proxy.pureproxy.decorator;
@Slf4j
public class DecoratorPatternTest {
@Test
void noDecorator() {
Component realComponent = new RealComponent();
DecoratorPatternClient client = new DecoratorPatternClient(realComponent);
client.execute();
}
}
2) 예제2 - 데코레이터 패턴 적용1
(1) 부가 기능 추가와 관계도
- 프록시를 통해서 할 수 있는 기능은 크게 접근 제어와 부가 기능 추가라는 2가지로 구분하는데 프록시를 활용해서 부가 기능을 추가하는 것을 데코레이터 패턴이라고 함
- 응답 값을 꾸며주는 데코레이터 프록시를 생성
(2) MessageDecorator
- 데코레이터 역할을 수행하는 클래스를 생성하여 Component를 구현하고, 실제 객체를 호출한 결과를 꾸며주는 로직을 추가하여 operation()메서드를 구현
- 꾸미기 전에는 data, 꾸민 후에는 ****data****이 출력됨
package hello.proxy.pureproxy.decorator.code;
@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(); // 실제 객체의 operation() 메서드 호출
String decoResult = "****" + result + "****"; // 응답값에 앞,뒤로 ****을 붙혀서 반환
log.info("MessageDecorator 꾸미기 적용 전={}, 적용 후={}", result, decoResult);
return decoResult;
}
}
(3) DecoratorPatternTest 추가 및 실행
- client -> messageDecorator -> realComponent의 객체 의존관계가 완성되고 client.execute()를 호출하면 데코레이터의 메서드 연산결과가 출력됨
@Test
void decorator1() {
Component realComponent = new RealComponent();
MessageDecorator decorator = new MessageDecorator(realComponent);
DecoratorPatternClient client = new DecoratorPatternClient(decorator);
client.execute();
}
3) 예제3 - 데코레이터 패턴 적용2
(1) 실행 시간을 측정하는 데코레이터 추가
- 기존 데코레이터에 기능을 더해서 실행 시간을 측정하는 기능을 추가
- 프록시는 체인이 될 수 있기 때문에 데코레이터 패턴에서 체인을 적용하면 부가 기능을 계속 추가할 수 있음
(2) TimeDecorator 추가
- messageDecorator를 생성했던것과 동일한 방식으로 구현
- operation메서드를 구현 시 실행시간을 출력하는 로직을 추가
package hello.proxy.pureproxy.decorator.code;
@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("TimeDecorator 종료 resultTime={}ms", resultTime);
return result;
}
}
(3) DecoratorPatternTest 추가 및 실행
- 지금까지 만든 부가기능을 client -> timeDecorator -> messageDecorator -> realComponent의 객체 의존관계를 설정 후 메서드를 실행
- 즉, 프록시가 프록시를 호출하는 프록시 체인을 사용해서 부가기능을 추가하였고 클라이언트는 최종결과만 받아서 사용함
- 부가기능이 모두 적용된 연산결과가 출력되는 것을 확인할 수 있음
@Test
void decorator2() {
Component realComponent = new RealComponent();
MessageDecorator decorator = new MessageDecorator(realComponent);
TimeDecorator timeDecorator = new TimeDecorator(decorator);
DecoratorPatternClient client = new DecoratorPatternClient(timeDecorator);
client.execute();
}
6. 프록시 패턴과 데코레이터 패턴 정리
1) GOF 데코레이터 패턴
(1) 모양
(2) 설명
- 위 모양은 GOF에서 설명하는 데코레이터 패턴의 기본 예제이며, 지금까지 구현한 것은 위 모양에서 Decorator를 생략하고 바로 Component로 데코레이터들을 구현한 예제이며 둘다 데코레이터 패턴임
- 구현한 데코레이터들의 코드를 잘 보면 기능에 일부 중복이 있는데 꾸며주는 역할을 하는 데코레이터들은 스스로 존재하지 않고 항상 꾸며줄 대상이 필요하기에 내부에서 호출 대상인 Component를 가지고 있어야하며 항상 Component를 호출해야함
- 이부분이 중복인데 이런 중복을 제거하기 위해 component를 위의 모양처럼 Decorator라는 추상 클래스를 만드는 방법도 고민할 수 있음
- 이렇게 하면 추가로 클래스 다이어그램에서 어떤 것이 실제 컴포넌트인지, 데코레이터인지 명확하게 구분할 수 있으며 여기까지 고민한 것이 바로 GOF에서 설명하는 데코레이터 패턴임
(3) 프록시 패턴 vs 데코레이터 패턴
- 프록시 패턴과 데코레이터 패턴은 모양이 거의 비슷함
- 위 두가지 패턴은 모양이 거의 같거나 상황에 따라 정말 똑같을 때도 있는데 디자인 패턴에서 중요한 것은 해당 패턴의 겉모양이 아니라 그 패턴을 만든 의도(intent)가 더 중요하기에 의도에 따라 패턴을 구분함
- 앞서 설명했듯 프록시 패턴의 의도는 다른 객체에 대한 접근을 제어하기 위해 대리자를 제공하는 것이고 데코레이터 패턴의 의도는 객체에 추가 책임(기능)을 동적으로 추가하고 기능 확장을 위한 유연한 대안을 제공하는 것임
- 즉, 프록시를 사용하고 해당 프록시가 접근 제어가 목적이면 프록시 패턴이며, 새로운 기능을 구하는 것이 목적이면 데코레이터 패턴이 되면 두가지 패턴을 모두 한번에 적용하여 사용할 수도 있음