이전 글에서 탬플릿 콜백 패턴으로 부가기능과 핵심 기능을 정말 잘 나누었지만 원본 코드(핵심 기능만 있던 코드)를 수정할 수 밖에 없는 상태였다. 이를 해결하기 위해선 프록시를 알아야 한다. 프록시에 대해 배울 겸 추가된 요구사항에 대해서도 배우자.
1. 특정 메서드에는 로그 기능을 사용하지 않을 것.
2. 다양한 케이스(i. 인터페이스 있는 구체 클래스 ii. 인터페이스 없는 구체 클래스 iii. 이전 두 방식은 수동 빈 등록을 사용할 것이고 마지막 방식은 컴포넌트 스캔을 적용한 클래스) 에 대해서도 만족해야 한다.
0. 프록시란?
컴퓨터 네트워크에서도 자주 다루 듯이 중간자 또는 대리자를 의미한다. client는 server에게 요청 하는 것이 아닌 proxy에게 요청을 하고 proxy가 server에게 클라이언트 대신 요청을 하는 역할을 한다. 응답을 하는 과정에서도 마찬가지.
프록시를 사용하는 이유는 중간자로서의 역할을 할 수 있다는 것이다. 예를 들면 캐싱, 접근 제어, 부가 기능 추가, 프록시 체인, 지연 로딩 등등..
이런 장점들 중 우리의 주 목표는 부가 기능 추가가 될 것이다. 클라이언트는 서버를 호출하는지 proxy를 호출하는지 구분을 못해야 하고 이를 위해 단순히 하나의 인터페이스를 구체 클래스와 proxy 두 가지를 구현하고 클라이언트가 proxy를 구체 클래스로서 의존하게 하면 된다.
프록시를 이용한 디자인 패턴은 크게 두 가지로 나뉜다. 물론 의도에 따라 나뉘고 캐싱, 접근 제어가 목적일 경우 프록시 패턴, 부가 기능을 추가하는 것이 목적일 경우 데코레이터 패턴이라 부른다. 우리의 목표는 이 데코레이터 패턴을 잘 다루자는 것이당!
1. 데코레이터 패턴
왼쪽 사진과 같이 Componenet에 대한 두 개의 구체 클래스가 존재하고 Client는 데코레이터를 사용하고 데코레이터는 실제 target인 RealComponent를 호출한다.
그렇다면 의존관계를 엮는 코드와 decorator 코드는 아래와 같을 것이다.
@Test
void decorator1() {
Component realComponent = new RealComponent();
Component messageDecorator = new MessageDecorator(realComponent);
DecoratorPatternClient client = new DecoratorPatternClient(messageDecorator);
client.execute();
}
@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 decoratorResult = "*****" + result + "*******";
return decoratorResult;
}
}
보이다 시피 operation을 호출하여 target을 불러내고 앞뒤로 ****로 결과 값을 꾸며냈다.
그렇다면 이번엔 실행 시간을 측정하는 TimeDecorator 또한 적용해 보자.
그럼 위와 같은 의존관계를 맺게 될 것이다. 하지만, 데코레이터 자체는 component가 반드시 필요하고 호출하는 코드가 존재할 수 밖에 없다. 그렇기에 이러한 중복 코드 조차 없애기 위해 Decorator라는 추상 클래스를 만들어 상속해서 만들어 낸다. 아래 사진처럼.
위 사진까지 적용한 모습이 바로 흔히 말하는 데코레이터 패턴의 정석이다.
1.1 인터페이스 있는 구체 클래스에 데코레이터 패턴을 적용하기.
인터페이스가 있다는 점을 이용하여 위의 예시 처럼 데코레이터는 인터페이스를 구현하고 target 객체를 주입받는다. 아래 사진 처럼.
그럼 런타임에 각 클라이언트는 proxy를 의존하고 각 프록시는 각 target을 요청하고 이 target은 다시 클라이언트가 되는 방식일 것이당. 그렇다면 Repository의 프록시는 아래 코드와 같을 것이다.
@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.request()");
//target 호출
target.save(itemId);
logTrace.end(status);
}catch (Exception e){
logTrace.exception(status, e);
throw e;
}
}
}
또한 각각의 의존관계 주입 상태는 아래 코드와 같을 것이다. 로그를 호출하지 않을 경우의 프록시 코드에서는 target.noLogMethod()와 같이 호출하면 끝난다.
@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);
}
}
각 단계에서 의존하는 다음 단계는 proxy임을 알 수 있다. 물론 컨테이너에는 프록시 객체가 등록되고 각 프록시 객체는 각 target객체를 의존한다. 아래 사진처럼.
1.2 인터페이스가 없는 구체 클래스에 적용
이전에는 인터페이스를 활용해서 프록시를 적용할 수 있었는데 이번에는 어떻게 해야할까? 인터페이스의 다형성을 이용한 것처럼 이번에는 상속의 다형성을 이용한다.
부모는 마음이 넒으니 클라이언트는 부모 타입으로 자식인 프록시를 주입 받고 자식은 부모의 기능을 호출하고 데코레이팅 하는 방식이다. 이를 보여줄 코드를 나열해 보자.
public class ConcreteClient {
private ConcreteLogic concreteLogic;
public ConcreteClient(ConcreteLogic concreteLogic) {
this.concreteLogic = concreteLogic;
}
public void execute(){
concreteLogic.operation();
}
}
public class ConcreteProxyTest {
@Test
void addProxy(){
ConcreteLogic concreteLogic = new ConcreteLogic();
TimeProxy timeProxy = new TimeProxy(concreteLogic);
ConcreteClient client = new ConcreteClient(timeProxy);
client.execute();
}
}
위 코드 처럼 Client는 ConcreteLogic을 사용하지만 의존관계 주입에서는 자식을 주입받는 형태를 띄고 있다.
뿐만 아니라 프록시는 아래 코드 처럼 ConcreteLogic을 상속 받아 기능을 사용하는 모습을 볼 수 있다.
@Slf4j
public class TimeProxy extends ConcreteLogic{
private ConcreteLogic concreteLogic;
public TimeProxy(ConcreteLogic concreteLogic) {
this.concreteLogic = concreteLogic;
}
@Override
public String operation() {
log.info("TimeDecorator 실행");
long startTime = System.currentTimeMillis();
String result = concreteLogic.operation();
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeDecorator 종료 resultTime={}ms", resultTime);
return result;
}
}
자식이 부모를 주입 받는 모습을 볼 수 있다. 물론 주입 안받고 super.operation()과 같이 해도 되지만 중요한 것은! 탬플릿 메서드 패턴은 부모 정보를 사용하지 않는다는 점과 다르게 사용한다는 것이 중요하다.
이를 우리의 요구사항에 접목 시키면 아래와 같다.
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();
}
}
Controller는 위와 같고 위 문단에서 한 것 처럼 수동으로 의존관계 주입할 때 proxy를 주입 받게 하는 Configuration 코드가 필요하다.
위에서 보이듯 클래스 기반 프록시에 단점이 보인다. 바로 자바 기본 문법에 의해 자식 클래스를 생성할 때는 항상 super() 로 부모 클래스의 생성자를 호출해야 한다. 이 부분을 생략하면 기본 생성자가 호출된다. 인터페이스가 있을 경우 이러한 고민을 하지 않아도 된다.
1.3 공통된 문제점
지금까지 프록시를 사용해서 기존 코드를 변경하지 않고, 로그 추적기라는 부가 기능을 적용할 수 있었다. 그런데 문제는 프록시 클래스를 너무 많이 만들어야 한다는 점이다. 잘 보면 프록시 클래스가 하는 일은 LogTrace 를 사용하는 것인데, 그 로직이 모두 똑같다. 대상 클래스만 다를 뿐이다. 만약 적용해야 하는 대상 클래스가 100개라면 프록시 클래스도 100개를 만들어야한다.
프록시 클래스를 하나만 만들어서 모든 곳에 적용하는 방법은 없을까? 바로 다음에 설명할 동적 프록시 기술이 이 문제를 해결해준다.
2. 중간 정리
잠시 여기까지 정리하자면, 핵심 기능과 부가기능을 분리하기 위한 여정이었다.
이를 해결하기 위했던 탬플릿 메서드 패턴은 자식은 필요하지 않는 부모를 상속 받아야 한다는 문제가 존재했다. 즉, 강한 결합이 문제이다. 이를 해결하기 위해 인터페이스를 도입한 전략 패턴이 생겨났다.
전략 패턴은 선 조립 후 사용이고 조립하면 단단하게 결합된다는 점 때문에 싱글톤에서 사용이 불가하니 전략 매서드 패턴(탬플릿 콜백) 패턴이 생겨났다.
하지만 이 탬플릿 콜백 패턴 마저도 부가기능과 핵심 기능을 분리하기는 했지만 부가기능을 호출하는 코드가 필요했다.
이렇게 핵심 기능이 부가기능을 호출하는 것이 아닌 부가기능이 핵심기능을 호출하도록 하고 클라이언트가 부가기능을 의존하도록 하는 방식으로 프록시를 사용하고자 한다!
하지만 지금까지의 방식은 동일한 부가기능을 핵심기능들에 적용하기 위해 똑같은 부가기능을 하는 클래스들을 정말 많이 만들어 내야한다. 뿐만 아니라 의존관계를 직접 설정하기 위해 컴포넌트 스캔 마저도 사용하지 못한다. 이러한 부가기능 코드의 중복을 줄이기 위해 나온 것이 동적 프록시! 이다. 추가적으로 컴포넌트 스캔 빈에게도 프록시를 적용하기 위한 것이 빈 후처리기 이다.
정말 엄청난 여정이다. 정말정말 거의다 왔다. 동적 프록시를 사용하게 되면 핵심 기능 코드에 부가기능을 호출하는 일도 없고 부가기능 코드의 중복 도 없앨 수 있다. 이정도면 정말 다 한거 아닐까? 맞다. 정말 거의 다 했고 이를 좀 더 편리하게 사용할 방법들이 나열 될 것이다.
3. 동적 프록시
동적 프록시는 두 가지의 경우가 있다. 인터페이스가 존재할 경우 jdk동적 프록시를 사용하고 인터페이스 없이 구체 클래스만 존재할 경우 CGLIB 동적 프록시를 사용한다. 이때 jdk 동적 프록시를 이해하기 위해서는 리플렉션 기술이 필요하다. 리플렉션은 반영이라는 말 그대로 클래스나 메서드의 메타 정보를 획득하고 이를 동적으로 호출 할 수 있다.
3.1 리플랙션
@Slf4j
public class ReflectionTest {
@Test
void reflection0(){
Hello target = new Hello();
//공통 로직1 시작
log.info("start");
String result1 = target.callA();// 호출 하는 메소드가 다름
log.info("result={}",result1);
//공통 로직1 종료
//공통 로직2 시작
log.info("start");
String result2 = target.callB();// 호출 하는 메소드가 다름
log.info("result={}",result2);
//공통 로직2 종료
}
@Slf4j
static class Hello{
public String callA(){
log.info("callA");
return "A";
}
public String callB(){
log.info("callB");
return "B";
}
}
}
위와 같은 상황에서 callA와 callB를 제외한 두 부가기능이 중복되는 것을 없앨 수 있을까? 예를 들어 log.info(); method(x)호출;log.info() 방식으로 x를 callA 또는 callB를 넣는 방식! 이것을 reflection으로 해낼 수 있다.
void reflection2() throws Exception {
Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
Hello target = new Hello();
Method methodCallA = classHello.getMethod("callA");
dynamicCall(methodCallA, target);
Method methodCallB = classHello.getMethod("callB");
dynamicCall(methodCallB, target);
}
private void dynamicCall(Method method, Object target) throws Exception {
log.info("start");
Object result = method.invoke(target);
log.info("result={}", result);
}
위 코드처럼 Class.forName인 리플렉션 기술로 메서드 이름을 통해 메서드 메타정보를 얻어내어 dynamicCall 메서드에서 메타 정보를 이용하여 callA 메서드를 호출해낸다.
문자열로 정보를 뽑아내는 모든 위험과 마찬가지로 "callA"를 오타낸다 해도 컴파일 시점에 잡아낼 수 없다. 런타임에 어랏 이런 메서드 존재하지 않아요! 라는 식으로 나타날 것이다. 따라서 리플렉션은 정말 필요한 순간에 써야하지 다른 방식이 있다면 과감하게 포기하는 것이 좋다.
3.2 jdk 동적 프록시
이름 그대로 jdk 이니깐 자바가 제공하는 동적 프록시 기능이다. jdk 동적 프록시에서 사용할 로직은 invocationHandler 인터페이스를 구현해서 작성하면된다.
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
invoke 메서드의 인자를 보면 proxy는 proxy 자신을 의미하고 method는 호출한 메서드, args는 메서드 인자들이다. 구현 코드를 보면 와닿을 것이당.
@Slf4j
public class TImeInvocationHandler implements InvocationHandler {
private final Object target;
public TImeInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
Object result = method.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}",resultTime);
return result;
}
}
target은 interface의 구체 클래스, 즉 proxy를 적용할 타겟을 의미한다. 데코레이터 패턴처럼 time log를 위아래로 호출하는 것을 볼 수 있다. method.invoke()를 통해서 target의 메서드를 메서드에 필요한 args와 함께 호출 하는 것을 볼 수 있다. 사용하는 코드는 아래와 같다.
void dynamicA(){
AInterface target = new AImpl();
TImeInvocationHandler handler = new TImeInvocationHandler(target);
AInterface proxy = (AInterface) Proxy.newProxyInstance(AInterface.class.getClassLoader(),
new Class[]{AInterface.class}, handler);
//어느 클래스 로더인지(프록시가 어디에 생성될지), 어느 인터페이스 기반으로 만들지,프록시가 사용할 로직
proxy.call();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}",proxy.getClass());
}
위 코드에서 proxy를 생성하는 코드가 보인다. Object 타입을 리턴하므로 AInterface로 타입 캐스팅을 하는 것을 볼 수 있고 proxy.call() 메서드를 호출한다. 에엥? 어떻게 이게 자동으로 되지? invoke를 불러야하는 거 아냐? 동작 과정은 아래와 같다.
1. 클라이언트는 JDK 동적 프록시의 call() 을 실행한다.
2. JDK 동적 프록시는 InvocationHandler.invoke() 를 호출한다. TimeInvocationHandler 가 구현체로 있으로 TimeInvocationHandler.invoke() 가 호출된다.
3. TimeInvocationHandler 가 내부 로직을 수행하고, method.invoke(target, args) 를 호출해서 target 인 실제 객체( AImpl)를 호출한다.
4. AImpl 인스턴스의 call() 이 실행된다.
5. AImpl 인스턴스의 call() 의 실행이 끝나면 TimeInvocationHandler 로 응답이 돌아온다. 시간 로그를 출력하고 결과를 반환한다.
즉 , jdk 동적 프록시 객체일 경우 invocationHandler를 호출하고 우리가 만들어낸 구현체를 호출한다. 이 덕에 프록시 객체의 기능을 하고 target객체를 호출하여 원래의 메서드를 호출하는 모습을 볼 수 있다.
이제 우리의 프로젝트에 적용해보자. 그 중 인터페이스가 있는 구현 클레스인 1번째 요구사항에 적용할 것이다.
public class LogTraceBasicHandler implements InvocationHandler {
private final Object target;
private final LogTrace logTrace;
public LogTraceBasicHandler(Object target, LogTrace logTrace) {
this.target = target;
this.logTrace = logTrace;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
TraceStatus status = null;
try {
String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()";
status = logTrace.begin(message);
//로직 호출
Object result = method.invoke(target, args);
logTrace.end(status);
return result;
}catch (Exception e){
logTrace.exception(status, e);
throw e;
}
}
}
즉 위와 같이 InvocationHandler의 구현체를 만들고 이 하나의 녀석이 controller, service, repository 각각에게 적용 될 것이다. 아래와 같이 config 파일을 만들어 내어 빈 대신 프록시를 등록하자.
@Configuration
public class DynamicProxyBasicConfig {
@Bean
public OrderControllerV1 orderControllerV1(LogTrace logTrace){
OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
OrderControllerV1 proxy = (OrderControllerV1)Proxy.newProxyInstance(OrderControllerV1.class.getClassLoader(), new Class[]{OrderControllerV1.class},
new LogTraceBasicHandler(orderController, logTrace));
return proxy;
}
@Bean
public OrderServiceV1 orderServiceV1(LogTrace logTrace){
OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
OrderServiceV1 proxy = (OrderServiceV1)Proxy.newProxyInstance(OrderServiceV1.class.getClassLoader(), new Class[]{OrderServiceV1.class},
new LogTraceBasicHandler(orderService, logTrace));
return proxy;
}
@Bean
public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace){
OrderRepositoryV1Impl orderRepository = new OrderRepositoryV1Impl();
OrderRepositoryV1 proxy = (OrderRepositoryV1)Proxy.newProxyInstance(OrderRepositoryV1.class.getClassLoader(), new Class[]{OrderRepositoryV1.class},
new LogTraceBasicHandler(orderRepository, logTrace));
return proxy;
}
}
위 설정 파일의 중요한 점은 각각의 Bean에 target객체를 둘러싼 proxy를 리턴한다는 것이다!
즉, 아래 사진처럼 동적 proxy가 나타나 데코레이팅을 하는 것을 볼 수 있다.
그렇다면 하나의 bean 객체에서 데코레이팅을 안하고 싶은 method에 대해서는 어떻게 해야할까? 그것은 아래의 코드를 보면 된다.
public class LogTraceFilterHandler implements InvocationHandler {
private final Object target;
private final LogTrace logTrace;
private final String[] patterns;
public LogTraceFilterHandler(Object target, LogTrace logTrace, String[] patterns) {
this.target = target;
this.logTrace = logTrace;
this.patterns = patterns;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//메서드 이름 필터
String methodName = method.getName();
//save, request, req*, *est
if(!PatternMatchUtils.simpleMatch(patterns, methodName)){
//적용할 메서드가 아니면 프록시 적용 하지 말고 method 호출
return method.invoke(target, args);
}
...필터에 걸리지 않는다면 프록시 적용...
}
}
위 처럼 patterns를 생성시 주입받아 PatternMatchUtils의 matching 기능을 이용하여 프록시를 생성할지 말지 결정한다.
jdk 동적 프록시 기술은 이처럼 인터페이스가 있는 경우에만 가능하다. 그렇다면 인터페이스가 없는 구체 클래스에는 어떻게 적용 될까?
4. CGLIB
CGLIB는 초기에도 심심치 않게 보였었다. 예를 들어 Configuration 빈 같은 경우는 CGLIB로 조작되어 올라가기도 했고 ObjectProvider를 사용할 경우에도 보였었다. CGLIB는 바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다. CGLIB를 사용하면 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다. CGLIB는 원래는 외부 라이브러리인데, 스프링 프레임워크가 스프링 내부 소스 코드에 포함했다. 따라서 스프링을 사용한다면 별도의 외부 라이브러리를 추가하지 않아도 사용할 수 있다.
즉, jdk 동적 프록시와 유사하니 차이점을 위주로 살펴보자. jdk 동적 프록시 같은 경우 invocationHandler를 구현한 것 처럼 CGLIB를 생성하고 싶다면 MethodInterceptor를 구현해야 한다.
public interface MethodInterceptor extends Callback {
Object intercept(Object obj, Method method, Object[] args, MethodProxy
proxy) throws Throwable;
}
각 인자들은 handlerInterceptor와 동일하다. 아무튼 아래와 같이 구현하고..
@Slf4j
public class TimeMethodInterceptor implements MethodInterceptor {
private final Object target;
public TimeMethodInterceptor(Object target) {
this.target = target;
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
Object result = methodProxy.invoke(target, args);
//Object result = method.invoke(target, args);위에게 조금 더 빠르다고 함
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}",resultTime);
return result;
}
}
아래의 코드처럼 사용한다. CGLIB는 구체 클래스를 상속받아 사용한다.
@Slf4j
public class CglibTest {
@Test
void cglib(){
ConcreteService target = new ConcreteService();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(ConcreteService.class);//ConcreteService를 상속 받은 proxy
enhancer.setCallback(new TimeMethodInterceptor(target));//methodInterceptor 넣기. methodInterceptor가 callback을 할당 받음
ConcreteService proxy = (ConcreteService)enhancer.create();//proxy 생성. ConcreteService를 상속 받았기에
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
proxy.call();
}
}
코드를 보면 CGLIB는 enhancer를 이용해 CGLIB를 만들고 구체 클래스와 methodInterceptor를 주면 프록시를 반환해준다.
로그 내용을 보자면 CGLIB일 경우 "대상클래스$$EnhancerByCGLIB$$임의 코드" 와 같은 형식으로 만들어 지고 proxy의 경우 "com.sun.proxy.$Proxy임의 숫자"와 같은 형식으로 생성된다.
CGLIB에는 제약조건이 존재한다. 부모 클래스에 기본 생성자가 필수로 필요하다. 왜냐하면 자식 클래스가 상속받아서 부모를 초기화 해야 하는데 기본 생성자가 없으면.. 어떻게 하죠? 클래스에 final이 붙으면 에러가 나오고 final이 붙은 메서드는 오버라이딩 할 수 없어서 프록시가 제대로 동작하지 않는다.
마무리..
지금 보면 너무나 훌륭하다. 동적 프록시 기술을 사용하면서 제약조건을 몇개만 지켜주면 데코레이팅 하고 싶은 메서드가 있는 클래스 별로 proxy를 만들 필요가 없어졌다. 하지만 여기서의 단점은 jdk 동적 프록시와 CGLIB를 위한 두 개의 프록시 클래스를 만들어 주어야 한다. 뿐만 아니라 특정 조건이 맞을때만 프록시를 적용하게 만드는 로직을 공통으로 묶을 수 없을까? 이를 위해 등장한 것이 스프링이 지원하는 프록시인 프록시 팩토리와 어드바이저가 등장한다. 뿐만 아니라 수동 @Bean 등록의 고민과 컴포넌트 스캔에도 적용하기 위한 빈 후처리기가 있다. 다음 글에서 다루어 보자.
'백앤드(스프링)' 카테고리의 다른 글
AOP 1편 [@Aspect, AOP 1편 정리] (0) | 2022.10.30 |
---|---|
AOP 1편 [프록시 팩토리, 빈 후처리기] (0) | 2022.10.28 |
AOP 1편 [디자인 패턴 - 1] (0) | 2022.10.28 |
토비 스터디 - 자바 ORM 표준 프로그래밍 JPA (0) | 2022.09.07 |
Bean Validation 심화 내용 (0) | 2022.09.04 |