Advice 종류와 순서
주요 어드바이스의 호출·반환·예외 접근 범위를 비교하고 트랜잭션·재시도·측정 애스펙트의 중첩 순서를 검증합니다.
어드바이스 애노테이션은 단순 문법 취향이 아닙니다.
대상 호출 여부, 반환 값, 실패 바인딩, finally 의미에 접근 가능한 범위가 다릅니다.
여러 애스펙트가 겹치면 @Order가 래퍼 중첩을 만들며 가장 낮은 순서가 바깥에서 먼저 들어가 마지막에 나옵니다.
숫자를 외우기보다 이벤트 순서를 실행해 확인합니다.
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를 사용하고 관찰 관심사는 읽기만 합니다.
애스펙트 내부 순서
같은 애스펙트 안의 여러 어드바이스가 동일 조인 지점에 맞으면 리플렉션의 메서드 순서가 소스 순서와 같다고 보장하지 않습니다.
확실한 순서가 필요하면 하나의 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입니다.
순서 값은 숫자 자체보다 정책 이름과 테스트로 관리합니다.
재시도·트랜잭션 순서
재시도 애스펙트가 트랜잭션 애스펙트 바깥이면 시도마다 트랜잭션 프록시를 다시 통과해 새 물리 트랜잭션을 얻을 수 있습니다.
트랜잭션이 바깥이면 첫 실패가 롤백 전용으로 만든 같은 트랜잭션에서 재시도해도 커밋할 수 없습니다.
멱등성이 있는 일시적 작업에는 보통 재시도를 바깥에, 트랜잭션을 안쪽에 두는 구성이 이해하기 쉽습니다.
타이머도 전체 지연 시간과 시도 지연 시간을 분리합니다.
전체 타이머를 재시도 밖에, 시도 타이머를 트랜잭션 또는 대상 가까이에 둡니다.
같은 미터 이름으로 둘을 섞으면 p95 해석이 틀립니다.
인가는 재시도보다 먼저 한 번만 수행할지, 각 시도에서 인증 주체 최신성을 다시 볼지 요구에 따라 정합니다.
대부분 불변 요청 인증 주체 검사는 외부가 적합하지만 오래 실행되는 재시도에는 인증 정보 만료가 중요할 수 있습니다.
예외 바인딩 범위
@AfterThrowing(throwing="failure") 파라미터 타입이 일치 범위를 제한합니다.
RuntimeException만 받으면 검사 실패를 관찰하지 못하고 Throwable은 오류까지 포함합니다.
메트릭 정책에 맞는 분류를 만들고 원본을 다시 던지는 책임은 Spring 체인에 맡깁니다.
어드바이스 자체가 예외를 던지면 대상 실패를 덮을 수 있습니다.
감사 수집 대상 실패를 허용할지 차단할지 분명히 하고 억제된 원인을 보존합니다.
실행 순서 테스트
스레드 안전한 목록에 고정 이벤트 코드를 넣고 정확한 순서를 검증합니다.
대상 실패에서도 외부 finally가 실행되는지, 정상 반환 이후 어드바이스는 실행되지 않고 예외 이후 어드바이스만 호출되는지 봅니다.
병렬 테스트에서 전역 정적 목록을 공유하지 않습니다.
연습 문제
인가, 전체 타이머, 재시도, 트랜잭션, 시도 타이머의 순서를 설계하세요.
일시적 실패 두 번 뒤 성공하는 대상에서 각 어드바이스와 트랜잭션 횟수를 예상하고 이벤트 순서 테스트를 작성하세요.
해설 보기
예시 순서는 인가 → 전체 타이머 → 재시도 → 트랜잭션 → 시도 타이머 → 대상입니다.
세 번째 성공이면 인가 1, 전체 타이머 1, 재시도 래퍼 1, 트랜잭션 3, 시도 타이머 3, 대상 3을 기대합니다.
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;
}
}재시도를 모두 소진하면 마지막 일시적 예외를 원인과 함께 호출자에게 전달하고 전체 타이머 결과를 실패로 기록합니다.