Advice 종류와 순서
어드바이스의 호출·반환·예외 접근 범위를 비교하고 로그·재시도 프록시 예제로 중첩 순서를 읽습니다. 트랜잭션 경계가 달라지는 전제도 구분합니다.
어드바이스 애노테이션의 종류는 문법 취향의 문제가 아닙니다. 종류마다 대상을 호출할지 정할 수 있는지, 반환값과 실패를 볼 수 있는지, finally처럼 항상 실행되는지가 다릅니다.
여러 애스펙트가 한 메서드에 겹치면 @Order가 래퍼의 중첩을 만듭니다. 순서 값이 가장 작은 애스펙트의 어드바이스가 바깥에서 먼저 들어가고 마지막에 나옵니다. 이 글에서는 숫자를 외우기보다 이벤트가 찍히는 순서를 따라가며 확인합니다.
반환·예외 경로와 @Order가 하나의 어드바이스 래퍼 스택에서 만난다
Audit 10→Timer 20→Target 진입 뒤 Timer·Audit finally로 역순 종료하는 다섯 이벤트를 추적한다.
ADVICE · WRAPPER ORDER AND COMPLETION
낮은 @Order가 먼저 들어가고 마지막에 나온다
서로 다른 aspect에서 Audit order 10이 Timer order 20을 감쌉니다. 정상 반환과 예외 전파 모두 바깥 래퍼의 finally가 역순으로 끝납니다.
- Audit 10 → Timer 20 → Target
다른 aspect의 낮은 order가 바깥입니다. 예제의 around는 대상을 한 번 진행합니다.
- Target → Timer out → Audit out
대상의 반환 또는 예외 뒤 finally가 안쪽부터 역순으로 종료합니다.
Before / Around · 진행 제어
Before는 반환을 교체하지 않지만 예외로 차단할 수 있습니다. Around는 proceed 횟수·인자·반환을 직접 소유합니다.
완료 Advice · 관찰 범위
AfterReturning은 정상 결과, AfterThrowing은 일치 예외를 관찰합니다. 같은 aspect의 After는 두 종류 뒤 finally 의미로 실행됩니다.
동일 종류의 내부 순서는 미정
같은 aspect에서 같은 종류의 advice가 여러 개면 소스 선언 순서에 의존하지 않습니다. 한 메서드로 합치거나 aspect를 나눠 @Order를 둡니다.
Retry 바깥 · TX 안쪽
독립 시도별 트랜잭션을 얻는 설정이라면 실패1 rollback → 재평가 → 시도2 새 TX입니다. 상위 TX·전파에 따라 물리 경계가 달라집니다.
Advice 자체 실패는 업무 실패를 덮을 수 있습니다. 원인 보존 정책과 실제 진입·종료·대상 횟수를 검증하고 전체/시도 타이머를 구분합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 서로 다른 두 Around의 이벤트다. Audit 10→Timer 20→Target에 들어간 뒤 반환 또는 예외는 Timer finally→Audit finally 순으로 종료한다. finally 자체가 실패하지 않는 전제이며 실제 RetryProxyTest의 트랜잭션 관측을 뜻하지 않는다. 세로 도표는 동일한 다섯 이벤트와 네 전달을 아래 방향으로 재배치했다.
- Audit 10 진입 뒤 Timer 20 진입, 대상 실행, Timer 종료, Audit 종료의 순서다. 단일 around 예시이며 대상과 advice 자체의 실패는 별도 계약으로 검증한다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | 반환·예외 경로와 @Order가 하나의 어드바이스 래퍼 스택에서 만난다 |
| 6 | 서로 다른 두 Around의 이벤트다. Audit 10→Timer 20→Target에 들어간 뒤 반환 또는 예외는 Timer finally→Audit finally 순으로 종료한다. finally 자체가 실패하지 않는 전제이며 실제 RetryProxyTest의 트랜잭션 관측을 뜻하지 않는다. 세로 도표는 동일한 다섯 이벤트와 네 전달을 아래 방향으로 재배치했다. |
| 19 | Audit 10 진입 뒤 Timer 20 진입, 대상 실행, Timer 종료, Audit 종료의 순서다. 단일 around 예시이며 대상과 advice 자체의 실패는 별도 계약으로 검증한다. |
| 32 | 독립 시도별 트랜잭션을 얻는 설정이라면 실패 1 rollback → 재평가 → 시도 2 새 TX입니다. 상위 TX·전파에 따라 물리 경계가 달라집니다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| Audit 진입 Order 10의 Audit Around가 먼저 들어옵니다. | 활성: Audit · Timer: 미진입 · Target: 미호출 | 바깥 Audit 실행이 시작됩니다. |
| Timer와 대상 실행 Audit 안에서 Order 20 Timer가 들어온 뒤 Target을 호출합니다. | 활성: Audit·Timer · Target: 실행 · 진입 순서: Audit→Timer→Target | Timer와 Target이 바깥 실행 안에 쌓입니다. |
| Timer 먼저 정리 대상이 반환하거나 예외를 던지면 안쪽 Timer finally가 먼저 실행됩니다. | Target: 종료 · Timer: finally · Audit: 대기 | 안쪽 Timer의 정리가 시작됩니다. |
| Audit 정리 Timer finally가 실패하지 않은 전제에서 바깥 Audit finally가 실행됩니다. | Timer: 종료 · Audit: finally · 종료 순서: Timer→Audit | 바깥 Audit까지 정리가 진행됩니다. |
반환·예외 경로와 @Order가 하나의 어드바이스 래퍼 스택에서 만난다
Audit 10→Timer 20→Target 진입 뒤 Timer·Audit finally로 역순 종료하는 다섯 이벤트를 추적한다.
ADVICE · WRAPPER ORDER AND COMPLETION
낮은 @Order가 먼저 들어가고 마지막에 나온다
서로 다른 aspect에서 Audit order 10이 Timer order 20을 감쌉니다. 정상 반환과 예외 전파 모두 바깥 래퍼의 finally가 역순으로 끝납니다.
- Audit 10 → Timer 20 → Target
다른 aspect의 낮은 order가 바깥입니다. 예제의 around는 대상을 한 번 진행합니다.
- Target → Timer out → Audit out
대상의 반환 또는 예외 뒤 finally가 안쪽부터 역순으로 종료합니다.
Before / Around · 진행 제어
Before는 반환을 교체하지 않지만 예외로 차단할 수 있습니다. Around는 proceed 횟수·인자·반환을 직접 소유합니다.
완료 Advice · 관찰 범위
AfterReturning은 정상 결과, AfterThrowing은 일치 예외를 관찰합니다. 같은 aspect의 After는 두 종류 뒤 finally 의미로 실행됩니다.
동일 종류의 내부 순서는 미정
같은 aspect에서 같은 종류의 advice가 여러 개면 소스 선언 순서에 의존하지 않습니다. 한 메서드로 합치거나 aspect를 나눠 @Order를 둡니다.
Retry 바깥 · TX 안쪽
독립 시도별 트랜잭션을 얻는 설정이라면 실패1 rollback → 재평가 → 시도2 새 TX입니다. 상위 TX·전파에 따라 물리 경계가 달라집니다.
Advice 자체 실패는 업무 실패를 덮을 수 있습니다. 원인 보존 정책과 실제 진입·종료·대상 횟수를 검증하고 전체/시도 타이머를 구분합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 서로 다른 두 Around의 이벤트다. Audit 10→Timer 20→Target에 들어간 뒤 반환 또는 예외는 Timer finally→Audit finally 순으로 종료한다. finally 자체가 실패하지 않는 전제이며 실제 RetryProxyTest의 트랜잭션 관측을 뜻하지 않는다. 세로 도표는 동일한 다섯 이벤트와 네 전달을 아래 방향으로 재배치했다.
- Audit 10 진입 뒤 Timer 20 진입, 대상 실행, Timer 종료, Audit 종료의 순서다. 단일 around 예시이며 대상과 advice 자체의 실패는 별도 계약으로 검증한다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | 반환·예외 경로와 @Order가 하나의 어드바이스 래퍼 스택에서 만난다 |
| 6 | 서로 다른 두 Around의 이벤트다. Audit 10→Timer 20→Target에 들어간 뒤 반환 또는 예외는 Timer finally→Audit finally 순으로 종료한다. finally 자체가 실패하지 않는 전제이며 실제 RetryProxyTest의 트랜잭션 관측을 뜻하지 않는다. 세로 도표는 동일한 다섯 이벤트와 네 전달을 아래 방향으로 재배치했다. |
| 19 | Audit 10 진입 뒤 Timer 20 진입, 대상 실행, Timer 종료, Audit 종료의 순서다. 단일 around 예시이며 대상과 advice 자체의 실패는 별도 계약으로 검증한다. |
| 32 | 독립 시도별 트랜잭션을 얻는 설정이라면 실패 1 rollback → 재평가 → 시도 2 새 TX입니다. 상위 TX·전파에 따라 물리 경계가 달라집니다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| Audit 진입 Order 10의 Audit Around가 먼저 들어옵니다. | 활성: Audit · Timer: 미진입 · Target: 미호출 | 바깥 Audit 실행이 시작됩니다. |
| Timer와 대상 실행 Audit 안에서 Order 20 Timer가 들어온 뒤 Target을 호출합니다. | 활성: Audit·Timer · Target: 실행 · 진입 순서: Audit→Timer→Target | Timer와 Target이 바깥 실행 안에 쌓입니다. |
| Timer 먼저 정리 대상이 반환하거나 예외를 던지면 안쪽 Timer finally가 먼저 실행됩니다. | Target: 종료 · Timer: finally · Audit: 대기 | 안쪽 Timer의 정리가 시작됩니다. |
| Audit 정리 Timer finally가 실패하지 않은 전제에서 바깥 Audit finally가 실행됩니다. | Timer: 종료 · Audit: finally · 종료 순서: Timer→Audit | 바깥 Audit까지 정리가 진행됩니다. |
그림은 서로 다른 두 애스펙트의 주변 어드바이스가 만드는 다섯 이벤트입니다. 순서 값 10인 Audit가 먼저 들어가고, 그 안에서 순서 값 20인 Timer가 들어간 뒤 대상이 한 번 실행됩니다. 대상이 반환하든 예외를 던지든 안쪽 Timer의 finally가 먼저, 바깥 Audit의 finally가 나중에 끝납니다. 이 순서는 두 finally가 스스로 실패하지 않는다는 전제에서 성립하며, 아래 칸들은 어드바이스 종류별로 다룰 수 있는 범위와 재시도·트랜잭션의 순서를 덧붙여 정리합니다.
Around가 맡는 범위
주변 어드바이스(@Around)는 대상의 앞뒤, 반환값, 예외, 인자를 모두 다룹니다. proceed()를 빠뜨리면 대상이 실행되지 않고, 두 번 호출하면 부수 효과도 두 번 일어납니다. 그래서 캐시, 인가, 재시도처럼 호출 횟수를 의도적으로 제어할 때 쓰고, 단순한 시작 로그라면 호출 전 어드바이스로 충분합니다.
package board.advice;
import java.util.concurrent.atomic.AtomicInteger;
public final class AroundContract {
public static <T> T exactlyOnce(
AtomicInteger counter,
Operation<T> operation
) {
counter.incrementAndGet();
return operation.call();
}
@FunctionalInterface
public interface Operation<T> {
T call();
}
public static void main(String[] args) {
var adviceCalls = new AtomicInteger();
var targetCalls = new AtomicInteger();
long id = exactlyOnce(adviceCalls, () -> {
targetCalls.incrementAndGet();
return 41L;
});
System.out.printf("id=%d advice=%d target=%d%n",
id, adviceCalls.get(), targetCalls.get());
}
}이 순수 자바 모델은 어드바이스가 대상을 정확히 한 번 진행하는 규칙을 카운터로 확인합니다. exactlyOnce가 어드바이스 역할로 adviceCalls를 한 번 올리고, 넘겨받은 람다를 한 번 호출하는 것이 proceed()에 해당합니다. 람다 안에서 targetCalls가 한 번 오르고 41을 돌려주므로, 세 값은 각각 41, 1, 1입니다.
id=41 advice=1 target=1Spring 어드바이스를 테스트할 때도 같은 개수를 검사합니다. 프록시가 생성되었는지만 보아서는 proceed를 잘못 쓴 것을 발견할 수 없습니다.
Before와 After 계열 어드바이스
@Before는 대상보다 먼저 실행되지만 반환값을 바꿀 수 없습니다. 다만 예외를 던져 호출을 막을 수 있으므로, 인가에 쓸 때는 거절했을 때 어떤 예외가 나가는지 명시합니다.
@AfterReturning은 정상 반환에서 결과를 보고, @AfterThrowing은 타입이 일치하는 예외를 파라미터로 받습니다. @After는 자바의 finally처럼 성공과 실패 모두에서 실행됩니다.
결과를 구분해야 하는 타이머라면, 하나의 지역 상태를 유지할 수 있는 @Around가 쓰기 쉽습니다. 여러 어드바이스 메서드로 나누면 시작 시각을 넘겨줄 곳이 마땅치 않아 필드에 두고 싶어지는데, 싱글톤 애스펙트에서는 그러면 안 됩니다. 반환 후 어드바이스에서 반환 객체를 바꾸면 대상의 규칙이 조용히 바뀌므로, 불변 DTO를 쓰고 기록하는 관심사는 읽기만 합니다.
애스펙트 안의 순서와 애스펙트 사이의 순서
같은 애스펙트 안에서 종류가 다른 어드바이스는 종류별 우선순위를 따릅니다. 특히 @After는 같은 애스펙트의 @AfterReturning이나 @AfterThrowing보다 뒤에, finally의 의미로 실행됩니다.
같은 종류의 어드바이스 메서드가 한 애스펙트에 여러 개라면, 리플렉션이 소스의 선언 순서를 보장하지 않으므로 그 사이의 순서는 정해져 있지 않습니다. 확실한 순서가 필요하면 하나의 @Around로 합치거나, 애스펙트를 나눠 각각 순서를 부여합니다.
package board.advice;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
public final class OrderedAspects {
@Aspect
@Order(10)
public static class OuterAudit {
@Around("execution(* board.application..*(..))")
public Object audit(ProceedingJoinPoint point) throws Throwable {
System.out.println("audit-in");
try {
return point.proceed();
} finally {
System.out.println("audit-out");
}
}
}
@Aspect
@Order(20)
public static class InnerTimer {
@Around("execution(* board.application..*(..))")
public Object time(ProceedingJoinPoint point) throws Throwable {
System.out.println("timer-in");
try {
return point.proceed();
} finally {
System.out.println("timer-out");
}
}
}
private OrderedAspects() { }
}두 애스펙트는 서로 다른 클래스이고 OuterAudit의 10이 InnerTimer의 20보다 우선하므로, 예상 순서는 audit-in, timer-in, 대상, timer-out, audit-out입니다. Audit의 proceed가 Timer로, Timer의 proceed가 대상으로 들어가며, 대상의 결과를 돌려주려면 안쪽 호출부터 끝나야 하므로 Timer의 finally 뒤에 Audit의 finally가 실행됩니다. 이것은 Spring의 어드바이스 순서 규칙 가운데 서로 다른 애스펙트 사이의 규칙이며, 같은 애스펙트 안에 같은 종류의 메서드 둘을 위아래로 놓은 경우까지 넓혀 적용하지 않습니다.
이 파일은 두 어드바이스의 정의만 보여 줍니다. 실제로 적용하려면 자동 프록시 지원을 활성화하고, 두 애스펙트와 board.application 범위의 대상을 Spring 빈으로 등록해야 합니다. 클래스 프록시를 쓴다면 대상 클래스와 메서드가 final이 아니어야 하고, 컨텍스트가 돌려준 프록시로 호출해야 합니다. @Aspect 구성의 범위 밖 대상 예제와 비교해 볼 수 있습니다.
설정부터 대상 호출, 이벤트 비교까지 이어지는 작은 예제는 로깅·재시도 AOP의 RetryProxyTest에 있습니다. 그 예제에서는 바깥 로그가 한 번 들어가고 안쪽 대상은 최대 세 번 진행되므로, 순서가 호출 횟수에 어떤 영향을 주는지도 읽을 수 있습니다. 순서 값은 숫자 자체보다 정책 이름과 테스트로 관리합니다.
재시도와 트랜잭션의 순서
재시도 애스펙트가 바깥에 있으면, 시도할 때마다 안쪽의 트랜잭션 어드바이스를 다시 지날 수 있습니다. 상위 트랜잭션이 없고 각 시도가 트랜잭션을 새로 시작하고 끝낸다는 전제라면, 시도마다 새 물리 트랜잭션을 얻습니다.
이미 상위 트랜잭션에 REQUIRED로 참여하고 있다면, 재시도가 바깥에 있어도 같은 물리 트랜잭션일 수 있습니다. 첫 실패가 롤백 조건에 맞아 트랜잭션이 롤백 전용으로 표시됐다면, 이후 시도가 성공해도 그 트랜잭션은 커밋할 수 없습니다. REQUIRES_NEW는 독립 트랜잭션을 주지만, 새 연결과 커넥션 풀 용량, 업무의 커밋 단위를 따로 따져야 합니다. 멱등성이 있는 일시적 작업이라면 재시도를 바깥에, 트랜잭션을 안쪽에 두는 구성이 이해하기 쉽습니다.
RetryProxyTest는 로그와 재시도의 순서만 구성하고 트랜잭션은 포함하지 않으므로, 그 이벤트를 물리 트랜잭션의 수나 롤백 결과로 해석하지 않습니다.
타이머도 전체 지연 시간과 시도별 지연 시간을 나눕니다. 전체 타이머는 재시도 바깥에, 시도 타이머는 트랜잭션이나 대상 가까이에 둡니다. 둘을 같은 미터 이름으로 섞으면 p95 같은 백분위수의 해석이 틀어집니다.
인가를 재시도 전에 한 번만 할지, 시도마다 인증 주체가 아직 유효한지 다시 볼지는 요구에 따라 정합니다. 대부분은 불변인 요청 인증 주체를 바깥에서 한 번 검사하면 되지만, 오래 걸리는 재시도라면 인증 정보의 만료가 중요할 수 있습니다.
더 깊이: 트랜잭션 어드바이저의 순서는 기본으로 가장 안쪽입니다
트랜잭션도 애스펙트와 같은 순서 규칙을 따르는 어드바이저 하나입니다. Spring Framework 7.0.10의 @EnableTransactionManagement는 트랜잭션 어드바이저의 순서를 정하는 order 속성을 두고, 기본값을 가장 낮은 우선순위인 Ordered.LOWEST_PRECEDENCE로 정해 둡니다. Spring Boot 4.1.2의 TransactionAutoConfiguration도 프록시 방식만 지정해 이 애노테이션을 쓰므로 순서는 기본값 그대로입니다. 그래서 재시도 애스펙트에 @Order로 그보다 작은 값을 주면, 트랜잭션은 자연스럽게 재시도의 안쪽에 놓여 시도마다 트랜잭션 어드바이스를 다시 지나게 됩니다.
문제는 순서를 적지 않은 애스펙트입니다. Spring 레퍼런스는 서로 다른 애스펙트의 어드바이스가 같은 조인 포인트에 걸릴 때 따로 정하지 않으면 실행 순서가 정해져 있지 않다고 설명합니다. 순서를 정하지 않은 애스펙트도 가장 낮은 우선순위로 취급되므로, @Order를 빠뜨린 재시도 애스펙트와 트랜잭션 어드바이저 가운데 어느 쪽이 바깥에 올지는 보장되지 않습니다. 재시도와 트랜잭션의 위치가 업무 결과를 바꾸는 이상, 재시도 쪽에는 반드시 명시적인 순서를 주고 트랜잭션의 순서가 필요하면 @EnableTransactionManagement(order = ...)로 함께 적어 둡니다.
예외 바인딩의 범위
@AfterThrowing(throwing = "failure")에서 failure 파라미터의 타입이 일치 범위를 제한합니다. RuntimeException으로 받으면 검사 예외를 보지 못하고, Throwable로 받으면 Error까지 포함합니다. 메트릭 정책에 맞는 분류를 정하고, 원래 예외를 다시 던지는 일은 Spring의 체인에 맡깁니다.
어드바이스 자체가 예외를 던지면 대상의 실패를 덮을 수 있습니다. 감사 기록기의 실패를 허용할지 호출을 막을지 분명히 정하고, 덮인 원인은 억제된 예외로 보존합니다.
실행 순서 테스트
스레드 안전한 목록에 고정된 이벤트 코드를 넣고, 정확한 순서를 검증합니다. 대상이 실패해도 바깥 finally가 실행되는지, 실패 경로에서는 반환 후 어드바이스가 실행되지 않고 예외 후 어드바이스만 호출되는지 봅니다. 병렬 테스트에서는 전역 정적 목록을 공유하지 않습니다.
정리
@Around는proceed횟수와 인자, 반환, 예외를 모두 다루며, 대상을 정확히 한 번 진행하는 것이 기본입니다.@Before는 반환을 바꿀 수 없지만 예외로 호출을 막을 수 있고,@AfterReturning·@AfterThrowing·@After는 각각 정상, 실패, 항상의 경로에서 실행됩니다.- 서로 다른 애스펙트는
@Order값이 작을수록 바깥에 놓이며, 같은 애스펙트 안의 같은 종류 어드바이스끼리는 순서가 정해져 있지 않습니다. - 재시도와 트랜잭션의 순서는 물리 트랜잭션의 수를 바꾸므로, 상위 트랜잭션과 전파 속성을 함께 보고 정합니다.
연습 문제
1. 다섯 래퍼의 순서
인가, 전체 타이머, 재시도, 트랜잭션, 시도 타이머의 순서를 설계하세요. 일시적 실패 두 번 뒤에 성공하는 대상에서 각 어드바이스와 트랜잭션의 횟수를 예상하고, 이벤트 순서 테스트를 작성하세요.
해설 보기
한 가지 순서는 인가, 전체 타이머, 재시도, 트랜잭션, 시도 타이머, 대상입니다. 세 번째 시도에서 성공하면 인가 1, 전체 타이머 1, 재시도 래퍼 1, 트랜잭션 3, 시도 타이머 3, 대상 3을 기대합니다.
이 개수는 각 시도가 트랜잭션을 따로 시작하고 끝낸다는 가정에서 나옵니다. 아래 AdviceCounts는 예상값을 비교하는 순수 모델이며, 실제 어드바이스나 물리 트랜잭션을 관찰하지는 않습니다.
package board.advice;
public record AdviceCounts(
int authorization,
int totalTimers,
int transactions,
int attemptTimers,
int targetCalls
) {
public boolean matchesThreeAttempts() {
return authorization == 1
&& totalTimers == 1
&& transactions == 3
&& attemptTimers == 3
&& targetCalls == 3;
}
}재시도를 모두 소진하면, 마지막 일시적 예외를 원인과 함께 호출자에게 전달하고 전체 타이머의 결과를 실패로 기록합니다.
2. 두 번 모두 실패하면
연습 1의 순서를 그대로 씁니다. 최대 총 시도는 2이고, 두 번 모두 재시도할 수 있는 일시적 실패입니다. 상위 트랜잭션은 없고 각 시도마다 트랜잭션을 새로 시작하고 끝내며, 두 실패 모두 롤백 조건을 만족합니다. 기록기와 롤백은 실패하지 않습니다. 인가, 전체 타이머, 시도 타이머, 대상, 롤백의 개수를 구하세요. 별도의 경우로 첫 대상 실패가 재시도 조건과 맞지 않으면 어떻게 달라지나요?
해설 보기
두 번 모두 일시적으로 실패하면 인가 1, 전체 타이머 1, 시도 타이머 2, 대상 2, 롤백 2입니다. 전체 타이머는 바깥의 요청 하나를, 시도 타이머는 안쪽의 두 호출을 감쌉니다.
첫 실패가 재시도 대상이 아니라면 두 번째 시도가 없으므로 인가 1, 전체 타이머 1, 시도 타이머 1, 대상 1, 롤백 1입니다. 이 수는 문제에서 정한 독립 트랜잭션 가정에 따른 모델이며, RetryProxyTest에서 실제로 측정한 트랜잭션 값이 아닙니다.
3. 순서 값을 바꾸면
OrderedAspects를 그대로 적용한 상태에서 대상이 IllegalStateException을 던지면 다섯 이벤트는 어떤 순서로 찍히나요? 별도로 OuterAudit을 @Order(20), InnerTimer를 @Order(10)으로 바꾸고 대상이 정상 반환하면 순서는 어떻게 되나요? 두 finally는 실패하지 않습니다.
해설 보기
대상이 예외를 던져도 audit-in, timer-in, 대상, timer-out, audit-out의 순서는 같습니다. 예외가 Timer의 finally를 거쳐 Audit의 finally로 올라가고, 호출자는 같은 IllegalStateException을 받습니다.
순서 값을 바꾸면 Timer가 바깥이 되어 timer-in, audit-in, 대상, audit-out, timer-out 순서가 됩니다. 이렇게 바꾸면 Timer가 Audit의 처리 시간까지 함께 재게 되므로, 어떤 범위를 측정할지에 따라 순서 값을 정합니다.
다음 글 실행·범위 포인트컷에서는 어드바이스를 붙일 메서드를 고르는 execution과 within 같은 포인트컷 지시자의 문법과 범위를 자세히 다룹니다.