S E P H ' S
[Spring] 4. Spring AOP 와 디자인 패턴 (1) 본문
Spring AOP
지난 포스트에서는 Spring의 핵심 3대 요소에 대해 알아봤다. 이번 포스트에서는 AOP 에 대해서 더 깊이 있게 다뤄볼 것이다. Spring에서 AOP 를 적용하는데에 있어 생각보다 단순하지는 않기 때문이다. 많이 복잡할 것이지만 차근차근 하나씩 개념을 짚어나가보면 좋을 것 같다.
AOP에 대한 기초 개념은 지난 포스트에서 다뤘기 때문에 더 깊이 있는 내용들을 위주로 다뤄보겠다. 크게 AOP에 사용되는 디자인 패턴과 용어 및 적용 방식 등과 같이 두 가지로 챕터를 나눠서 정리할 것이다.
AOP 와 프록시 패턴, 데코레이터 패턴
AOP는 로직을 볼때, 핵심적인 관점과 부가적인 관점을 나누어 보고 그 관점을 기준으로 모듈화를 한다고 했었다. 다음 그림을 보자.
그림과 같이 클래스 A, B, C에서 공통적으로 나타나는 색 블록들은 중복되는 메소드나 필드, 코드 등을 의미한다. 이때 만약 클래스 A의 초록색 부분을 수정해야 한다면 클래스 B, C의 부분도 일일이 찾아 수정해야 하는 불편함이 있다. 이는 SOLID 원칙(단일 책임 원칙)을 위배하며 유지보수를 어렵게 만든다. 이렇게 코드 상에서 계속 반복적으로 사용되는 부분들을 흩어진 관심사(CrossCutting Concerns)라고 한다.
OOP : 비즈니스 로직의 모듈화
- 모듈화의 핵심 단위는 비즈니스 로직
AOP : 인프라 혹은 부가기능의 모듈화
- 대표적인 예시 : 모니터링 및 로깅, 동기화, 오류 검사 및 처리, 성능 최적화(캐싱) 등
- 각각의 모듈들의 주 목적 외에 필요한 부가적인 기능들
AOP 에서 각 관점을 기준으로 로직을 모듈화 한다는 것은 흩어진 관심사를 모듈화 하겠다는 것이다. 그림과 같이 Aspect X, Y, Z와 같이 모듈화로 코드를 구성해둔 다음, 필요한 곳에서 사용하면 되는 것이다. 간단히 한 줄로 요약하자면 "AOP는 공통 기능을 재사용하는 기법"이다. AOP의 장점은 다음과 같다.
- 애플리케이션 전체에 흩어진 공통 기능이 하나의 장소에서 관리되어 유지보수가 좋다.
- 핵심 로직과 부가 기능의 명확한 분리로, 핵심 로직은 자신의 목적 외에 사항들에는 신경쓰지 않는다.
그래서 프록시 패턴과 데코레이터 패턴은 어디서 나오는가?
관점을 기준으로 부가적인 기능을 모듈화 한다는 특히 로그 추적기, 로깅을 위해서 AOP를 도입하고는 한다. 로깅을 디자인 패턴에 대한 이해 없이 구현한다면 핵심 기능을 위한 코드보다 로깅을 위한 코드가 더 많아지게 된다. 클래스가 많아지게 된다면 수정 요소도 많아지게 될 것이고 정확도도 떨어지게 된다.
1차적으로 핵심 코드와 부가 기능 코드를 분리하고 모듈화 하는데에 있어서 템플릿 메소드 패턴, 전략 패턴, 템플릿 콜백 패턴 등을 선택할 수 있다. 먼저 템플릿 메소드부터 짚어보자. GOF에서 템플릿 메소드 패턴의 목적은 "작업에서 알고리즘의 골격을 정의하고 일부 단계를 하위 클래스로 연기한다. 템플릿 메소드를 사용하면 하위 클래스가 알고리즘의 구조를 변경하지 않고도 알고리즘의 특정 단계를 재정의 할 수 있다." 이다. 쉽게 말하자면 부모 클래스에 알고리즘의 골격인 템플릿을 정의하고 일부 변경되는 로직은 자식 클래스에 정의한다. 즉, 상속과 오버라이딩을 통한 다형성으로 문제를 해결하는 것이다.
템플릿 메소드는 상속을 사용하기 때문에 상속에서 오는 단점들을 그대로 들고 간다. 가장 큰 단점은 자식 클래스와 부모 클래스가 강하게 결합되어있다는 것이다. 만약 부모 클래스가 수정되어야 한다면 자식 클래스 또한 영향을 미친다. 예를 들면 부모 클래스에서 필드 값이 추가된다면 자식 클래스는 생성자 또한 강제로 수정되어야 하는 것이다. 이를 보다 개선하기 위해 '상속' 보다는 '위임'을 택한다. 인터페이스를 전달하여 '위임'하고 구현하는 방식으로 강한 결합을 다소 해결하는 것이다. 이것이 전략 패턴의 사용이다.
GOF에서는 전략패턴의 목적을 다음과 같이 이야기한다. "알고리즘 제품군을 정의하고 각각을 캡슐화하여 상호 교환 가능하게 만든다. 전략을 사용하면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다."
템플릿 메소드 패턴은 변하지 않는 부분(부가 기능)을 부모 클래스(추상 클래스)로 만들고 변하는 부분(핵심 기능)을 상속받은 자식 클래스가 사용하며 구현되었다. 반면 전략 패턴은 변하지 않는 부분(부가 기능)을 Context라는 클래스를 만들어 구현한다. 그리고 변하는 부분을 Strategy라는 Interface를 만들고 필요한 형태의 구현체를 원하는 만큼 만들어둔다. 원하는 기능의 구현은 변하지 않는 부분이 변하는 부분을 참조하면서 이뤄진다. 이때 참조는 Interface를 참조, 역할에만 집중하기 때문에 결합은 약해진다.
전략 패턴의 두 가지 구현 방법
1. Context의 필드에 Strategty를 필드로 갖는다.
public class Context {
private final Strategy strategy;
public Context (Strategy strategy) {
this.strategy = strategy;
}
public void execute() {
long startTimeMs = System.currentTimeMillis();
strategy.call();
long lastTimeMs = System.currentTimeMillis();
long resultTime = lastTimeMs - startTimeMs;
log.info("result time = {}", resultTime);
}
}
2. Context 실행 시에 파라미터로 Strategy를 넘겨준다.
public class Context {
public void execute(Strategy strategy) {
long startTimeMs = System.currentTimeMillis();
strategy.call();
long lastTimeMs = System.currentTimeMillis();
long resultTime = lastTimeMs - startTimeMs;
log.info("result time = {}", resultTime);
}
}
- 사용 시점에 인자를 통해서 전략을 전달받는다.
- 전달받는 시점에 사용은 되지 않고 후에 사용됨. CallBack()이라고도 불린다.
템플릿 콜백 패턴
전략 패턴의 두 가지 구현 방법 중에 인자로 전략을 전달하는 형태를 템플릿 콜백 패턴이라고 한다. 이는 GOF에서는 정의 되지는 않았다. 스프링에서 주로 이런 형태의 패턴이 구성되기 때문에 스프링에서 사용하는 템플릿 콜백 패턴이라고 이야기한다. 이 템플릿 콜백 패턴을 통해 실행 시점에 전략을 전달하여 로깅을 구현할 수 있다.
위 패턴들의 문제점
위 패턴들을 사용하여 핵심 코드, 부가 기능 코드를 분리하고 모듈화할 수 있다는 것을 알았다. 하지만 로그 적용을 위해 원본 코드에 손을 대야한다는 한계점이 있다. 이를 '프록시' 개념을 통해 해결할 수 있다.
드디어 프록시 개념의 도입
원본 코드의 변경 없이 원하는 기능을 도입하기 위해서는 프록시 개념이 필요하다. 위의 그림에서처럼 클라이언트가 서버에 직접 요청을 했다고 하면 서버에서는 직접 모든 것을 모두 처리해야했기 때문에 변경이 필요하다.
여기서 클라이언트와 서버 사이에 대체자를 하나 만들고 이 대체자가 로그 기능을 구현해준다면? 그렇게 되면 서버는 원래 역할만 하면 되고 프록시는 로그의 기능만 하면 된다. 객체지향의 다형성 기능을 통해서 말이다. 두 가지 방법으로 구현이 가능하다.
- 서버가 인터페이스를 가질 때 (인터페이스 구현)
- 인터페이스를 구현했지만 부가기능만 구현한 구현체를 만든다.
- 구현체는 내부적으로 기존 서버의 참조값을 가진다.
- 서버가 인터페이스가 없을 때 (상속)
- 서버를 상속받은 클래스를 만든다.
- 자식 클래스는 내부적으로 부모 클래스 타겟을 가진다.
- 부가 기능을 수행하다가 필요한 시점이 오면 타켓의 메인 메소드를 실행한다.
위 방법을 참고하여 프록시 객체를 만들고 필요한 기능을 구현해준 다음에 원래 객체는 객체의 일을 수행하도록 한다. 프록시 객체는 타겟이 되는 객체를 상속이나 구현한 클래스이다. 내부적으로는 타겟 필드만 하나 더 가진다. 또 오버라이딩 된 메소드를 실행하고 오버라이딩 된 메소드 내부에 실제 타겟의 메소드도 실행하도록 한다.
예시를 들어보도록 하겠다. 비즈니스 로직을 수행하는 OriginController가 있다고 생각해보자. 이 OriginController의 인터페이스를 하나 만들고 프록시 클래스인 OriginProxyController를 이를 구현하도록 한다. 구조는 다음과 같다.
@RestController
@RequiredArgsConstructor
public class OriginalProxyController implements IOriginalProxyController {
private final OriginalController target;
private final LogTracer logTracer;
@Override
public ResponsEntity getPostings() {
Long startTimeMs = System.currentTimeMillis();
ResponseEntity response = target.getPostings();
Long endTimeMs = System.currentTimeMillis();
log.info("result time = {}", endTimeMs - startTimeMs);
return response;
}
}
@RestController를 통해 실제 컨트롤러가 아닌 프록시 컨트롤러를 스프링 빈으로 등록하게 한다. 원래 실제 컨트롤러는 @Component로만 등록하여 DI만 받을 수 있도록 한다. 이렇게 되면 요청이 되면 프록시 컨트롤러가 반응하여 로깅을 시작하고 진짜 원래 컨트롤러가 비즈니스 로직을 수행하고 로깅을 끝마칠 수 있다.
프록시의 개념은 두 가지로 구현이 가능하다
프록시 패턴과 데코레이터 패턴은 둘다 프록시 객체를 사용하고 패턴 모양 역시 비슷하다. 그래서 프록시의 개념은 이 두 패턴을 사용하여 구현할 수 있는데 주의할 점은 의도에 따라서 두 패턴의 쓰임새가 다르다는 것이다. 프록시 패턴과 데코레이터 패턴의 차이는 패턴의 목적에서 온다.
- 프록시 패턴 : 프록시의 접근 제한을 살리기 위한 패턴(캐싱, 지연 로딩, 권한 관리 등)
- 데코레이터 패턴 : 프록시의 부가 기능을 살리기 위한 패턴(로그 추적기, 시간 추적기 등)
프록시 패턴 테스트 코드 작성
각 패턴의 구조를 보면서 이해해보도록 하자.
프록시 패턴의 의도는 '접근 제한'이다. 권한 관리가 될 수도 있고 캐싱, 지연로딩이 될 수도 있다. 아래 작성된 테스트 코드에서는 '캐싱'에 의미를 둔다.
Subject 인터페이스
public interface Subject {
String operation();
}
Subject 구현체 (실제 타겟값)
@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();
}
}
}
프록시 객체 (캐시)
package example.proxy.pureproxy.proxy.code;
import lombok.extern.slf4j.Slf4j;
@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 ProxyPatternClient {
private Subject subject;
public ProxyPatternClient(Subject subject) {
this.subject = subject;
}
public void execute() {
subject.operation();
}
}
프록시 도입 전, 테스트 코드
@Test
void noProxyTest() {
RealSubject realSubject = new RealSubject();
ProxyPatternClient client = new ProxyPatternClient(realSubject);
cilent.execute();
cilent.execute();
cilent.execute();
}
- 클라이언트 코드가 실제 실행 객체를 알고 있다.
- 실제 실행 객체는 execute() 한번 당 1초를 sleep 한다.
- 실제 실행 객체는 execute()를 3번 했으므로 3초를 sleep 한다.
프록시 도입 후, 테스트 코드
@Test
void cacheProxyTest() {
RealSubject realSubject = new RealSubject();
CacheProxy cacheProxy = new CacheProxy(realSubject);
ProxyPatternClient client = new ProxyPatternClient(cacheProxy);
client.execute();
client.execute();
client.execute();
}
- 프록시 객체(cacheProxy)는 내부적으로 realSubject를 가진다.
- 클라이언트는 CacheProxy 객체에 의존한다.
- 클라이언트는 execute() 첫번째 실행만 실제 객체에 접근한다. 클라이언트(client)가 의존하고 있는 프록시 객체가 실제 객체에 접근후 데이터를 한번 가져오면, 그 이후로는 내부 저장소에 값을 가지고 다음부터는 내부 저장소에 있는 값을 Return 하기 때문이다.
프록시 패턴 도입 결과
내부적으로 접근 제어(캐싱)을 하기 위해 프록시 패턴을 도입했다. 타겟 클래스(realSubject)가 인터페이스를 가지고 있고 이 인터페이스를 똑같이 구현한 프록시 클래스를 하나 만든다. 그 프록시 클래스는 내부적으로 타겟 클래스를 참조로 갖는다. 클라이언트는 동일한 인터페이스 타입이 전달되므로 이 객체가 프록시인지 실제 타겟인지 구분할 수 없다. 이렇게 프록시 객체를 클라이언트가 참조하게 함으로써 원본 코드를 수정하지 않고 캐싱 기능을 추가할 수 있다.
데코레이터 패턴 테스트 코드 작성 : 인터페이스가 있는 경우
데코레이터 패턴은 부가기능을 추가하고자 하는 의도를 가질때 사용한다. 구성은 다를 것이 없다. 의도만 다르다. TimeDecorator와 MessageDecorator와 같이 실행시간, 받아온 데이터에 message를 덧붙일 때 사용하는 부가기능들이 있다. 테스트 코드에서는 받아온 데이터에 "*******"를 붙이고 실행시간을 출력하는 데코레이터를 작성한다.
실제 타겟 클래스(RealComponent)의 인터페이스
public interface Component {
String operation();
}
실제 타겟 클래스(RealComponent)의 구현체
@Slf4j
public class RealComponent implements Component {
@Override
public String operation() {
log.info("RealComponent 실행! ");
return "data";
}
}
프록시 클래스 : TimeDecorator (실행 시간 측정기)
@Slf4j
public class TimeDecorator implements Component {
private Component component;
public TimeDecorator(Component component) {
this.component = component;
}
@Override
public String operation() {
log.info("Time Decorator 실행 ! ");
long startTimeMs = System.currentTimeMillis();
// 핵심 기능 수행
String result = component.operation();
long endTimeMs = System.surrentTimeMillis();
long resultTimes = endTimeMs - startTimeMs;
// 부가 기능 수행
log.info("Time Decorator 종료, Result Time = {}", resultTimes);
return result;
}
}
프록시 클래스 : MessageDecorator (문자에 ****** 추가하기 )
@Slf4j
public class MessageDecorator implements Component {
private Component component;
public MessageDecorator(Component component) {
this.component = component;
}
@Override
public String operation() {
log.info("Message Decorator 실행 ! ");
// 핵심 기능 실제 객체 호출 "data"를 돌려준다.
String result = component.operation();
// 부가 기능
String decoResult = "******" + result + "******";
log.info("MessageDecorator 적용 전 = {}, 적용 후 = {}", result, decoResult);
return decoResult;
}
}
데코레이터 패턴 클라이언트
@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 client = new DecoratorPatternClient(realComponent);
client.execute();
}
- 클라이언트는 realComponent에 의존한다.
- client.execute()를 하면 realComponent로 부터 호출되고 "data"라는 문자열을 돌려받는다.
데코레이터 패턴 도입 후, 테스트 코드 (시간 출력 + 문자열 변경)
@Test
void decorator() {
RealComponent realComponent = new RealComponent();
MessageDecorator messageDecorator = new MessageDecorator(realComponent);
TimeDecorator timeDecorator = new TimeDecorator(messageDecorator);
DecoratorPatternClient client = new DecoratorPatternClient(timeDecorator);
client.execute();
}
- 메시지 데코레이터는 내부적으로 실제 객체를 참조한다.
- 시간 데코레이터는 내부적으로 메시지 데코레이터를 참조한다.
- 데코레이터 클라이언트는 시간 데이터 객체를 참조한다. execute를 수행하면 시간 데코레이터 - 메시지 데코레이터 - 실제 객체 순으로 실행이된다.
데코레이터 패턴 도입 결과
위와 같은 테스트 코드로 작성하면 다음과 같은 실행 순서를 지닌다.
원본 코드의 변경 없이 프록시 개념을 사용하여 시간을 측정하고 문자열을 변경하는 부가 기능을 추가하여 realComponent가 실행되는 구조이다.
데코레이터 패턴 테스트 코드 작성 : 인터페이스가 없는 경우
프록시 객체는 같은 타입의 다형성을 이용하여 구성하는 것이다. 인터페이스가 없으면 실제 객체를 똑같이 상속받은 클래스를 만들고 내부적으로 부모 클래스를 타겟으로 가진다. 그리고 메소드를 오버라이딩 하여 필요한 부가 기능을 구현하도록 할 수 있다.
실제 타겟 클래스
@Slf4j
public class ConcreteLogic {
public void call() {
log.info("콘크리트 로직 실행 ! ");
}
}
프록시 클래스 : 타겟 클래스 상속
@Slf4j
public class TimeProxy extends ConcreteLogic {
private final ConcreteLogic target;
public TimeProxy(ConcreteLogic target) {
this.target = target;
}
@Override
public void call() {
long startTime = System.currentTimeMillis();
target.call();
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("실행 완료, resultTime = {}", resultTime);
}
}
클라이언트 클래스 : 단순 실행
public class ConcreteClient {
private final ConcreteLogic concreteLogic;
public ConcreteClient(ConcreteLogic concreteLogic) {
this.concreteLogic = concreteLogic;
}
public void execute() {
concreteLogic.call();
}
}
테스트 코드
// 실제 객체만 호출.
@Test
void noProxyTest() {
ConcreteLogic concreteLogic = new ConcreteLogic();
ConcreteClient client = new ConcreteClient(concreteLogic);
client.execute();
}
// 실행 시간까지 같이 측정
@Test
void proxyTest() {
ConcreteLogic concreteLogic = new ConcreteLogic();
TimeProxy timeProxy = new TimeProxy(concreteLogic);
ConcreteClient client = new ConcreteClient(timeProxy);
client.execute();
}
- noProxyTest는 클라이언트가 실제 객체에 의존한다. 따라서 실제 객체의 기능만 수행한다.
- proxyTest는 실제 객체를 가지고 있는 프록시 객체에 클라이언트가 의존한다. 따라서 실행 시간이 측정되면서 실제 객체의 기능도 함께 수행한다.
상속받은 데코레이터 패턴 도입 정리
실제 객체는 인터페이스를 가지고 있지 않았다. 인터페이스가 없었기 때문에 실제 객체를 상속받은 자식 클래스를 만들었다. 자식 클래스는 내부적으로 부모 클래스를 타겟으로 가졌다. 그리고 부모 클래스의 메소드를 오버라이딩하여 필요한 기능을 구현했다. 이 역시 의존 관계 설정만 해주어 원본 코드의 수정 없이 필요한 기능을 추가할 수 있다.
프록시 개념 로그 추적기에 도입하기
테스트 코드를 작성하며 알게 된 것을 잠시 정리해보자.
- 실제 객체를 동일하게 상속 또는 구현한 프록시 객체를 만들어 내부적으로 실제 객체를 참조하게 한다.
- 프록시 객체와 실제 객체 간의 의존관계를 잘 설정한다.
위 과정을 거치면 원본 코드 수정 없이 필요로 하는 부가기능을 추가할 수 있게 된다. 이를 로그 추적기에 적용하기 위해서는 다음과 같은 작업을 진행해야한다.
- 인터페이스를 구현한 프록시 클래스를 만든다. 인터페이스가 없다면 구체 클래스를 상속 받은 프록시 클래스를 만든다.
- 프록시 객체가 내부적으로 실제 클래스를 갖도록 의존관계를 설정하고 스프링 빈으로 등록한다.
인터페이스가 존재하는 컨트롤러, 서비스, 레포지토리의 의존관계는 위와 같았다. 이를 프록시 객체를 통해 아래와 같이 의존관계를 만들 것이다.
프록시 클래스 만들기 : 인터페이스 있는 클래스 (예제는 Service 하나만) - 구현
public class OderServiceInterfaceProxy implements OrderServiceV1 {
private final OrderServiceV1 target;
private final LogTrace logTrace;
public OderServiceInterfaceProxy(OrderServiceV1 target, LogTrace logTrace) {
this.target = target;
this.logTrace = logTrace;
}
@Override
public void orderItem(String itemId) {
TraceStatus status = null;
try {
status = logTrace.begin("orderService.orderItem()");
// 로직 호출
target.orderItem(itemId);
logTrace.end(status);
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
- 내부적으로 Target을 갖도록 만든다. 로그를 찍어야 하므로 로그 추적기도 갖는다.
- 기존 템플릿 콜백 패턴의 로직을 가져온다. 필요 시점에 target.orderItem()으로 실제 객체의 핵심 기능을 수행할 수 있도록 작성한다.
프록시 클래스 만들기 : 인터페이스 없는 클래스 (예제는 Service 하나만) - 상속
public class OrderServiceConcreteProxy extends OrderServiceV2 {
private final OrderServiceV2 target;
private final LogTrace logTrace;
public OrderServiceConcreteProxy(OrderServiceV2 target, LogTrace logTrace) {
super(null);
this.target = target;
this.logTrace = logTrace;
}
@Override
public void orderItem(String itemId) {
TraceStatus status = null;
try {
status = logTrace.begin("orderService.orderItem()");
// 로직 호출
target.orderItem(itemId);
logTrace.end(status);
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
- 내부적으로 Target과 로그 추적기를 갖도록 한다.
- 기존 테플릿 콜백 패턴의 로직을 가져오고 필요 시점에 target.orderItem()으로 실제 객체의 핵심 기능을 수행하도록 한다.
- 생성자에서 부모 클래스 생성도 함께 들어가는데 부모 클래스는 사용할 일이 없다. 따라서 Null을 넘겨준다.
인터페이스 있는 프록시 클래스의 의존관계 설정
@Configuration
public class InterfaceProxyConfig {
@Bean
public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
OrderControllerV1 orderControllerV1 = new OrderControllerV1Impl(orderServiceV1(logTrace));
OrderControllerV1 proxy = new OrderControllerInterfaceProxy(orderControllerV1, logTrace);
return proxy;
}
@Bean
public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
OrderServiceV1 orderServiceV1 = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
OrderServiceV1 proxy = new OrderServiceInterfaceProxy(orderServiceV1(orderServiceV1, logTrace));
return proxy;
}
@Bean
public OrderRepositoryV1 ordeRepositoryV1(LogTrace logTrace) {
OrderRepositoryV1 orderRepositoryV1 = new OrderRepositoryV1Impl();
OrderRepositoryV1 proxy = new OrderRepositoryInterfaceProxy(orderRepositoryV1, logTrace);
return proxy;
}
}
- 의존관계를 설정한다.
- 프록시 객체가 실제 객체를 참조해야하기 때문에 실제 객체를 먼저 생성한다.
- 프록시 객체를 생성하면서 내부적으로 실제 객체를 참조할 수 있도록 변수를 넘겨준다.
- 프록시 객체를 리턴한다.
위처럼 실행하면 내부적으로 실제 객체를 참조하는 프록시 객체가 빈 이름으로 등록된다. orderRepositoryV1를 예로 들자면 orderRepositoryV1이라는 빈 이름으로 OrderRepositoryV1 인터페이스를 구현한 프록시 객체가 스프링 빈으로 등록이 되는 것이다.
인터페이스 없는 프록시 클래스의 의존관계 설정
@Configuration
public class ConcreteProxyConfig {
@Bean
public OrderControllerV2 orderControllerV2(LogTrace logTrace) {
OrderControllerV2 orderControllerV2 = new OrderControllerV2(orderServiceV2(logTrace));
OrderControllerV2 proxy = new OrderControllerConcreteProxy(orderControllerV2, logTrace);
return proxy;
}
@Bean
public OrderServiceV2 orderServiceV2(LogTrace logTrace) {
OrderServiceV2 orderServiceV2 = new OrderServiceV2(orderRepositoryV2(logTrace));
OrderServiceV2 proxy = new OrderServiceConcreteProxy(orderServiceV2, logTrace);
return proxy;
}
@Bean
public OrderRepositoryV2 orderRepositoryV2(LogTrace logTrace) {
OrderRepositoryV2 orderRepositoryV2 = new OrderRepositoryV2();
OrderRepositoryV2 proxy = new OrderRepositoryConcreteProxy(orderRepositoryV2, logTrace);
return proxy;
}
}
- 의존관계를 설정한다.
- 프록시 객체가 실제 객체를 참조해야하기 때문에 실제 객체를 먼저 생성한다.
- 프록시 객체를 생성하면서 내부적으로 실제 객체를 참조할 수 있도록 변수를 넘겨준다.
- 프록시 객체를 리턴한다.
위처럼 실행하면 내부적으로 실제 객체를 참조하는 프록시 객체가 빈 이름으로 등록된다. orderRepositoryV2를 예로 들자면 orderRepositoryV2이라는 빈 이름으로 OrderRepositoryV2 클래스를 상속받은 프록시 객체가 스프링 빈으로 등록이 되는 것이다.
정리
핵심은 관점지향 프로그래밍 즉, 핵심 기능과 부가 기능을 구분하여 관점을 기준으로 모듈화 하는데 있어서 사용된 개념이 무엇인가이다. 그것이 바로 프록시 개념 이고 프록시 개념을 도입하기 위해 디자인 패턴 중 프록시 패턴과 데코레이터 패턴을 사용할 수 있다는 것이다. 모듈화의 필요성으로 템플릿 메소드, 전략, 템플릿 콜백 패턴을 사용했고 이것들의 단점은 원본 코드에 손을 대야했다. 이를 개선하기 위해 프록시 개념을 도입하였고 사용한 패턴이 프록시 패턴, 데코레이터 패턴이었다.
하지만 문제점은 또 다시 등장한다. 바로 부가 기능을 도입하고자 하는 클래스의 개수만큼 필요한 클래스를 만들어야 한다는 것. 이것은 동적 프록시 적용 (JDK 동적 프록시, CGLIB, ProxyFactory)으로 해결할 수 있다. 이것을 이해하기 위해서는 다시 AOP의 용어들을 알아야 한다. 그래서 다음 포스팅에서는 AOP 용어들을 정리해보고 그 다음에 알아보려고 한다.
이 포스팅을 보면서 관점을 기준으로 어떻게 모듈화 했는가에 대한 흐름을 잡기 위해서라는 것을 기억하자. 아마도 그렇게 된다면 이후 포스팅도 이해하는데 어려움이 없을 것이다.
이번 포스팅을 하면서 참고한 블로그에 좋은 참고사항이 있어서 몇자 적고 마무리하겠다.
인터페이스 기반 프록시 vs 상속 기반 프록시
1. 상속 기반 프록시에는 몇가지 제약이 있다.
- 부모 클래스의 생성자를 호출해야 한다.
- 클래스에 final 키워드가 붙으면 상속이 불가능하다.
- 메소드에 final 키워드가 붙으면 해당 메소드를 오버라이딩 할 수 없다.
2. 인터페이스 기반은 역할과 구현이 나누어져 있다.
3. 인터페이스와 구현으로 나누는 것을 모든 것에 도입하는 것은 바람직하지 않다. 변화가 많은 곳에는 인터페이스를 도입하여 효율을 기대할 수 있겠으나 변화가 없는 곳이라면 그리 실용적이지 않다.
출처
'Programing & Coding > Spring' 카테고리의 다른 글
[Spring] 6. Spring PSA (0) | 2023.05.26 |
---|---|
[Spring] 5. Spring AOP - 총정리 (2) (0) | 2023.05.22 |
[Spring] 3. Spring 핵심 3대요소 (IoC/DI, AOP, PSA) (2) | 2023.05.07 |
[Spring] 2. Spring 기초, 핵심 원리 이해 (0) | 2023.04.28 |
[Spring] Spring 에서의 싱글톤 패턴 (0) | 2023.03.12 |