이 글에서 다루는 것
"Spring 소스 파헤치기" 시리즈의 열한 번째 글입니다. 10편에서는 ProxyFactory에 대상 객체와 어드바이스를 넣으면 JDK 방식이나 CGLIB 방식으로 프록시가 만들어지는 과정을 봤습니다. 그런데 실제로 Spring을 쓸 때 ProxyFactory를 직접 부르는 일은 거의 없습니다. @EnableAspectJAutoProxy를 켜고 @Aspect 클래스를 빈으로 두면 컨테이너가 알아서 프록시를 만들어 끼워 줍니다. 이번에는 그 "알아서"의 속을 따라갑니다.
답하려는 질문은 세 가지입니다.
@EnableAspectJAutoProxy를 켜면@Aspect클래스는 어떻게 발견되어 어드바이저가 되는가- 컨테이너는 어떤 빈을 프록시로 감쌀지 어떻게 정하는가
- 프록시의 메서드를 부르면
@Around,@Before,@After,@AfterReturning,@AfterThrowing은 어떤 순서로 실행되는가
| 항목 | 내용 |
|---|---|
| 기준 소스 | Spring Framework 7.1.0-SNAPSHOT |
| 커밋 | f447f3c310 (2026-10-01, main 브랜치) |
| 주로 읽는 모듈 | spring-aop (일부 spring-context) |
| 주로 읽는 클래스 | AbstractAutoProxyCreator, AbstractAdvisorAutoProxyCreator, AnnotationAwareAspectJAutoProxyCreator, BeanFactoryAspectJAdvisorsBuilder, ReflectiveAspectJAdvisorFactory, DefaultAdvisorChainFactory, ReflectiveMethodInvocation |
글에 나오는 줄 번호와 세부 동작은 모두 이 커밋 기준입니다. 다른 버전에서는 줄 번호가 다르고 세부 내용도 달라질 수 있습니다. 같은 커밋의 소스를 받는 방법은 1편에 있습니다.
원래 공부 노트는 줄 단위로 훨씬 깊이 들어가지만, 이 글은 Spring을 처음 보는 분이 따라올 수 있는 큰 줄기만 골랐습니다. 빼놓은 것은 글 끝의 "더 깊이 들어가면"에 이름만 적어 두었습니다.
먼저 알아 둘 말
프록시(proxy)는 원래 객체를 감싸서 대신 나서는 객체이고, AOP는 여러 클래스에 공통으로 필요한 동작을 프록시로 끼워 넣는 기법입니다. 프록시를 만드는 두 가지 방법(JDK 방식, CGLIB 방식)과 ProxyFactory는 10편에 있습니다.
어스펙트(aspect)는 "끼워 넣을 동작"과 "어디에 끼울지"를 함께 적어 둔 클래스입니다. Spring에서는 클래스에 @Aspect를 붙여 표시합니다. 그 안의 메서드 하나하나가 어드바이스(advice), 곧 실제로 끼워 넣을 동작이고, @Before, @After, @Around 같은 어노테이션이 "원래 메서드의 앞에서, 뒤에서, 앞뒤 모두에서"를 정합니다. "어디에 끼울지"는 포인트컷(pointcut)이라고 부르며, execution(* com.example.Service.*(..)) 같은 문자열 표현식으로 적습니다.
어드바이저(advisor)는 Spring AOP 내부에서 쓰는 낱말로, 포인트컷 하나와 어드바이스 하나를 묶은 것입니다. Advisor 인터페이스는 어드바이스를 돌려주는 getAdvice()를 가지고(Advisor 36–55행), 포인트컷까지 가진 쪽이 PointcutAdvisor입니다(26–31행). @Aspect 클래스에 어드바이스 메서드가 다섯 개 있으면 어드바이저도 다섯 개가 됩니다.
인터셉터(interceptor)와 어드바이스 체인은 프록시가 실제로 실행하는 단위입니다. MethodInterceptor는 invoke(MethodInvocation) 메서드 하나를 가진 인터페이스로(45–57행), 자기 일을 한 뒤 invocation.proceed()로 다음 인터셉터에게 넘깁니다. 이렇게 한 줄로 늘어선 인터셉터들이 어드바이스 체인이고, 맨 끝에서 원래 메서드가 불립니다.
빈 후처리기(BeanPostProcessor)는 빈이 만들어질 때 끼어드는 훅입니다. 4편에서 "초기화 후" 훅이 다른 객체를 돌려주면 그것이 빈이 된다고 했습니다. 자동 프록시가 들어오는 길이 바로 그 훅이고, 후처리기의 종류와 순서는 6편에 있습니다.
자동 프록시 생성기(auto-proxy creator)는 이 글의 주인공입니다. "빈 하나하나가 만들어질 때마다 어드바이저들을 들고 가서 맞는 것이 있는지 보고, 있으면 프록시로 감싸는" 빈 후처리기입니다.
1. @EnableAspectJAutoProxy 한 줄이 하는 일
@EnableAspectJAutoProxy를 열어 보면 속성 둘(proxyTargetClass, exposeProxy) 말고는 @Import(AspectJAutoProxyRegistrar.class) 하나가 붙어 있을 뿐입니다(119–123행). 9편에서 본 대로 @Import에 ImportBeanDefinitionRegistrar를 주면 그 클래스가 빈 정의를 직접 등록할 수 있습니다. AspectJAutoProxyRegistrar가 하는 일은 사실상 한 줄입니다.
@Override
public void registerBeanDefinitions(
AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
// ...
}
줄인 자리에는 어노테이션의 proxyTargetClass, exposeProxy 속성을 읽어 반영하는 코드가 있습니다. AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary는 org.springframework.aop.config.internalAutoProxyCreator라는 긴 이름으로(54–55행) AnnotationAwareAspectJAutoProxyCreator 클래스의 빈 정의 하나를 등록합니다(94–98행, 등록 코드는 136–141행). 그 빈 정의의 역할은 ROLE_INFRASTRUCTURE(사용자 빈이 아니라 Spring 내부용이라는 표시)이고, order 속성은 HIGHEST_PRECEDENCE입니다. 이 빈은 BeanPostProcessor이므로 7편의 refresh()가 후처리기 등록 단계에서 찾아 등록하고, 그 뒤 만들어지는 모든 빈의 "초기화 후" 훅에 끼어듭니다.
어노테이션 한 줄이 빈 정의 하나로 바뀌는 길. 회색 칸은 앞 글에서 본 부분입니다.
헷갈리기 쉬운 곳: 자동 프록시 생성기는 컨테이너에 하나뿐입니다
AopConfigUtils에는 자동 프록시 생성기 클래스 세 개가 우선순위 순서로 적혀 있습니다.
static {
// Set up the escalation list...
APC_PRIORITY_LIST.add(InfrastructureAdvisorAutoProxyCreator.class);
APC_PRIORITY_LIST.add(AspectJAwareAdvisorAutoProxyCreator.class);
APC_PRIORITY_LIST.add(AnnotationAwareAspectJAutoProxyCreator.class);
}
spring-aop/src/main/java/org/springframework/aop/config/AopConfigUtils.java 62–67행
같은 이름의 빈 정의가 이미 있으면 새로 등록하지 않고, 지금 요청된 클래스의 우선순위가 더 높을 때만 빈 정의의 클래스 이름을 바꿔 승격합니다(124–134행). 예를 들어 @EnableTransactionManagement가 먼저 AutoProxyRegistrar를 통해 InfrastructureAdvisorAutoProxyCreator를 등록해 두었더라도(AutoProxyRegistrar 73행, TransactionManagementConfigurationSelector 49행) @EnableAspectJAutoProxy가 오면 가장 능력이 많은 AnnotationAwareAspectJAutoProxyCreator로 바뀝니다. 그래서 컨테이너 하나에 자동 프록시 생성기는 늘 하나이고, 한 빈이 두 생성기에게 두 번 감싸이는 일은 없습니다.
또 하나, @EnableAspectJAutoProxy의 설명 주석에는 이 기능이 클래스패스에 aspectjweaver가 있어야 한다고 적혀 있습니다(110–112행). @Aspect, @Before 같은 어노테이션 자체가 AspectJ 라이브러리의 것이고, 포인트컷 표현식을 해석하는 것도 AspectJ이기 때문입니다.
2. 자동 프록시 생성기는 후처리기입니다
AnnotationAwareAspectJAutoProxyCreator는 ProxyProcessorSupport에서 시작해 다섯 클래스가 이어지는 상속 계층의 맨 아래에 있습니다. 각 층이 맡은 일이 다르므로 먼저 지도를 보겠습니다.
위에서 아래로 상속합니다. 초록 칸이 @EnableAspectJAutoProxy가 등록하는 클래스입니다.
| 클래스 | 맡은 일 | 위치 |
|---|---|---|
ProxyProcessorSupport |
프록시 설정값과 Ordered, 인터페이스가 있으면 JDK 방식으로 가는 판단 |
35행, 97–116행 |
AbstractAutoProxyCreator |
후처리기 구현. "감쌀까?" → "감싼다" | 96–97행 |
AbstractAdvisorAutoProxyCreator |
"이 빈에 맞는 어드바이저"를 찾는 네 단계 | 55행 |
AspectJAwareAdvisorAutoProxyCreator |
AspectJ 규칙으로 정렬, ExposeInvocationInterceptor 추가, 어스펙트 자신은 건너뜀 |
50–51행 |
AnnotationAwareAspectJAutoProxyCreator |
후보에 @Aspect 빈에서 만든 어드바이저를 더함 |
51행 |
시작점은 AbstractAutoProxyCreator의 "초기화 후" 훅입니다. 4편이 인용했듯 조기 참조로 이미 프록시를 내준 빈이 아니면 wrapIfNecessary를 부릅니다(285–293행). 조기 참조 쪽(237–241행)도 결국 같은 wrapIfNecessary로 옵니다. 그 본문입니다.
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
// Create proxy if we have advice.
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
노란색은 advisedBeans 캐시, 파란색은 절차, 초록색은 프록시가 빈이 되는 끝입니다.
위에서부터 읽으면 이렇습니다.
- 이미 판단한 빈이면 끝.
advisedBeans는 "이 빈을 감쌌나"를 기억하는 맵입니다(134행).FALSE로 적혀 있으면 어드바이저를 다시 찾지 않습니다. - 기반 클래스는 감싸지 않습니다.
isInfrastructureClass는 빈이Advice,Pointcut,Advisor,AopInfrastructureBean이면 참입니다(359–368행).AnnotationAwareAspectJAutoProxyCreator는 여기에 "@Aspect클래스이면"을 더합니다(98–110행). 어스펙트 자신이 프록시가 되면 어드바이스 호출이 프록시를 거치게 되어 문제가 생긴다고 주석에 적혀 있습니다. - 맞는 어드바이저를 찾습니다.
getAdvicesAndAdvisorsForBean은 이 클래스에서는 추상 메서드이고(611–612행), 다음 층이 구현합니다. 하나도 없으면DO_NOT_PROXY, 곧null을 돌려주고(103행) 원본이 그대로 빈이 됩니다. - 있으면 프록시를 만듭니다.
createProxy는buildProxy로 가서 10편의ProxyFactory를 만들고, 어드바이저를 넣고,getProxy를 부릅니다(440–494행). 대상 객체는SingletonTargetSource로 감싸 넣는데, 방금 초기화를 마친 그 원본 객체입니다.
JDK 방식이냐 CGLIB 방식이냐는 여기서 정해집니다
buildProxy 안에 이런 분기가 있습니다(451–466행). 빈 정의에 "클래스로 감싸라"는 표시가 있으면 proxyTargetClass를 켜고, 아니면 evaluateProxyInterfaces가 빈 클래스의 인터페이스를 살핍니다. 메서드가 하나라도 있는 "쓸 만한" 인터페이스가 있으면 그 인터페이스들을 프록시에 더하고, 없으면 proxyTargetClass를 켭니다(ProxyProcessorSupport 97–116행). 그다음은 10편에서 본 대로 ProxyFactory가 proxyTargetClass와 인터페이스를 보고 방식을 고릅니다.
실제로 확인해 봅니다. 인터페이스가 있는 빈, 없는 빈, 그리고 어드바이스가 하나도 맞지 않는 빈을 함께 둡니다. 이 예제는 릴리스 버전인 Spring Framework 7.0.9로 실행해 확인했습니다(이 시리즈가 읽는 7.1.0-SNAPSHOT을 직접 빌드해 돌린 것은 아닙니다).
// 이 글을 위해 쓴 예제입니다 (Spring의 소스가 아닙니다)
public interface PaymentService { void pay(); }
public static class PaymentServiceImpl implements PaymentService {
@Override public void pay() { }
}
public static class ReportService { // 인터페이스가 없다
public void print() { }
}
public static class PlainService { // 어드바이스가 하나도 맞지 않는다
public void nothing() { }
}
@Aspect
public static class TraceAspect {
@Before("execution(* ex.u10.ProxyTypeExample.PaymentService.*(..)) || " +
"execution(* ex.u10.ProxyTypeExample.ReportService.*(..))")
public void trace() { }
}
@EnableAspectJAutoProxy가 붙은 설정 클래스에 네 빈을 등록하고 각 빈의 클래스를 찍어 봤습니다.
PaymentService 빈의 클래스: jdk.proxy2.$Proxy16
JDK 프록시인가? true
ReportService 빈의 클래스: ex.u10.ProxyTypeExample$ReportService$$SpringCGLIB$$0
JDK 프록시인가? false
PlainService 빈의 클래스: ex.u10.ProxyTypeExample$PlainService
getBean(PaymentServiceImpl.class): NoSuchBeanDefinitionException
인터페이스가 있는 PaymentService는 JDK 프록시, 없는 ReportService는 CGLIB 프록시(클래스 이름에 $$SpringCGLIB$$)가 됐고, 어드바이스가 맞지 않는 PlainService는 원본 클래스 그대로입니다. 마지막 줄이 흔한 함정입니다. JDK 프록시는 인터페이스만 구현하지 PaymentServiceImpl을 상속하지 않으므로, 구현 클래스 타입으로 getBean을 부르면 빈을 찾지 못합니다.
헷갈리기 쉬운 곳
- 어드바이저가 하나도 맞지 않으면 프록시는 만들어지지 않습니다. 위 출력의
PlainService가 그 경우입니다. "@EnableAspectJAutoProxy를 켰으니 모든 빈이 프록시"가 아니라, 맞는 어드바이저가 있는 빈만 프록시입니다. - 감싸는 판단은 빈 하나마다, 캐시와 함께 이루어집니다.
advisedBeans의 열쇠는 빈 클래스와 빈 이름을 합친 것이라(305–312행) 같은 클래스의 빈이 둘이면 따로 판단합니다. - 후처리기는 "초기화 후" 훅만 쓰는 것이 아닙니다.
postProcessBeforeInstantiation(인스턴스화 전 훅)에서도 기반 클래스 여부를 미리advisedBeans에 적어 둡니다(244–255행). 이때shouldSkip이 불리는데, 다음 절에서 보듯 그것이 어드바이저 후보를 처음으로 모으는 계기가 됩니다.
3. @Aspect는 어떻게 어드바이저가 되나
2절의 3번, getAdvicesAndAdvisorsForBean은 AbstractAdvisorAutoProxyCreator가 구현하고(75–84행), 실제 일은 findEligibleAdvisors가 합니다. 네 단계가 한눈에 보입니다.
protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
List<Advisor> candidateAdvisors = findCandidateAdvisors();
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
extendAdvisors(eligibleAdvisors);
if (!eligibleAdvisors.isEmpty()) {
try {
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
}
// ...
}
return eligibleAdvisors;
}
- 후보 모으기(
findCandidateAdvisors) — 컨테이너에 있는 어드바이저를 전부 모읍니다. 이 절의 주제입니다. - 맞는 것 고르기(
findAdvisorsThatCanApply) — 그 가운데 지금 이 빈 클래스에 적용될 수 있는 것만 남깁니다. 4절에서 봅니다. - 덧붙이기(
extendAdvisors) — 필요한 보조 어드바이저를 더합니다. 기본 구현은 비어 있습니다(175–176행). - 정렬(
sortAdvisors) — 실행 순서를 정합니다. 기본 구현은@Order와Ordered를 보는AnnotationAwareOrderComparator입니다(161–164행).
후보는 두 군데서 옵니다
기본 findCandidateAdvisors는 BeanFactoryAdvisorRetrievalHelper에게 맡기는데, 이것은 컨테이너에서 Advisor 타입인 빈을 전부 찾아 getBean으로 가져옵니다(66–112행). 트랜잭션 기능처럼 어드바이저를 직접 빈으로 등록하는 쪽이 쓰는 길입니다. 그런데 @Aspect 클래스는 Advisor를 구현하지 않은 평범한 클래스입니다. 그래서 AnnotationAwareAspectJAutoProxyCreator가 이 메서드를 덮어써서, 부모가 모은 목록에 aspectJAdvisorsBuilder.buildAspectJAdvisors()의 결과를 더합니다(87–96행). 그 buildAspectJAdvisors가 @Aspect를 찾는 곳입니다. 처음 불렸을 때의 뼈대만 남기면 이렇습니다.
String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this.beanFactory, Object.class, true, false);
for (String beanName : beanNames) {
// ...
Class<?> beanType = this.beanFactory.getType(beanName, false);
// ...
if (this.advisorFactory.isAspect(beanType)) {
// ...
MetadataAwareAspectInstanceFactory factory =
new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
if (this.beanFactory.isSingleton(beanName)) {
this.advisorsCache.put(beanName, classAdvisors);
}
// ...
advisors.addAll(classAdvisors);
// ...
aspectNames.add(beanName);
// ...
}
}
this.aspectBeanNames = aspectNames;
return advisors;
Object.class 타입으로 빈 이름을 구하므로 컨테이너의 모든 빈 이름을 훑습니다. 빈을 만들지는 않고 getType으로 클래스만 알아낸 뒤 isAspect, 곧 클래스에 @Aspect가 붙어 있는지를 봅니다(AbstractAspectJAdvisorFactory 87–91행). 어스펙트이면 advisorFactory.getAdvisors로 어드바이저 목록을 만들고, 싱글톤이면 advisorsCache에 빈 이름을 열쇠로 넣어 둡니다. 어스펙트 빈 이름 목록(aspectBeanNames)도 기억해 두어, 두 번째부터는 훑지 않고 캐시에서 꺼냅니다(149–164행).
어스펙트 클래스 하나가 어드바이저 여러 개로 바뀌는 길. 노란 상자가 두 번째부터 쓰는 캐시입니다.
어스펙트 하나가 어드바이저 여러 개로
ReflectiveAspectJAdvisorFactory.getAdvisors는 어스펙트 클래스의 메서드를 훑어 어드바이스 메서드마다 어드바이저를 하나씩 만듭니다(122–166행). 어떤 메서드를 어떤 순서로 보는지는 getAdvisorMethods에 있습니다.
private List<Method> getAdvisorMethods(Class<?> aspectClass) {
List<Method> methods = new ArrayList<>();
ReflectionUtils.doWithMethods(aspectClass, methods::add, adviceMethodFilter);
if (methods.size() > 1) {
methods.sort(adviceMethodComparator);
}
return methods;
}
adviceMethodFilter는 @Pointcut이 붙은 메서드를 뺍니다(74–76행). 포인트컷 메서드는 표현식에 이름을 붙이는 용도일 뿐 끼워 넣을 동작이 없기 때문입니다. 그리고 adviceMethodComparator가 어노테이션 종류로 정렬합니다. 이 글의 핵심 순서가 여기서 나옵니다.
static {
// Note: although @After is ordered before @AfterReturning and @AfterThrowing,
// an @After advice method will actually be invoked after @AfterReturning and
// @AfterThrowing methods due to the fact that AspectJAfterAdvice.invoke(MethodInvocation)
// invokes proceed() in a `try` block and only invokes the @After advice method
// in a corresponding `finally` block.
Comparator<Method> adviceKindComparator = new ConvertingComparator<Method, @Nullable Annotation>(
new InstanceComparator<>(
Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),
method -> {
AspectJAnnotation ann = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);
return (ann != null ? ann.getAnnotation() : null);
});
Comparator<Method> methodNameComparator = new ConvertingComparator<>(Method::getName);
adviceMethodComparator = adviceKindComparator.thenComparing(methodNameComparator);
}
Around → Before → After → AfterReturning → AfterThrowing 순서이고, 같은 종류끼리는 메서드 이름 순입니다. 소스 파일에 적은 순서가 아닙니다. 주석에 그 이유가 있습니다. Java 7부터 JDK가 메서드를 선언 순서대로 돌려주지 않아서, 실행할 때마다 순서가 달라지지 않도록 종류와 이름으로 정렬하기로 한 것입니다(136–143행). 주석 첫 줄의 "@After가 @AfterReturning보다 앞에 정렬되지만 실제로는 뒤에 실행된다"는 말은 5절에서 출력으로 확인합니다.
메서드 하나는 getAdvisor에서 어드바이저가 됩니다(200–222행). 어노테이션의 pointcut 또는 value 속성에 적힌 문자열로(AbstractAspectJAdvisorFactory 161행, 201–209행) AspectJExpressionPointcut을 만들고(224–238행), 그 포인트컷과 메서드를 InstantiationModelAwarePointcutAdvisorImpl에 담습니다(213–214행). 이 긴 이름의 클래스가 "@Aspect에서 온 어드바이저"입니다. 싱글톤 어스펙트이면 생성자에서 곧바로 어드바이스 객체도 만듭니다(109–114행).
어드바이스 객체는 getAdvice가 어노테이션별로 다른 클래스를 골라 만듭니다.
switch (aspectJAnnotation.getAnnotationType()) {
case AtPointcut -> {
// ...
return null;
}
case AtAround -> springAdvice = new AspectJAroundAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
case AtBefore -> springAdvice = new AspectJMethodBeforeAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
case AtAfter -> springAdvice = new AspectJAfterAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
case AtAfterReturning -> {
springAdvice = new AspectJAfterReturningAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
// ...
}
case AtAfterThrowing -> {
springAdvice = new AspectJAfterThrowingAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
// ...
}
// ...
}
다섯 클래스는 모두 AbstractAspectJAdvice를 상속하지만 구현하는 인터페이스가 다릅니다. 이 차이가 5절의 실행 방식 차이로 이어집니다.
| 어노테이션 | 어드바이스 클래스 | 구현하는 인터페이스 | 위치 |
|---|---|---|---|
@Around |
AspectJAroundAdvice |
MethodInterceptor |
39행 |
@Before |
AspectJMethodBeforeAdvice |
MethodBeforeAdvice |
34행 |
@After |
AspectJAfterAdvice |
MethodInterceptor, AfterAdvice |
35–36행 |
@AfterReturning |
AspectJAfterReturningAdvice |
AfterReturningAdvice, AfterAdvice |
39–40행 |
@AfterThrowing |
AspectJAfterThrowingAdvice |
MethodInterceptor, AfterAdvice |
35–36행 |
어드바이스 메서드를 실제로 부르는 곳은 공통 부모의 invokeAdviceMethodWithGivenArgs입니다. 어스펙트 빈을 aspectInstanceFactory에서 꺼내 리플렉션으로 메서드를 호출하고, 어드바이스 메서드가 던진 예외는 포장을 벗겨 그대로 다시 던집니다(AbstractAspectJAdvice 633–658행).
헷갈리기 쉬운 곳
@Aspect클래스는 빈이어야 합니다. 빈 이름을 훑어서 찾으므로@Component나@Bean으로 등록되지 않은 어스펙트는 존재하지 않는 것과 같습니다.- 어스펙트 훑기는 한 번뿐입니다.
aspectBeanNames가 채워진 뒤에는 다시 훑지 않습니다(88–93행). 그 "한 번"은 생각보다 이릅니다. 2절 끝에서 본 대로 기반 클래스가 아닌 빈이면 어떤 빈이든 인스턴스화 전 훅에서shouldSkip이 불리고,AspectJAwareAdvisorAutoProxyCreator의shouldSkip은findCandidateAdvisors를 부르기 때문입니다(102–112행). 그 뒤에 컨테이너에 빈 정의를 더해도 어스펙트로 인식되지 않습니다. @Aspect클래스 안의 모든 메서드가 어드바이스는 아닙니다.getAdvisor는 AspectJ 어노테이션이 없는 메서드에 대해null을 돌려주고(206–210행),getAdvisors는 그런 것을 목록에 넣지 않습니다(144–147행).
4. 어떤 빈을 감쌀지, 그리고 어떤 순서로
후보가 모였으면 지금 만들고 있는 빈에 맞는 것만 남깁니다(3절의 2번). AbstractAdvisorAutoProxyCreator.findAdvisorsThatCanApply는 AopUtils.findAdvisorsThatCanApply에 맡기고(130–140행), 그것은 후보마다 canApply를 묻습니다(319–340행). canApply의 뼈대입니다.
public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
Assert.notNull(pc, "Pointcut must not be null");
if (!pc.getClassFilter().matches(targetClass)) {
return false;
}
MethodMatcher methodMatcher = pc.getMethodMatcher();
// ...
Set<Class<?>> classes = new LinkedHashSet<>();
if (!Proxy.isProxyClass(targetClass)) {
classes.add(ClassUtils.getUserClass(targetClass));
}
classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
for (Class<?> clazz : classes) {
Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
for (Method method : methods) {
// ...
methodMatcher.matches(method, targetClass)) {
return true;
}
}
}
return false;
}
spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java 239–274행
포인트컷은 클래스를 거르는 ClassFilter와 메서드를 거르는 MethodMatcher로 이루어져 있습니다(Pointcut 33–45행). 먼저 클래스 필터로 거르고, 통과하면 그 클래스와 인터페이스의 모든 메서드를 돌며 하나라도 맞으면 이 어드바이저를 남깁니다. "메서드 하나라도"인 이유는 이 단계가 "프록시를 만들 것인가"만 정하기 때문입니다. 실제로 어느 메서드에 어드바이스를 적용할지는 5절에서 호출 때 다시 봅니다. AspectJExpressionPointcut은 ClassFilter이면서 MethodMatcher여서(83–84행) 두 질문 모두 AspectJ의 표현식 해석기에 넘깁니다(273행, 336행).
맨 앞에 하나를 더 끼웁니다
3절의 3번 extendAdvisors를 AspectJAwareAdvisorAutoProxyCreator가 덮어씁니다(97–100행). 안에서 부르는 AspectJProxyUtils.makeAdvisorChainAspectJCapableIfNecessary는 목록에 AspectJ 어드바이저가 하나라도 있으면 ExposeInvocationInterceptor.ADVISOR를 0번 자리에 넣습니다(49–67행). ExposeInvocationInterceptor는 지금 진행 중인 호출(MethodInvocation)을 ThreadLocal에 넣어 두고 다음으로 넘기는 인터셉터입니다(91–101행). 이것이 필요한 이유는 @Before나 @After 어드바이스가 자기 차례에 "지금 어떤 메서드가 불리고 있는가"(JoinPoint)를 알아야 하는데, MethodBeforeAdvice 인터페이스의 before()로는 그 정보를 직접 받지 못하기 때문입니다. 그래서 AbstractAspectJAdvice는 ExposeInvocationInterceptor.currentInvocation()으로 꺼내 씁니다(80–91행, 670–676행).
정렬: 어스펙트 사이는 @Order, 한 어스펙트 안은 종류 순
3절의 4번 sortAdvisors도 AspectJAwareAdvisorAutoProxyCreator가 덮어씁니다(72–90행). 비교 기준은 AspectJPrecedenceComparator이고, 그 compare는 두 단계입니다(81–88행).
- 먼저
AnnotationAwareOrderComparator로 비교합니다(66–68행).@Aspect에서 온 어드바이저의 순서값은 어스펙트 클래스의@Order(또는Ordered구현)에서 오고, 없으면 가장 낮은LOWEST_PRECEDENCE입니다(BeanFactoryAspectInstanceFactory128–143행,InstantiationModelAwarePointcutAdvisorImpl180–182행). 값이 작을수록 앞입니다. - 순서값이 같고 같은 어스펙트에서 왔으면 선언 순서로 비교합니다(90–123행). 그런데 3절에서 본 대로 리플렉션으로 찾은 어드바이스의 선언 순서는 전부
0으로 고정되어 있어서(136–144행) 이 비교는 늘 "같음"입니다. 결국 한 어스펙트 안의 순서는getAdvisorMethods가 정렬해 둔 종류 순서가 그대로 남습니다.
정렬은 AspectJ 라이브러리의 PartialOrder.sort가 하는데(79행), 이 저장소 바깥의 코드라 소스로 확인하지는 않았습니다. 대신 결과를 예제로 확인합니다. @Order(2)와 @Order(1)이 붙은 어스펙트 두 개에 각각 @Before와 @After를 두고, 빈 등록 순서는 일부러 2 → 1로 했습니다.
// 이 글을 위해 쓴 예제입니다 (Spring의 소스가 아닙니다)
@Aspect
@Order(2)
public static class SecondAspect {
@Before("execution(* ex.u10.AspectOrderExample.OrderService.*(..))")
public void before() { System.out.println(" @Order(2) @Before"); }
@After("execution(* ex.u10.AspectOrderExample.OrderService.*(..))")
public void after() { System.out.println(" @Order(2) @After"); }
}
@Aspect
@Order(1)
public static class FirstAspect {
@Before("execution(* ex.u10.AspectOrderExample.OrderService.*(..))")
public void before() { System.out.println(" @Order(1) @Before"); }
@After("execution(* ex.u10.AspectOrderExample.OrderService.*(..))")
public void after() { System.out.println(" @Order(1) @After"); }
}
@Order(1) @Before
@Order(2) @Before
[대상] place() 실행
@Order(2) @After
@Order(1) @After
값이 작은 @Order(1)이 바깥에 섭니다. 들어갈 때 먼저, 나올 때 나중입니다. 빈 등록 순서는 영향이 없었습니다.
이 절의 세 단계를 한 줄로 이은 그림입니다. 결과 목록은 본문의 정렬 규칙을 예제에 적용한 것이고(PartialOrder.sort 자체는 소스로 확인하지 않았습니다), 맨 아래 순서는 예제의 실제 출력입니다.
헷갈리기 쉬운 곳
@Order는 어드바이스 메서드가 아니라 어스펙트 클래스에 붙입니다. 순서값을 어스펙트 빈에서 읽기 때문입니다. 같은 어스펙트 안의 두@Before사이 순서는 메서드 이름순이지@Order로 바꿀 수 없습니다.- 순서를 지정하지 않은 어스펙트는 가장 뒤입니다. 기본값이
LOWEST_PRECEDENCE이므로,@Order가 있는 어스펙트가 늘 바깥에 섭니다. 지정하지 않은 어스펙트끼리의 순서는 정해져 있지 않다고 설명 주석에 적혀 있습니다(AbstractAdvisorAutoProxyCreator42–48행). ExposeInvocationInterceptor보다 앞에 설 수 있는 어드바이스가 있습니다. 이 인터셉터의 순서값은HIGHEST_PRECEDENCE + 1입니다(103–106행). 어스펙트에HIGHEST_PRECEDENCE를 주면 그보다 앞에 서게 되고, 그 어드바이스가JoinPoint를 쓰면 예외가 납니다. 예외 메시지에 이 상황이 그대로 적혀 있습니다(71–82행).
5. 프록시의 메서드를 부르면: 어드바이스 체인
이제 프록시가 만들어졌습니다. 메서드를 부르면 무슨 일이 일어날까요. JDK 프록시의 invoke를 보면(10편에서 본 그 메서드입니다) 어드바이스 체인을 만드는 줄과 실행하는 줄이 있습니다.
// Get the interception chain for this method.
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
// ...
if (chain.isEmpty()) {
// ...
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
// We need to create a method invocation...
MethodInvocation invocation =
new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
// Proceed to the joinpoint through the interceptor chain.
retVal = invocation.proceed();
}
spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java 202–220행
CGLIB 프록시의 DynamicAdvisedInterceptor.intercept도 같은 두 줄을 가지고 있습니다(724행, 738행). 그러니 방식과 상관없이 "체인 만들기"와 "proceed()"만 보면 됩니다.
체인 만들기: 어드바이저를 이 메서드용 인터셉터 목록으로
getInterceptorsAndDynamicInterceptionAdvice는 메서드를 열쇠로 하는 캐시(methodCache)를 먼저 보고, 없으면 DefaultAdvisorChainFactory에게 만들게 합니다(AdvisedSupport 516–538행). 만드는 쪽의 핵심은 이렇습니다.
for (Advisor advisor : advisors) {
if (advisor instanceof PointcutAdvisor pointcutAdvisor) {
// Add it conditionally.
if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {
MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();
boolean match;
// ...
match = mm.matches(method, actualClass);
// ...
if (match) {
MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
// ...
interceptorList.addAll(Arrays.asList(interceptors));
// ...
}
}
}
// ...
}
spring-aop/src/main/java/org/springframework/aop/framework/DefaultAdvisorChainFactory.java 69–109행
프록시에 들어 있는 어드바이저를 정렬된 순서 그대로 돌면서, 이번에 불린 이 메서드에 포인트컷이 맞는 것만 골라 인터셉터로 바꿉니다. 4절의 canApply가 "클래스에 하나라도 맞는 메서드가 있나"였다면 여기는 "바로 이 메서드에 맞나"입니다. 그래서 같은 프록시라도 메서드마다 체인이 다를 수 있고, 메서드별로 캐시됩니다.
"인터셉터로 바꾼다"는 것은 registry.getInterceptors(advisor)가 합니다. 3절 표에서 다섯 어드바이스 가운데 둘(@Before, @AfterReturning)은 MethodInterceptor가 아니었습니다. DefaultAdvisorAdapterRegistry는 어드바이스가 MethodInterceptor이면 그대로 쓰고, 아니면 등록된 어댑터를 돌며 감싸 줍니다(80–96행). 기본 어댑터는 셋입니다(52–56행). 그 가운데 둘이 감싸 주는 인터셉터를 보면 "앞"과 "뒤"가 코드 그대로입니다.
@Override
public @Nullable Object invoke(MethodInvocation mi) throws Throwable {
this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
return mi.proceed();
}
@Override
public @Nullable Object invoke(MethodInvocation mi) throws Throwable {
Object retVal = mi.proceed();
this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());
return retVal;
}
나머지 셋은 스스로 MethodInterceptor라서 invoke가 직접 있습니다. @After와 @AfterThrowing의 invoke는 이 글의 마지막 수수께끼를 풀어 줍니다.
@Override
public @Nullable Object invoke(MethodInvocation mi) throws Throwable {
try {
return mi.proceed();
}
finally {
invokeAdviceMethod(getJoinPointMatch(), null, null);
}
}
spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterAdvice.java 45–53행
@Override
public @Nullable Object invoke(MethodInvocation mi) throws Throwable {
try {
return mi.proceed();
}
catch (Throwable ex) {
if (shouldInvokeOnThrowing(ex)) {
invokeAdviceMethod(getJoinPointMatch(), null, ex);
}
throw ex;
}
}
spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterThrowingAdvice.java 60–71행
@After는 finally이므로 정상이든 예외든 실행되고, @AfterThrowing은 catch에서 어드바이스를 부른 뒤 throw ex로 예외를 다시 던집니다. 예외를 삼키지 않습니다. @Around는 어드바이스 메서드에 ProceedingJoinPoint를 넘기고, 그 proceed()가 MethodInvocation의 복제본으로 체인을 이어 갑니다(AspectJAroundAdvice 64–71행, MethodInvocationProceedingJoinPoint 81–83행).
proceed(): 번호 하나로 체인을 따라갑니다
체인을 실행하는 ReflectiveMethodInvocation.proceed()는 짧습니다. 10편에서 이름만 봤던 그 클래스입니다.
@Override
public @Nullable Object proceed() throws Throwable {
// We start with an index of -1 and increment early.
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
return invokeJoinpoint();
}
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher dm) {
// ...
}
else {
// It's an interceptor, so we just invoke it: The pointcut will have
// been evaluated statically before this object was constructed.
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
}
spring-aop/src/main/java/org/springframework/aop/framework/ReflectiveMethodInvocation.java 154–181행
currentInterceptorIndex는 -1에서 시작합니다(89행). proceed()가 불릴 때마다 번호를 하나 올려 그 자리의 인터셉터를 부르고, 인터셉터는 자기 일을 하다가 다시 proceed()를 부릅니다. 번호가 끝에 닿으면 invokeJoinpoint(), 곧 원본 객체의 메서드를 리플렉션으로 부릅니다(189–191행). 재귀이므로 들어갈 때는 앞에서 뒤로, 돌아올 때는 뒤에서 앞으로 실행됩니다. 양파 껍질을 벗겼다가 다시 덮는 모양입니다.
실제 출력 순서
다섯 어드바이스를 한 어스펙트에 모두 두고, 소스 파일에서는 선언 순서를 일부러 뒤섞었습니다(@After → @AfterReturning → @Before → @Around → @AfterThrowing).
// 이 글을 위해 쓴 예제입니다 (Spring의 소스가 아닙니다). 포인트컷은 모두 같은 표현식입니다.
@Aspect
public static class LoggingAspect {
// 일부러 소스의 선언 순서를 뒤섞어 둔다 (After → AfterReturning → Before → Around → AfterThrowing)
@After("execution(* ex.u10.AdviceOrderExample.GreetingService.*(..))")
public void after() {
System.out.println(" @After");
}
@AfterReturning(pointcut = "execution(* ex.u10.AdviceOrderExample.GreetingService.*(..))", returning = "result")
public void afterReturning(Object result) {
System.out.println(" @AfterReturning result=" + result);
}
@Before("execution(* ex.u10.AdviceOrderExample.GreetingService.*(..))")
public void before() {
System.out.println(" @Before");
}
@Around("execution(* ex.u10.AdviceOrderExample.GreetingService.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println(" @Around 앞");
try {
return pjp.proceed();
}
finally {
System.out.println(" @Around 뒤");
}
}
@AfterThrowing(pointcut = "execution(* ex.u10.AdviceOrderExample.GreetingService.*(..))", throwing = "ex")
public void afterThrowing(Throwable ex) {
System.out.println(" @AfterThrowing ex=" + ex.getMessage());
}
}
정상 반환하는 greet("spring")과 예외를 던지는 fail()을 차례로 불렀습니다.
== greet("spring") — 정상 반환
@Around 앞
@Before
[대상] greet(spring) 실행
@AfterReturning result=Hello, spring
@After
@Around 뒤
반환값: Hello, spring
== fail() — 예외
@Around 앞
@Before
[대상] fail() 실행 — 예외를 던진다
@AfterThrowing ex=boom
@After
@Around 뒤
main 에서 잡은 예외: boom
이 예제의 체인. 위에서 아래로 들어갔다가 아래에서 위로 돌아옵니다. 회색 칸은 4절에서 맨 앞에 끼운 보조 인터셉터입니다.
출력을 체인과 맞춰 보면 이렇습니다.
| 순서 | 체인의 인터셉터 | 들어갈 때 | 돌아올 때 |
|---|---|---|---|
| ① | ExposeInvocationInterceptor |
호출을 ThreadLocal에 둔다 |
되돌린다 |
| ② | AspectJAroundAdvice |
@Around 앞 |
@Around 뒤 |
| ③ | MethodBeforeAdviceInterceptor |
@Before |
— |
| ④ | AspectJAfterAdvice |
— | @After (finally) |
| ⑤ | AfterReturningAdviceInterceptor |
— | 정상이면 @AfterReturning |
| ⑥ | AspectJAfterThrowingAdvice |
— | 예외이면 @AfterThrowing 뒤 다시 던짐 |
| 끝 | invokeJoinpoint |
원본 메서드 실행 |
소스 선언 순서를 뒤섞어도 결과는 3절 comparator의 Around → Before → After → AfterReturning → AfterThrowing 순서로 체인이 짜였고, 돌아오는 길이 뒤집히기 때문에 @AfterReturning이 @After보다 먼저 출력됩니다. 3절 주석의 "정렬은 @After가 앞이지만 실행은 뒤"가 이것입니다. 예외가 난 경우에는 ⑤가 proceed()에서 바로 예외를 받아 afterReturning을 부르지 않고 지나갔고, ④의 finally와 ②의 finally는 실행됐으며, 예외는 main까지 올라왔습니다.
헷갈리기 쉬운 곳
- 체인은 메서드마다 다르고, 어드바이스가 없는 메서드는 체인 자체가 비어 있습니다. 위
JdkDynamicAopProxy코드의chain.isEmpty()분기가 그 경우로,MethodInvocation을 만들지도 않고 원본을 바로 부릅니다. 프록시라고 해서 모든 메서드가 느려지는 것은 아닙니다. @AfterReturning은 반환 타입을 봅니다.returning인자의 타입이 실제 반환값과 맞지 않으면 어드바이스를 부르지 않습니다(AspectJAfterReturningAdvice64–69행).@AfterThrowing의throwing인자도 같은 식으로 예외 타입을 봅니다(77–79행).@Around에서proceed()를 부르지 않으면 그 안쪽은 전부 실행되지 않습니다. 체인이 재귀로 이어지기 때문에, ②가proceed()를 생략하면 ③부터 원본 메서드까지 아무것도 불리지 않습니다. 코드에서 확인한 구조상 그렇다는 뜻이고, 예제로 돌려 보지는 않았습니다.
6. 자기 호출에는 어드바이스가 붙지 않습니다
2절의 createProxy에서 대상은 new SingletonTargetSource(bean), 곧 원본 객체였습니다. 어드바이스 체인은 프록시 안에만 있고, 원본 객체는 자기가 감싸였다는 사실을 모릅니다. 그래서 원본 객체가 자기 메서드를 this로 부르면 프록시를 거치지 않습니다.
// 이 글을 위해 쓴 예제입니다 (Spring의 소스가 아닙니다)
public static class TaskService {
public void outer() {
System.out.println(" [대상] outer() 실행, this 의 클래스: " + this.getClass().getSimpleName());
inner(); // this.inner() — 프록시를 거치지 않는다
}
public void inner() {
System.out.println(" [대상] inner() 실행");
}
}
TaskService의 모든 메서드에 @Before를 걸고 바깥에서 outer()와 inner()를 한 번씩 불렀습니다.
== 바깥에서 outer() 호출
@Before outer()
[대상] outer() 실행, this 의 클래스: TaskService
[대상] inner() 실행
== 바깥에서 inner() 호출
@Before inner()
[대상] inner() 실행
왼쪽이 자기 호출, 오른쪽이 바깥 호출. 빨간 칸이 어드바이스가 빠지는 자리입니다.
outer() 안에서 부른 inner()에는 @Before가 없습니다. this의 클래스가 TaskService(프록시가 아닌 원본)라고 찍힌 것이 이유를 보여 줍니다. 이 글에서는 다루지 않지만, 이 경우를 위해 프록시 자신을 ThreadLocal로 내주는 exposeProxy 설정과 AopContext.currentProxy()가 있습니다(EnableAspectJAutoProxy 137행, AopContext 67–72행).
더 깊이 들어가면
이 글은 큰 줄기를 위해 많은 것을 뺐습니다. 공부 노트에는 있지만 여기서는 이름만 적어 둡니다. 어스펙트를 대상 객체마다 따로 만드는 perthis·pertarget 인스턴스화 모델(BeanFactoryAspectJAdvisorsBuilder 123–133행). 인터페이스를 덧붙이는 @DeclareParents(ReflectiveAspectJAdvisorFactory 157–163행). 인자 값까지 봐야 해서 호출 때마다 다시 맞춰 보는 동적 포인트컷(isRuntime, AspectJExpressionPointcut 341–342행; proceed()의 InterceptorAndDynamicMethodMatcher 분기가 그것입니다). 어드바이스 메서드의 인자 이름을 포인트컷의 args() 등과 맞추는 argBinding(AbstractAspectJAdvice 556행부터). 인스턴스화 전 훅에서 커스텀 TargetSource로 프록시를 먼저 만들어 버리는 TargetSourceCreator(AbstractAutoProxyCreator 257–269행). 대상이 고정되고 설정이 얼어 있을 때 CGLIB 프록시가 메서드별 체인을 미리 묶어 두는 최적화(CglibAopProxy 360–391행). 그리고 @EnableAspectJAutoProxy 없이 트랜잭션 등이 쓰는 InfrastructureAdvisorAutoProxyCreator와 XML <aop:config>가 쓰는 AspectJAwareAdvisorAutoProxyCreator의 등록 경로(AopConfigUtils 70–88행)가 있습니다.
정리
| 개념 | 한 줄 요약 |
|---|---|
@EnableAspectJAutoProxy |
@Import로 registrar를 불러 AnnotationAwareAspectJAutoProxyCreator 빈 정의 하나를 등록한다. 생성기는 컨테이너에 하나뿐이고 더 능력 있는 것으로 승격된다 |
| 자동 프록시 생성기 | BeanPostProcessor. "초기화 후" 훅에서 wrapIfNecessary로 감쌀지 판단한다 |
advisedBeans |
빈마다 "감쌌나"를 기억하는 캐시. 기반 클래스와 어스펙트 자신은 FALSE |
| 후보 어드바이저 | Advisor 타입 빈 + @Aspect 빈에서 만든 어드바이저. 모든 빈 이름을 한 번 훑고 캐시한다 |
| 어스펙트 → 어드바이저 | 어드바이스 메서드 하나가 어드바이저 하나. 메서드는 Around → Before → After → AfterReturning → AfterThrowing 순으로 정렬된다 |
| 감쌀지 판단 | canApply: 클래스 필터를 통과하고 메서드 하나라도 포인트컷에 맞으면 감싼다. 하나도 없으면 원본 그대로 |
| 정렬 | 어스펙트 사이는 @Order(작을수록 바깥), 한 어스펙트 안은 종류 순. 맨 앞에 ExposeInvocationInterceptor |
| 어드바이스 체인 | 호출된 메서드에 맞는 어드바이저만 인터셉터로 바꿔 메서드별로 캐시. MethodInterceptor가 아닌 어드바이스는 어댑터가 감싼다 |
proceed() |
번호를 하나씩 올리며 인터셉터를 부르는 재귀. 들어갈 때 앞→뒤, 돌아올 때 뒤→앞 |
@After와 @AfterReturning |
정렬은 @After가 앞이지만 finally라서 실행은 @AfterReturning 뒤 |
@AfterThrowing |
catch에서 어드바이스를 부르고 throw ex로 다시 던진다. 예외를 삼키지 않는다 |
| 자기 호출 | 어드바이스는 프록시에만 있다. this.inner()는 프록시를 거치지 않는다 |
다음에 볼 것
1편의 지도에서 주 경로 아홉 단위(U01–U07, U09, U10)가 이 글로 끝났습니다. 컨테이너가 설계도를 읽고 빈을 만들고 서로 이어 주는 과정, 그리고 그 위에 프록시를 얹는 AOP까지가 Spring의 뼈대입니다. 다음부터는 이 뼈대를 전제로 하는 주변과 위층으로 갑니다.
다음 글 12편: 이벤트, Environment, Resource — 컨테이너 주변의 세 추상화에서는 주 경로에 들어 있지 않아 미뤄 둔 Phase 1의 마지막 단위를 봅니다. 빈끼리 이벤트를 주고받는 일, 설정값을 프로파일과 프로퍼티로 읽는 일, 파일과 클래스패스의 자원을 같은 모양으로 다루는 일이 각각 어떤 인터페이스로 추상화되어 있는지입니다.