스프링
스프링이 지원하는 프록시
이전문제점
- 인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB를 적용하려면 어떻게 해야할까?
- 두 기술을 함께 사용할 때 부가 기능을 제공하기 위해 JDK 동적 프록시가 제공하는 InvocationHandler 와 CGLIB가 제공하는 MethodInterceptor 를 각각 중복으로 만들어서 관리해야 할까?
- 특정 조건에 맞을 때 프록시 로직을 적용하는 기능도 공통으로 제공되었으면?
Q: 인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB를 적용하려면 어떻게 해야할까?
스프링은 유사한 구체적인 기술들이 있을 때, 그것들을 통합해서 일관성 있게 접근할 수 있고, 더욱 편리하게 사용할 수 있는 추상화된 기술을 제공한다.
스프링은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리( ProxyFactory )라는 기능을 제공한다.
이전에는 상황에 따라서 JDK 동적 프록시를 사용하거나 CGLIB를 사용해야 했다면, 이제는 이 프록시 팩토리 하나로 편리하게 동적 프록시를 생성할 수 있다.
프록시 팩토리는 인터페이스가 있으면 JDK 동적 프록시를 사용하고, 구체 클래스만 있다면 CGLIB를 사용한다. 그리고 이 설정을 변경할 수도 있다.

Q: 두 기술을 함께 사용할 때 부가 기능을 적용하기 위해 JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB가 제공하는 MethodInterceptor를 각각 중복으로 따로 만들어야 할까?
스프링은 이 문제를 해결하기 위해 부가 기능을 적용할 때 Advice 라는 새로운 개념을 도입했다.
개발자는 InvocationHandler 나 MethodInterceptor 를 신경쓰지 않고, Advice 만 만들면 된다. 결과적으로 InvocationHandler 나 MethodInterceptor 는 Advice 를 호출하게 된다.
프록시 팩토리를 사용하면 Advice 를 호출하는 전용 InvocationHandler , MethodInterceptor 를 내부에서 사용한다.

Q: 특정 조건에 맞을 때 프록시 로직을 적용하는 기능도 공통으로 제공되었으면?
앞서 특정 메서드 이름의 조건에 맞을 때만 프록시 부가 기능이 적용되는 코드를 직접 만들었다.(Filter)
스프링은 Pointcut 이라는 개념을 도입해서 이 문제를 일관성 있게 해결한다.
Advice 만들기
Advice 는 프록시에 적용하는 부가 기능 로직이다. 이것은 JDK 동적 프록시가 제공하는 InvocationHandler 와 CGLIB가 제공하는 MethodInterceptor 의 개념과 유사한다.
둘을 개념적으로 추상화 한 것이다. 프록시 팩토리를 사용하면 둘 대신에 Advice 를 사용하면 된다.

CGLIB만을 사용하는 경우

프록시 팩토리의 기술 선택 방법
- 대상에 인터페이스가 있으면: JDK 동적 프록시, 인터페이스 기반 프록시
- 대상에 인터페이스가 없으면: CGLIB, 구체 클래스 기반 프록시
- proxyTargetClass=true : CGLIB, 구체 클래스 기반 프록시, 인터페이스 여부와 상관없음
정리
- 프록시 팩토리의 서비스 추상화 덕분에 구체적인 CGLIB, JDK 동적 프록시 기술에 의존하지 않고, 매우 편리하게 동적 프록시를 생성할 수 있다.
- 프록시의 부가 기능 로직도 특정 기술에 종속적이지 않게 Advice 하나로 편리하게 사용할 수 있었다. 이것은 프록시 팩토리가 내부에서 JDK 동적 프록시인 경우 InvocationHandler 가 Advice 를 호출하도록 개발해두고, CGLIB인 경우 MethodInterceptor 가 Advice 를 호출하도록 기능을 개발해두었기 때문이다.
- 즉, 같은 기능일 경우 인터페이스라면 각각 만들어줬어야 하지만, 이제는 상관없다.
참고
💡 스프링 부트는 AOP를 적용할 때 기본적으로 proxyTargetClass=true 로 설정해서 사용한다. 따라서 인터페이스가 있어도 항상 CGLIB를 사용해서 구체 클래스를 기반으로 프록시를 생성한다. 자세한 이유는 강의 뒷 부분에서 설명한다.

쉽게 기억하기
- 조언( Advice )을 어디( Pointcut )에 할 것인가?
- 조언자( Advisor )는 어디( Pointcut )에 조언( Advice )을 해야할지 알고 있다.
역할과 책임 이렇게 구분한 것은 역할과 책임을 명확하게 분리한 것이다.
- 포인트컷은 대상 여부를 확인하는 필터 역할만 담당한다.
- 어드바이스는 깔끔하게 부가 기능 로직만 담당한다.
- 둘을 합치면 어드바이저가 된다. 스프링의 어드바이저는 하나의 포인트컷 + 하나의 어드바이스로 구성된다.

Advisor 적용
@Slf4j
public class AdvisorTest {
@Test
void advisorTest1() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
}
- new DefaultPointcutAdvisor : Advisor 인터페이스의 가장 일반적인 구현체이다. 생성자를 통해 하나의 포인트컷과 하나의 어드바이스를 넣어주면 된다. 어드바이저는 하나의 포인트컷과 하나의 어드바이스로 구성된다.
- Pointcut.TRUE : 항상 true 를 반환하는 포인트컷이다. 이후에 직접 포인트컷을 구현해볼 것이다.
- new TimeAdvice() : 앞서 개발한 TimeAdvice 어드바이스를 제공한다.
- proxyFactory.addAdvisor(advisor) : 프록시 팩토리에 적용할 어드바이저를 지정한다. 어드바이저는 내부에 포인트컷과 어드바이스를 모두 가지고 있다. 따라서 어디에 어떤 부가 기능을 적용해야 할지 어드바이저 하나로 알 수 있다. 프록시 팩토리를 사용할 때 어드바이저는 필수이다.
- 그런데 생각해보면 이전에 분명히 proxyFactory.addAdvice(new TimeAdvice()) 이렇게 어드바이저가 아니라 어드바이스를 바로 적용했다. 이것은 단순히 편의 메서드이고 결과적으로 해당 메서드 내부에서 지금 코드와 똑같은 다음 어드바이저가 생성된다. DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice())
- 내부 클래스에 들어가보면 addAdvice() 안에 Pointcut.TRUE가 있음

직접 만든 포인트컷
static class MyPointcut implements Pointcut {
@Override
public ClassFilter getClassFilter() {
return ClassFilter.TRUE;
}
@Override
public MethodMatcher getMethodMatcher() {
return new MyMethodMatcher();
}
}
static class MyMethodMatcher implements MethodMatcher {
private String matchName = "save";
@Override
public boolean matches(Method method, Class<?> targetClass) {
boolean result = method.getName().equals(matchName);
log.info("포인트컷 호출 method={} targetClass={}", method.getName(), targetClass);
log.info("포인트컷 결과 result={}", result);
return result;
}
@Override
public boolean isRuntime() {
return false;
}
@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
return false;
}
}
MyPointcut
- 직접 구현한 포인트컷이다. Pointcut 인터페이스를 구현한다.
- 현재 메서드 기준으로 로직을 적용하면 된다. 클래스 필터는 항상 true 를 반환하도록 했고, 메서드 비교 기능은 MyMethodMatcher를 사용한다.
MyMethodMatcher
- 직접 구현한 MethodMatcher 이다. MethodMatcher 인터페이스를 구현한다.
- matches() : 이 메서드에 method , targetClass 정보가 넘어온다. 이 정보로 어드바이스를 적용할지 적용하지 않을지 판단할 수 있다.
- 여기서는 메서드 이름이 "save" 인 경우에 true 를 반환하도록 판단 로직을 적용했다.
- isRuntime() , matches(... args) : isRuntime() 이 값이 참이면 matches(... args) 메서드가 대신 호출된다. 동적으로 넘어오는 매개변수를 판단 로직으로 사용할 수 있다. isRuntime() 이 false 인 경우 클래스의 정적 정보만 사용하기 때문에 스프링이 내부에서 캐싱을 통해 성능 향상이 가능하지만, isRuntime() 이 true 인 경우 매개변수가 동적으로 변경된다고 가정하기 때 문에 캐싱을 하지 않는다. 크게 중요한 부분은 아니니 참고만 하고 넘어가자
new DefaultPointcutAdvisor(new MyPointcut(), new TimeAdvice())
- 어드바이저에 직접 구현한 포인트컷을 사용한다.

스프링이 제공하는 포인트컷
스프링은 무수히 많은 포인트컷을 제공한다. 대표적인 몇가지만 알아보자.
- NameMatchMethodPointcut : 메서드 이름을 기반으로 매칭한다. 내부에서는 PatternMatchUtils 를 사용한다.
- 예) *xxx* 허용
- JdkRegexpMethodPointcut : JDK 정규 표현식을 기반으로 포인트컷을 매칭한다.
- TruePointcut : 항상 참을 반환한다.
- AnnotationMatchingPointcut : 애노테이션으로 매칭한다.
- AspectJExpressionPointcut : aspectJ 표현식으로 매칭한다.(실무에서 사용)
여러 어드바이저 적용
@Test
@DisplayName("여러 프록시")
void multiAdvisorTest1() {
//client -> proxy2(advisor2) -> proxy1(advisor1) -> target
//프록시1 생성
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory1 = new ProxyFactory(target);
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
proxyFactory1.addAdvisor(advisor1);
ServiceInterface proxy1 = (ServiceInterface) proxyFactory1.getProxy();
//프록시2 생성, target -> proxy1 입력
ProxyFactory proxyFactory2 = new ProxyFactory(proxy1);
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
proxyFactory2.addAdvisor(advisor2);
ServiceInterface proxy2 = (ServiceInterface) proxyFactory2.getProxy();
//실행
proxy2.save();
}
여기서 주의할점은 proxyFactory2는 proxy1을 타겟으로 삼는다.
여러 프록시의 문제
위 방법이 잘못된 것은 아니지만, 프록시를 2번 생성해야 한다는 문제가 있다. 만약 적용해야 하는 어드바이저가 10개라면 10개의 프록시를 생성해야한다.
따라서, 스프링은 하나의 프록시에 여러개의 어드바이저를 넣는 것을 지원한다.
@Test
@DisplayName("하나의 프록시, 여러 어드바이저")
void multiAdvisorTest2() {
//client -> proxy -> advisor2 -> advisor1 -> target
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
//프록시1 생성
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory1 = new ProxyFactory(target);
proxyFactory1.addAdvisor(advisor2);
proxyFactory1.addAdvisor(advisor1);
ServiceInterface proxy = (ServiceInterface) proxyFactory1.getProxy();
//실행
proxy.save();
}
addAdvisor를 추가한 순서대로 실행된다.
💡 중요
사실 이번 장을 이렇게 풀어서 설명한 이유가 있다. 스프링의 AOP를 처음 공부하거나 사용하면, AOP 적용 수 만큼 프록시가 생성된다고 착각하게 된다. 실제 많은 실무 개발자들도 이렇게 생각하는 것을 보았다.
스프링은 AOP를 적용할 때, 최적화를 진행해서 지금처럼 프록시는 하나만 만들고, 하나의 프록시에 여러 어드바이저를 적용한다.
정리하면 하나의 target 에 여러 AOP가 동시에 적용되어도, 스프링의 AOP는 target 마다 하나의 프록시만 생성한다. 이부분을 꼭 기억해두자.
즉, 여러 AOP를 적용되어도 하나의 프록시만 생성된다.
실제 적용
LogTraceAdvice
public class LogTraceAdvice implements MethodInterceptor {
private final LogTrace logTrace;
public LogTraceAdvice(LogTrace logTrace) {
this.logTrace = logTrace;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
TraceStatus status = null;
try {
Method method = invocation.getMethod();
String message = method.getDeclaringClass().getSimpleName() + "." +
method.getName() + "()";
status = logTrace.begin(message);
//로직 호출
Object result = invocation.proceed();
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
ProxyFactoryConfigV1(인터페이스 버전)
@Slf4j
@Configuration
public class ProxyFactoryConfigV1 {
@Bean
public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
ProxyFactory factory = new ProxyFactory(orderController);
factory.addAdvisor(getAdvisor(logTrace));
OrderControllerV1 proxy = (OrderControllerV1) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderController.getClass());
return proxy;
}
@Bean
public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
ProxyFactory factory = new ProxyFactory(orderService);
factory.addAdvisor(getAdvisor(logTrace));
OrderServiceV1 proxy = (OrderServiceV1) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderService.getClass());
return proxy;
}
@Bean
public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
OrderRepositoryV1Impl orderRepository = new OrderRepositoryV1Impl();
ProxyFactory factory = new ProxyFactory(orderRepository);
factory.addAdvisor(getAdvisor(logTrace));
OrderRepositoryV1 proxy = (OrderRepositoryV1) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderRepository.getClass());
return proxy;
}
private Advisor getAdvisor(LogTrace logTrace) {
//pointcut
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
//advice
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
ProxyFactoryConfigV2(클래스 상속 버전)
@Slf4j
@Configuration
public class ProxyFactoryConfigV2 {
@Bean
public OrderControllerV2 orderControllerV2(LogTrace logTrace) {
OrderControllerV2 orderController = new OrderControllerV2(orderServiceV2(logTrace));
ProxyFactory factory = new ProxyFactory(orderController);
factory.addAdvisor(getAdvisor(logTrace));
OrderControllerV2 proxy = (OrderControllerV2) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderController.getClass());
return proxy;
}
@Bean
public OrderServiceV2 orderServiceV2(LogTrace logTrace) {
OrderServiceV2 orderService = new OrderServiceV2(orderRepositoryV2(logTrace));
ProxyFactory factory = new ProxyFactory(orderService);
factory.addAdvisor(getAdvisor(logTrace));
OrderServiceV2 proxy = (OrderServiceV2) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderService.getClass());
return proxy;
}
@Bean
public OrderRepositoryV2 orderRepositoryV2(LogTrace logTrace) {
OrderRepositoryV2 orderRepository = new OrderRepositoryV2();
ProxyFactory factory = new ProxyFactory(orderRepository);
factory.addAdvisor(getAdvisor(logTrace));
OrderRepositoryV2 proxy = (OrderRepositoryV2) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderRepository.getClass());
return proxy;
}
private Advisor getAdvisor(LogTrace logTrace) {
//pointcut
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
//advice
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
정리
프록시 팩토리 덕분에 개발자는 매우 편리하게 프록시를 생성할 수 있게 되었다. 추가로 어드바이저, 어드바이스, 포인트컷 이라는 개념 덕분에 어떤 부가 기능을 어디에 적용할 지 명확하게 이해할 수 있었다.
남은 문제
프록시 팩토리와 어드바이저 같은 개념 덕분에 지금까지 고민했던 문제들은 해결되었다. 프록시도 깔끔하게 적용하고 포인트컷으로 어디에 부가 기능을 적용할지도 명확하게 정의할 수 있다. 원본 코드를 전혀 손대지 않고 프록시를 통해 부가 기능도 적용할 수 있었다. 그런데 아직 해결되지 않는 문제가 있다.
문제1 - 너무 많은 설정
바로 ProxyFactoryConfigV1 , ProxyFactoryConfigV2 와 같은 설정 파일이 지나치게 많다는 점이다. 예를 들어서 애플리케이션에 스프링 빈이 100개가 있다면 여기에 프록시를 통해 부가 기능을 적용하려면 100개의 동적 프록시 생성 코드를 만들어야 한다! 무수히 많은 설정 파일 때문에 설정 지옥을 경험하게 될 것이다. 최근에는 스프링 빈을 등록하기 귀찮아서 컴포넌트 스캔까지 사용하는데, 이렇게 직접 등록하는 것도 모자라서, 프록시를 적용하는 코드까지 빈 생성 코드에 넣어야 한다.
문제2 - 컴포넌트 스캔
애플리케이션 V3처럼 컴포넌트 스캔을 사용하는 경우 지금까지 학습한 방법으로는 프록시 적용이 불가능하다. 왜냐하면 실제 객체를 컴포넌트 스캔으로 스프링 컨테이너에 스프링 빈으로 등록을 다 해버린 상태이기 때문이다. 지금까지 학습한 프록시를 적용하려면, 실제 객체를 스프링 컨테이너에 빈으로 등록하는 것이 아니라
ProxyFactoryConfigV1 에서 한 것 처럼, 부가 기능이 있는 프록시를 실제 객체 대신 스프링 컨테이너에 빈으로 등록해야 한다.
두 가지 문제를 한번에 해결하는 방법이 바로 다음에 설명할 빈 후처리기이다.
Jenkins
'개발 기록' 카테고리의 다른 글
2024-08-04 학습일기 (0) | 2024.08.04 |
---|---|
2024-08-03 학습일기 (0) | 2024.08.03 |
2024-08-01 학습일기 (0) | 2024.08.01 |
2024-07-31 학습일기 (0) | 2024.07.31 |
2024-07-30 학습일기 (0) | 2024.07.30 |