Proxy?
먼저 Proxy가 뭘까요.?
Proxy는 일종의 대리자 입니다
디자인 패턴중에서 Proxy 패턴을 들어본적이 있으신가요?
우리가 특정한 Interface를 노출시키지 않고, 외부로부터 감추고 싶을 때 사용하는 것이
바로 Proxy 패턴입니다.
자세한 내용은 이전에 정리해 놓았으니 이 링크 참고해주세요 ㅎㅎ
그렇다면 Spring에서 지원하는 Proxy와 디자인 패턴에서의 Proxy 패턴은 유사할까요.?
정답은.. 아닙니다
일반적으로 Proxy는 실제 Target의 기능을 대신 수행하면서, 기능을 확장하거나 추가하는 실제 객체를 의미하고,
Proxy 패턴은 Target에 대한 기능을 확장하지는 않고, Client가 Target에 접근하는 방식을 변경해줍니다.
오히려 Proxy는 Template Method Pattern 과 비슷하다라고 할까요.?
그렇다면 왜 사용할까요?
Proxy를 사용하는 이유는 아주 간단합니다
OCP(Open - Closed Principle)을 지키기 위해서 사용합니다
개방 폐쇄 원칙(=OCP)란 소프트웨어는 확장에 대해서는 열려있어야 하고, 수정에 대해서는 닫혀있어야 한다 라는 원칙입니다
어느 정도 원리는 알았으니..
이제 Spring에서 근간이 되면 AOP에 대해서 알아보도록 할게요
AOP(Aspect Oriented Progarmming)
Spring 에서는 Proxy를 바탕으로 우리의 관심사를 추출하는 AOP 를 제공하고 있습니다
그럼 도대체 어떻게 관심사별로 추출할 수 있을까요?
바로 Proxy를 이용한 런타임 위빙(Runtime Weaving) 을 통해서 관심사를 추출할 수 있습니다.
여기서 Runtime Weaving 이란?
Weaving is the process of applying aspects to a target object to create a new, proxied object.
-> Weaving은 target 객체를 새로운 proxied 객체로 적용시키는 과정이다.
그래서 Runtime Weaving은?
Runtime시에 이러한 Weaving이 진행되는 방식이다.!
Spring AOP에서는 이러한 기능을 2가지 방법으로 구사하고 있는데요.
- JDK Dynamic Proxy
- CGlib Proxy
한번 이 둘에 대해서 알아보는 시간을 가져보도록 할게요 ㅎㅎ
JDK Dynamic Proxy
JDK 에서 제공하는 Dynamic Proxy는 1.3 버젼부터 생긴 기능이며,
Interface를 기반으로 Proxy를 생성해주는 방식입니다.!
그렇기 때문에 Interface를 강제화 한다는 단점이 있다는...
Dynamic Proxy는 Invocation Handler를 상속받아서 실체를 구현하게 되는데,
이 과정에서 특정 Object에 대해 Reflection을 사용하기 때문에 성능이 조금 떨어지는 크리티컬한 단점이 있습니다.
package study.proxy;
import core.aop.pointcut.MethodMatcher;
import lombok.RequiredArgsConstructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
@RequiredArgsConstructor
public class UpperCaseHandler implements InvocationHandler {
private final Car car;
private final MethodMatcher methodMatcher;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
final String methodName = (String) method.invoke(car, args);
if (methodMatcher.matches(method)) {
return methodName.toUpperCase();
}
return methodName;
}
}
실제로 Invocation Handler를 상속받아서 구현한 예시인데요..
Car 라는 인터페이스를 상속받아서 의존성 주입을 해준 모습입니다.
여기서는 MethodMatcher라는 인터페이스도 같이 주입해준 모습인데요.
해당 인터페이스는 Method 를 선택적으로 Proxy화 하기 위해서, 의존성 주입으로 설정해 주었습니다.!
위 Invocation Handler는 invoke를 통해서 proxy 로직이 진행되는데요,
invoke 라는 메서드 내부 로직에 Reflection을 해야하는 아쉬움이 있습니다. ㅠㅠ
CGlib Proxy
CGlib Proxy는 Enhancer를 바탕으로 Proxy를 구현하는 방식입니다
이 방식은 JDK Dynamic Proxy와는 다르게 Reflection을 사용하지 않고,
Extends(상속) 방식을 이용해서 Proxy화 할 메서드를 오버라이딩 하는 방식입니다
한번 실제 코드를 구경하러 가볼까요.?
먼저 Proxy화를 진행할 Target Class를 생성하고,
package study.proxy;
public class CarTarget implements Car {
@Override
public String start(String name) {
return "Car " + name + " started!";
}
@Override
public String stop(String name) {
return "Car " + name + " stopped!";
}
}
간단하게 start와 stop 메서드를 통해 차가 멈추고 시작하는 것을 메서드로 나타냈습니다
그리고 저희는 start 메서드만 Proxy 화를 진행하고 싶은데요..
package study.proxy.matcher;
import core.aop.pointcut.MethodMatcher;
import java.lang.reflect.Method;
public class StartMethodMatcher implements MethodMatcher {
private static final String TALK_PREFIX = "start";
@Override
public boolean matches(Method method) {
final String methodName = method.getName();
return methodName.startsWith(TALK_PREFIX);
}
}
그래서 MethodMathcer라는 인터페이스를 선언하고 상속해서 특정 조건에 의해 필터링 하겠습니다!
그 다음은
실제 Proxy로 핸들링할 Handler가 필요한데, CGlib 에서는 이를 MethodInterceptor 라는 인터페이스로 정의되어 있습니다
package study.proxy;
import core.aop.pointcut.MethodMatcher;
import lombok.RequiredArgsConstructor;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
@RequiredArgsConstructor
public class UpperCaseInterceptor implements MethodInterceptor {
private final MethodMatcher methodMatcher;
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
final String methodName = (String) proxy.invokeSuper(obj, args);
if (methodMatcher.matches(method)) {
return methodName.toUpperCase();
}
return methodName;
}
}
Intercept 라는 메서드를 오버라이딩 해서, Proxy 로직을 진행하게 됩니다
실제로 테스트해보면은..?
@Test
@DisplayName("cglib Proxy 테스트")
void cglibProxyTest() {
/* given */
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(CarTarget.class);
enhancer.setCallback(new UpperCaseInterceptor(new StartMethodMatcher()));
/* when */
final Car proxiedCar = (Car) enhancer.create();
/* then */
assertThat(proxiedCar.start("huisam")).isEqualTo("CAR HUISAM STARTED!");
assertThat(proxiedCar.stop("huisam")).isEqualTo("Car huisam stopped!");
}
정상적으로 Proxied 된 객체로 실행되는 것을 볼 수 있습니다
여기서는 start 라는 메서드만 proxy화 하게 설정하였고, 이를 테스트하는 코드입니다
코드에서도 볼 수 있듯이 Enhancer 객체는 반드시 SuperClass(부모클래스)를 지정해야 합니다
그 다음 CallBack 을 통해서 어떠한 Handler 를 설정할 것인지 바로 명시해야 하죠
CGlib은 기본적으로 Byte 코드를 조작해서, 바이너리가 만들어지기 때문에 JDK Dynamic Proxy보다 성능적으로 더 우세합니다
다만, final 객체 혹은 private 접근자로 된 메서드는 상속(Override)가 지원되지 않기 때문에
제약적인 Proxy 구현이 가능하다는 것을 알 수 있습니다
그래서 AOP랑 무슨 관계인데?
여기까지만 보면 되게 평범한 코드 같지만,
사실 위에서 서술한 코드가 AOP의 근본이 되는 코드입니다
위 코드에서 예제로 이용한 코드를 Spring AOP에 접목시켜보면
JDK Dynamic Proxy에서 InvocationHandler, CGlib 에서 MethodInterceptor 는
Spring AOP에서 JoinPoint 라는 개념과 일치합니다
그리고, 위에서 특정 조건에 의해 필터링 하는 MethodMatcher 는
Spring AOP에서 PointCut 라는 개념과 일치합니다
마지막으로 Proxy 로직이 실행되는 JDK Dynamic Proxy에 invoke 메서드, CGlib 에서 Intercept 메서드는
Spring AOP에서 Advice 라는 개념과 일치합니다
아하,
우리는 이제야 깨달았습니다.
그렇게나 어려운 개념들이 사실 알고보면 그렇게 어려운건 아니었습니다 ㅎㅎ
물론 실제로 Spring AOP를 활용한다고, 하나부터 열까지 다 직접 구현하지는 않습니다
실제 현업에서는 @AspectJ(Class, Method 단위) 어노테이션과,
어떻게 Advice를 지정할 것인가에 대한 @Before, @Around, @AfterThrowing ...
특정 조건을 필터링할 Expression 을 기반으로 PointCut 설정
들을 편하게 하는 것이 이미 만들어져있으니까요 ^-^
하지만
알고 쓰는 것과
모르고 쓰는 것은
하늘과 땅 차이라고 생각합니다 ㅎㅎ
정리
- Spring AOP는 Proxy를 기반으로 한 Runtime Weaving 방식이다
- Spring AOP에서는 JDK Dynamic Proxy 와 CGlib 을 통해 Proxy화 한다
- JDK Dynamic Proxy는 Reflection을 기반으로 이루어지고
- CGlib 은 상속을 기반으로 이루어진다
제 게시글이 Spring AOP를 공부하는데에 많이 도움이 되었으면 좋겠네요 ^-^
참고
Aspect weaving at Runtime
'Developer > Spring' 카테고리의 다른 글
Spring DI(Dependency Injection) & IoC Container - 의존성 주입이란? (0) | 2020.10.18 |
---|---|
Spring Boot 에서 Log4j2 를 설정하는 방법 (0) | 2020.09.28 |
Spring에서 API 문서를 자동화하는 방법 - Spring REST Docs (0) | 2020.06.22 |
Spring Boot 에서 log를 남기는 방법 - Spring log 남기기 (4) | 2020.06.13 |
Spring Valid - 스프링에서 Reqeust Body를 Validation 하는 방법 (0) | 2020.05.08 |