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까지 정리가 진행됩니다. |
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());
}
}id=41 advice=1 target=1Spring 어드바이스 테스트에서도 동일한 개수를 검사합니다.
프록시 생성 여부만으로 진행 오용을 발견할 수 없습니다.
Before·After 어드바이스
@Before는 대상 전에 실행하지만 반환을 바꿀 수 없습니다.
예외를 던져 호출을 차단할 수 있으므로 인가에 사용할 때 실패 규칙을 명시합니다.
@AfterReturning은 정상 반환에서 결과를 관찰하고 @AfterThrowing은 일치하는 예외를 바인딩합니다.
@After는 Java의 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() { }
}예상 순서는 audit-in → timer-in → target → timer-out → audit-out입니다.
예시 서로 다른 두 Aspect에서 10이 20보다 우선하므로 Audit가 바깥 래퍼입니다. Audit의 proceed가 Timer로, Timer의 proceed가 대상으로 들어갑니다. 대상의 반환값을 전달하려면 안쪽 호출부터 끝나야 하므로 Timer의 finally 뒤 Audit의 finally가 실행됩니다. 이 두 finally가 실패하지 않으면 대상이 예외를 던져도 종료 두 이벤트의 역순은 같습니다.
이것은 서로 다른 Aspect의 우선순위 규칙에 맞춘 두 Around의 순서입니다. 같은 Aspect 안의 같은 종류 메서드 두 개를 단지 소스 위아래로 배치한 경우까지 그 순서를 일반화하지 않습니다.
이 파일은 두 advice의 정의만 보여 줍니다. 실제로 적용하려면 자동 프록시 지원을 활성화하고 두 애스펙트와 board.application 범위의 대상을 Spring 빈으로 등록해야 합니다. 클래스 프록시를 선택했다면 대상 클래스와 대상 메서드가 final이 아니어야 하며, 컨텍스트가 돌려준 프록시에서 호출해야 합니다. @Aspect 구성의 범위 밖 대상 예제와 비교해 보세요.
설정부터 대상 호출과 이벤트 비교까지 이어지는 작은 예제는 로깅·재시도 AOP의 RetryProxyTest에 있습니다. 그 예제에서는 바깥 로그가 한 번 들어가고 안쪽 대상은 최대 세 번 진행되므로, 순서가 호출 횟수에 어떤 영향을 주는지도 읽을 수 있습니다.
순서 값은 숫자 자체보다 정책 이름과 테스트로 관리합니다.
재시도·트랜잭션 순서
재시도 애스펙트가 바깥이면 시도마다 안쪽 트랜잭션 advice를 다시 통과할 수 있습니다. 상위 트랜잭션이 없고 각 시도가 트랜잭션을 시작·종료한다는 전제에서 새 물리 트랜잭션을 얻습니다.
이미 상위 트랜잭션에 REQUIRED로 참여하고 있다면 retry가 바깥이어도 같은 물리 트랜잭션일 수 있습니다. 해당 실패가 rollback 조건에 맞아 롤백 전용으로 표시됐다면 이후 시도가 성공해도 그 트랜잭션을 커밋할 수 없습니다. REQUIRES_NEW는 독립 트랜잭션이지만 새 연결과 풀 용량, 업무의 커밋 단위를 따로 판단해야 합니다.
멱등성이 있는 일시적 작업에는 보통 재시도를 바깥에, 트랜잭션을 안쪽에 두는 구성이 이해하기 쉽습니다.
RetryProxyTest는 로그와 재시도의 순서만 구성하며 트랜잭션은 포함하지 않습니다. 그 이벤트를 물리 트랜잭션 개수나 rollback 관측값으로 해석하지 않습니다.
타이머도 전체 지연 시간과 시도 지연 시간을 분리합니다.
전체 타이머를 재시도 밖에, 시도 타이머를 트랜잭션 또는 대상 가까이에 둡니다.
같은 미터 이름으로 둘을 섞으면 p95 해석이 틀립니다.
독립 확인: 원 해설의 인가 → 전체 타이머 → 재시도 → 트랜잭션 → 시도 타이머 → 대상 순서를 씁니다. 최대 총 시도는 2이고, 두 번 모두 재시도 가능한 일시 실패입니다. 상위 트랜잭션은 없으며 각 시도마다 트랜잭션을 새로 시작·종료하고 두 실패 모두 롤백 조건을 만족합니다. 기록기·롤백은 실패하지 않습니다. 인가/전체/시도/대상/롤백 개수를 구하세요. 별도 경우로 첫 대상 실패가 재시도 조건과 맞지 않으면 어떻게 달라지나요?
풀이 확인
두 번 일시 실패라면 인가 1·전체 타이머 1·시도 타이머 2·대상 2·롤백 2입니다. 전체 타이머는 바깥 요청 하나를, 시도 타이머는 안쪽의 두 호출을 감쌉니다. 첫 실패가 재시도 대상이 아닌 별도 경우에는 두 번째 시도가 없어 인가 1·전체 1·시도 1·대상 1·롤백 1입니다. 이 수는 문제의 독립 트랜잭션 가정에 따른 모델이며 원 RetryProxyTest의 실제 트랜잭션 관측값은 아닙니다.
인가는 재시도보다 먼저 한 번만 수행할지, 각 시도에서 인증 주체 최신성을 다시 볼지 요구에 따라 정합니다.
대부분 불변 요청 인증 주체 검사는 외부가 적합하지만 오래 실행되는 재시도에는 인증 정보 만료가 중요할 수 있습니다.
예외 바인딩 범위
@AfterThrowing(throwing="failure") 파라미터 타입이 일치 범위를 제한합니다.
RuntimeException만 받으면 검사 예외(checked exception)를 관찰하지 못하고 Throwable은 오류까지 포함합니다.
메트릭 정책에 맞는 분류를 만들고 원본을 다시 던지는 책임은 Spring 체인에 맡깁니다.
어드바이스 자체가 예외를 던지면 대상 실패를 덮을 수 있습니다.
감사 수집 대상 실패를 허용할지 차단할지 분명히 하고 억제된 원인을 보존합니다.
실행 순서 테스트
스레드 안전한 목록에 고정 이벤트 코드를 넣고 정확한 순서를 검증합니다.
대상 실패에서도 외부 finally가 실행되는지, 정상 반환 이후 어드바이스는 실행되지 않고 예외 이후 어드바이스만 호출되는지 봅니다.
병렬 테스트에서 전역 정적 목록을 공유하지 않습니다.
연습 문제
인가, 전체 타이머, 재시도, 트랜잭션, 시도 타이머의 순서를 설계하세요.
일시적 실패 두 번 뒤 성공하는 대상에서 각 어드바이스와 트랜잭션 횟수를 예상하고 이벤트 순서 테스트를 작성하세요.
해설 보기
예시 순서는 인가 → 전체 타이머 → 재시도 → 트랜잭션 → 시도 타이머 → 대상입니다.
세 번째 성공이면 인가 1, 전체 타이머 1, 재시도 래퍼 1, 트랜잭션 3, 시도 타이머 3, 대상 3을 기대합니다.
이는 각 시도가 별도 트랜잭션 경계를 시작·종료한다는 가정의 개수입니다. 아래 AdviceCounts는 예상값을 비교하는 순수 모델이며 실제 advice나 물리 트랜잭션을 관찰하지 않습니다.
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;
}
}재시도를 모두 소진하면 마지막 일시적 예외를 원인과 함께 호출자에게 전달하고 전체 타이머 결과를 실패로 기록합니다.