기존에 템플릿 콜백 패턴을 적용하여 로그 추적기 부분과 주문 서비스 로직을 구분하였다. 하지만 Controller, Service, Repository에 템플릿의 콜백을 넘겨주는 코드가 여전히 존재하여 기존의 코드들을 수정해야 한다는 점이 아직 과제로 남아있었다.
하여 새로운 요구사항이 추가되었다.
요구사항 추가
• 원본 코드를 전혀 수정하지 않고, 로그 추적기를 적용해라.
• 특정 메서드는 로그를 출력하지 않는 기능
- 보안상 일부는 로그를 출력하면 안된다.
• 다음과 같은 다양한 케이스에 적용할 수 있어야 한다.
- v1 인터페이스가 있는 구현 클래스에 적용
- v2 인터페이스가 없는 구체 클래스에 적용
- v3 컴포넌트 스캔 대상에 기능 적용
원본 코드를 수정하지 않고, 로그 추적기를 적용하기 위해 프록시에 대해 알아보자.
프록시
클라이언트와 서버의 관계는 아래와 같다.
Client는 Server에게 요청을 한다! Server는 Client의 요청을 처리해준다.
이처럼 Client가 Server에게 직접 요청하는 것을 직접 호출이라한다.
하지만 아래와 같은 간접 호출도 존재한다.
이와 같은 방법은 Client가 어떤 대리자를 통해 간접적으로 Server에 요청할 수 있다.
이와 같은 대리자를 Proxy라고 한다.
이와 같은 프록시를 통한 간접 호출의 장점은 직접 호출과 다르게 간접 호출을 하면 대리자가 중간에서 여러가지 일을 할 수 있다는 점이다.
프록시를 이해할 수 있는 재미있는 예시 :
1. 엄마에게 라면을 사달라고 부탁 했는데, 엄마는 그 라면은 이미 집에 있다고 할 수도 있다. 그러면 기대한 것 보다 더 빨리 라면을 먹을 수 있다. (접근 제어, 캐싱)
2. 아버지께 자동차 주유를 부탁했는데, 아버지가 주유 뿐만 아니라 세차까지 하고 왔다. 클라이언트가 기대한 것 외에 세차라는 부가 기능까지 얻게 되었다. (부가 기능 추가)
3. 내가 동생에게 라면을 사달라고 했는데, 동생은 또 다른 누군가에게 라면을 사달라고 다시 요청할 수도 있다. 중요한 점은 클라이언트는 대리자를 통해서 요청했기 때문에 그 이후 과정은 모른다는 점이다. 동생을 통해서 라면이 나에게 도착하기만 하면 된다. (프록시 체인, 대리자가 또 다른 대리자를 부름 )
프록시가 되기 위해서는 Client가 "얘가 서버인가? 아니면 프록시인가?" 를 몰라야 한다!
즉 프록시와 서버가 같은 인터페이스를 사용해야 한다. 아래의 다이어그램과 같다.
클래스 다이어그램을 보면 프록시와 서버가 같은 인터페이스를 사용하고 있다. 그리고 클라이언트는 ServerInterface에만 의존하고 있으므로, DI를 사용하여 Clinet에 Proxy든, Server든 의존관계를 주입할 수 있다.
프록시의 주요 기능
프록시를 통해서 할 수 있는 일은 크게 2가지로 구분할 수 있다.
1. 접근 제어
- 권한에 따른 접근 차단
- 캐싱
- 지연 로딩
2. 부가 기능 추가
- 원래 서버가 제공하는 기능에 더해서 부가 기능을 수행한다.
예) 요청 값이나, 응답 값을 중간에 변형한다.
예) 실행 시간을 측정해서 추가 로그를 남긴다.
프록시 객체가 중간에 있으면 크게 접근 제어와 부가 기능 추가를 수행할 수 있다.
GOF 디자인 패턴에서 데코레이터 패턴과 프록시 패턴 모두 프록시를 사용하는 디자인 패턴이다. 그리고 그들의 생김새도 매우 비슷하다.
하여 이 둘을 나눌 때, intent(의도)로 나눈다.
즉 접근 제어가 목적이라면 프록시 패턴이고, 새로운 기능 추가라면 데코레이터 패턴이다.
프록시 패턴 예제 코드 살펴보기
먼저 간단한 테스트를 위해 아래와 같은 클래스들을 만들 것이다.
일단 처음에는 프록시를 적용하지 않은 코드를 보자.
public interface Subject {
String operation();
}
위의 Subject를 구현한 RealSubject
@Slf4j
public class RealSubject implements Subject{
@Override
public String operation() {
log.info("실제 객체 호출");
sleep(1000); //특정 로직을 수행하고
return "Data"; //결과 데이터를 반환하는데 약 1초가 걸린다고 가정
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Subject타입의 객체에게 요청을 하는 Client
public class ProxyPatternClient {
private Subject subject;
public ProxyPatternClient(Subject subject) { //Subject타입 객체를 DI받으면
this.subject = subject;
}
public void execute(){//Subject타입 객체의 operation을 실행시킨다.(요청)
subject.operation();
}
}
테스트는 아래와 같다. client는 excute메서드를 통해 subject 타입 객체에게 3번의 요청을 한다. (operation 3번 수행)
@Test
void noProxyTest(){
RealSubject realSubject = new RealSubject();
ProxyPatternClient proxyPatternClient = new ProxyPatternClient(realSubject);
proxyPatternClient.execute();
proxyPatternClient.execute();
proxyPatternClient.execute();
}
실행결과:
21:07:30.094 [Test worker] INFO hello.proxy.pureproxy.proxy.code.RealSubject - 실제 객체 호출
21:07:31.102 [Test worker] INFO hello.proxy.pureproxy.proxy.code.RealSubject - 실제 객체 호출
21:07:32.104 [Test worker] INFO hello.proxy.pureproxy.proxy.code.RealSubject - 실제 객체 호출
//세번의 요청을 수행하는데 대략 3초 정도가 걸렸다.
그런데 생객해보면 1초나 걸리는 로직을 방금 전에 수행했는데, 이를 또 실행시키는 것은 부하를 일으킬 수 있다. 하여 처음에 수행한 결과를 캐시에 저장해두고, 캐싱된 데이터를 돌려주는 것이 성능상으로 훨씬 좋을 것이다. (이렇게 캐시를 사용하는 것이 접근 제어에 해당한다.)
이제 프록시 객체를 만들어서 이와 같은 일을 할 수 있도록 해보자.
Cache를 위한 프록시 객체는 아래와 같다.
@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;
}
}
프록시 객체 역시 Subject를 implements해야 하고, 필드에 존재하는 target(실제 객체)을 생성자에서 주입받고, 캐시에 target을 실행시킨 결과가 담겨 있지 않다면 target을 실행시켜 응답결과를 cacheValue에 저장하고, 그것을 클라이언트에게 돌려준다.
이를 테스트해보자
@Test
void cacheProxyTest(){
RealSubject realSubject = new RealSubject();
CacheProxy cacheProxy = new CacheProxy(realSubject);
ProxyPatternClient proxyPatternClient = new ProxyPatternClient(cacheProxy);
proxyPatternClient.execute();
proxyPatternClient.execute();
proxyPatternClient.execute();
}
실행결과:
21:21:51.196 [Test worker] INFO hello.proxy.pureproxy.proxy.code.CacheProxy - 프록시 호출
21:21:51.198 [Test worker] INFO hello.proxy.pureproxy.proxy.code.RealSubject - 실제 객체 호출
21:21:52.202 [Test worker] INFO hello.proxy.pureproxy.proxy.code.CacheProxy - 프록시 호출
21:21:52.203 [Test worker] INFO hello.proxy.pureproxy.proxy.code.CacheProxy - 프록시 호출
실행결과를 보면 맨 처음 이후인 2번째 excute부터는 실제 객체의 operation이 수행되지 않았다.
3번의 excute를 실행하는데 걸린 시간이 캐시를 사용하여 매우 단축되었다.
이렇게 클라이언트의 의존관계를 RealSubject객체를 Proxy객체로 바꿔도 클라이언트는 수정할 것이 없다.
데이코레이터 패턴 예제 코드 살펴보기
이번에도 역시 데코레이터 패턴을 도입하기 전의 코드를 먼저 보자.
Component
public interface Component {
String operation();
}
Component를 구현한 RealComponent
@Slf4j
public class RealComponent implements Component{
@Override
public String operation() {
log.info("RealComponent 실행");
return "Data";
}
}
Component타입을 주입받는 Client
@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);
}
}
이를 테스틀해보면 아래와 같다.
@Test
void noDecorator(){
RealComponent realComponent = new RealComponent();
DecoratorPatternClient decoratorPatternClient = new DecoratorPatternClient(realComponent);
decoratorPatternClient.execute();
}
실행결과:
11:22:43.139 [Test worker] INFO hello.proxy.pureproxy.decorator.code.RealComponent - RealComponent 실행
11:22:43.141 [Test worker] INFO hello.proxy.pureproxy.decorator.code.DecoratorPatternClient - result=Data
이번에는 프록시를 사용하여 부가 기능을 더해보자.
아래와 같이 반환 데이터를 꾸며주는 데코레이터를 만들었다.
@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 decoratorTest1(){
RealComponent realSubject = new RealComponent();
MessageDecorator messageDecorator = new MessageDecorator(realSubject);
DecoratorPatternClient decoratorPatternClient = new DecoratorPatternClient(messageDecorator);
decoratorPatternClient.execute();
}
실행결과:
11:33:51.877 [Test worker] INFO hello.proxy.pureproxy.decorator.code.MessageDecorator - MessageDecorator 실행
11:33:51.880 [Test worker] INFO hello.proxy.pureproxy.decorator.code.RealComponent - RealComponent 실행
11:33:51.891 [Test worker] INFO hello.proxy.pureproxy.decorator.code.MessageDecorator - MessageDecorator 꾸미기 적용 전 = Data, 적용 후 = *** Data ***
11:33:51.894 [Test worker] INFO hello.proxy.pureproxy.decorator.code.DecoratorPatternClient - result=*** Data ***
이번에는 기존의 데코레이터에다 또 다른 데코레이터를 하나 더 추가해볼 것이다.
로직 수행시간을 측정하는 TimeDeocorator
@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();
log.info("TimeDecorator 종료. resultTime = {}ms", (endTime-startTime));
return result;
}
}
이를 테스트해보자.
@Test
void decoratorTest2(){
RealComponent realSubject = new RealComponent();
MessageDecorator messageDecorator = new MessageDecorator(realSubject);
TimeDecorator timeDecorator = new TimeDecorator(messageDecorator);
DecoratorPatternClient decoratorPatternClient = new DecoratorPatternClient(timeDecorator);
decoratorPatternClient.execute();
}
실행결과:
11:44:44.698 [Test worker] INFO hello.proxy.pureproxy.decorator.code.TimeDecorator - TimeDecorator 실행
11:44:44.700 [Test worker] INFO hello.proxy.pureproxy.decorator.code.MessageDecorator - MessageDecorator 실행
11:44:44.700 [Test worker] INFO hello.proxy.pureproxy.decorator.code.RealComponent - RealComponent 실행
11:44:44.704 [Test worker] INFO hello.proxy.pureproxy.decorator.code.MessageDecorator - MessageDecorator 꾸미기 적용 전 = Data, 적용 후 = *** Data ***
11:44:44.706 [Test worker] INFO hello.proxy.pureproxy.decorator.code.TimeDecorator - TimeDecorator 종료. resultTime = 6ms
11:44:44.706 [Test worker] INFO hello.proxy.pureproxy.decorator.code.DecoratorPatternClient - result=*** Data ***
프록시 패턴과 데코레이터 패턴 정리
여기서 생각해보면 Decorator 기능에 일부 중복이 있다. 꾸며주는 역할을 하는 Decorator 들은 스스로 존재할 수 없다. 항상 꾸며줄 대상이 있어야 한다. 따라서 내부에 호출 대상인 component 를 가지고 있어야 한다. 그리고 component 를 항상 호출해야 한다. 이 부분이 중복이다. 이런 중복을 제거하기 위해 component 를 속성으로 가지고 있는 Decorator 라는 추상 클래스를 만드는 방법도 고민할 수 있다. 이렇게 하면 추가로 클래스 다이어그램에서 어떤 것이 실제 컴포넌트인지, 데코레이터인지 명확하게 구분할 수 있다. 여기까지 고민한 것이 바로 GOF에서 설명하는 데코레이터 패턴의 기본 예제이다.
프록시 패턴 vs 데코레이터 패턴 여기까지 진행하면 몇가지 의문이 들 것이다.
Decorator 라는 추상 클래스를 만들어야 데코레이터 패턴일까? 프록시 패턴과 데코레이터 패턴은 그 모양이 거의 비슷한 것 같은데?
의도(intent)
사실 프록시 패턴과 데코레이터 패턴은 그 모양이 거의 같고, 상황에 따라 정말 똑같을 때도 있다. 그러면 둘을 어떻게 구분하는 것일까?
디자인 패턴에서 중요한 것은 해당 패턴의 겉모양이 아니라 그 패턴을 만든 의도가 더 중요하다. 따라서 의도에 따라 패턴을 구분한다.
프록시 패턴의 의도: 다른 개체에 대한 접근을 제어하기 위해 대리자를 제공
데코레이터 패턴의 의도: 객체에 추가 책임(기능)을 동적으로 추가하고, 기능 확장을 위한 유연한 대안 제공
정리
프록시를 사용하고 해당 프록시가 접근 제어가 목적이라면 프록시 패턴이고, 새로운 기능을 추가하는 것이 목적이라면 데코레이터 패턴이 된다.
김영한님의 인프런 강의와 PDF를 바탕으로 정리하였습니다.
'Spring > spring AOP' 카테고리의 다른 글
스프링 코어2 - 구체 클래스 기반 프록시 (0) | 2022.01.08 |
---|---|
스프링 코어2 - 인터페이스 기반 프록시 (0) | 2022.01.08 |
스프링 코어2 - 프록시 학습을 위한 프로젝트 만들기 (0) | 2022.01.06 |
스프링 코어2 - 템플릿 콜백 패턴 (0) | 2022.01.05 |
스프링 코어2 -전략 패턴 (0) | 2022.01.05 |