-
[Spring] 스프링 핵심 원리 - 스프링이 지원하는 프록시Study/Spring 2025. 4. 20. 22:44
앞서 살펴본 동적 프록시를 사용할 때 문제점
- 인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB를 적용하려면 어떻게 해야할까?
- 두 기술을 함께 사용할 때 부가 기능을 제공하기 위해 JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB가 제공하는 MethodInterceptor를 각각 중복으로 만들어서 관리해야 할까?
- 특정 조건에 부합하는 경우에만 프록시 로직을 적용하려면 어떻게 해야 할까?
미리 요약하자면,
- 스프링 프록시 팩토리의 서비스 추상화 덕분에 구체적인 CGLIB, JDK 동적 프록시 기술에 의존하지 않고, 편리하게 동적 프록시를 생성할 수 있음
- 프록시 팩토리의 프록시 기술 선택 기준
- 대상에 인터페이스가 있으면 : JDK 동적 프록시 (인터페이스 기반 프록시)
- 대상에 인터페이스가 없으면 : CGLIB (구체 클래스 기반 프록시)
- proxyTargetClass=true : 인터페이스 여부와 상관없이 무조건 CGLIB 사용
- 스프링부트는 AOP를 적용할 때 기본적으로 proxyTargetClass=true로 설정해서 사용함
- 프록시 팩토리의 프록시 기술 선택 기준
- 프록시의 부가 기능 로직도 특정 기술에 종속적이지 않게 Advice 하나로 편리하게 사용할 수 있음
- 프록시 팩토리가 내부에서 JDK 동적 프록시인 경우 InvocationHandler가 Advice를 호출하도록 개발해두고, CGLIB인 경우 MethodInterceptor가 Advice를 호출하도록 기능을 개발해두었기 때문임
Q. 인터페이스 존재 여부에 따라 JDK 동적 프록시 or CGLIB를 적용하려면 어떻게 해야할까?
- 스프링은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리(ProxyFactory) 기능을 제공함
- 프록시 팩토리는 인터페이스가 있으면 JDK 동적 프록시를 사용하고, 구체 클래스만 있다면 CGLIB를 사용함
- 해당 설정은 변경 가능 ex) 무조건 CGLIB만 사용하도록
Q. 두 기술을 함께 사용할 때 부가 기능을 적용하기 위해 InvocationHandler와 MethodInterceptor를 모두 구현해야 할까?
- 스프링은 부가 기능을 적용할 때 Advice라는 새로운 개념을 도입함
- Advice만 만들면 결과적으로 InvocationHandler나 MethodInterceptor는 Advice를 호출하게 됨
- ProxyFactory를 사용하면 Advice를 호출하는 전용 InvocationHandler와 MethodInterceptor를 내부에서 사용함
Q. 특정 조건에 부합할 때만 프록시 로직을 적용하려면?
- 스프링은 Pointcut이라는 개념을 도입함
예제)
더보기Advice 만들기
- Advice는 프록시에 적용하는 부가 기능 로직
- Advice를 만드는 방법은 여러가지가 있지만, 기본적인 방법은 다음 인터페이스를 구현하는 것임
package org.aopalliance.intercept; @FunctionalInterface public interface MethodInterceptor extends Interceptor { @Nullable Object invoke(@Nonnull MethodInvocation invocation) throws Throwable; }
- MethodInterceptor는 Interceptor를 상속하고, Interceptor는 Advice 인터페이스를 상속함
Advice 구현 클래스 (부가 기능 정의)
import lombok.extern.slf4j.Slf4j; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @Slf4j public class TimeAdvice implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { log.info("TimeProxy 실행"); long start = System.currentTimeMillis(); Object result = invocation.proceed(); long end = System.currentTimeMillis(); long duration = end - start; log.info("TimeProxy 종료 duration={}", duration); return result; } }
- target 클래스의 정보는 MethodInvocation invocation에 포함되어 있음
테스트
import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.support.AopUtils; import static org.assertj.core.api.Assertions.assertThat; @Slf4j class ProxyFactoryTest { @Test @DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용") void interfaceProxy() { ServiceInterface target = new ServiceImpl(); ProxyFactory proxyFactory = new ProxyFactory(target); proxyFactory.addAdvice(new TimeAdvice()); // 프록시가 사용할 부가 기능 로직 설정 ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy(); log.info("targetClass={}", target.getClass()); log.info("proxyClass={}", proxy.getClass()); proxy.save(); assertThat(AopUtils.isAopProxy(proxy)).isTrue(); assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue(); assertThat(AopUtils.isCglibProxy(proxy)).isFalse(); } @Test @DisplayName("구체 클래스만 있으면 CGLIB 사용") void concreteProxy() { ConcreteService target = new ConcreteService(); ProxyFactory proxyFactory = new ProxyFactory(target); proxyFactory.addAdvice(new TimeAdvice()); ConcreteService proxy = (ConcreteService) proxyFactory.getProxy(); log.info("targetClass={}", target.getClass()); log.info("proxyClass={}", proxy.getClass()); proxy.call(); assertThat(AopUtils.isAopProxy(proxy)).isTrue(); assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse(); assertThat(AopUtils.isCglibProxy(proxy)).isTrue(); } }
- new ProxyFactory(target)
- 프록시 팩토리 생성할 때, 생성자에 프록시의 호출 대상 전달함
- 이 인스턴스 정보를 기반으로 프록시를 생성함
실행 결과
- 타깃 인터페이스가 존재하므로 JDK 동적 프록시가 적용됨
- 인터페이스 없이 구체 클래스만 존재하므로 CGLIB 사용한 프록시가 적용됨
포인트컷, 어드바이스, 어드바이저
- 포인트컷(Pointcut) --> 어디에 적용할 것인가?
- 부가 기능 적용 여부를 판단하는 필터링 로직 (주로 클래스와 메서드 이름으로 필터링)
- 이름 그대로 어떤 포인트(Point)에 기능을 적용할지 적용하지 않을지를 잘라서(Cut) 구분하는 것
- 어드바이스(Advice) --> 무엇을 적용할 것인가?
- 프록시가 호출하는 부가 기능
- 어드바이저(Advisor)
- 하나의 포인트컷과 어드바이스를 가지고 있는 것
참고
'Study > Spring' 카테고리의 다른 글
[Spring] 스프링 핵심 원리 - @Aspect AOP (0) 2025.04.22 [Spring] 스프링 핵심 원리 - 빈 후처리기 (0) 2025.04.21 [Spring] 스프링 핵심 원리 - 동적 프록시 (0) 2025.04.20 [Spring] 스프링 핵심 원리 - ThreadLocal (1) 2025.04.19 [Spring] Spring MVC - 필터, 인터셉터 (0) 2025.04.19