@Aspect 구성
Aspect 메타데이터가 어드바이저로 변환되는 과정을 실행하고 이름 있는 포인트컷·적용 범위·상태 관리 원칙을 설계합니다.
@Aspect는 포인트컷 표현식과 어드바이스 메서드를 하나의 정책 모듈에 모읍니다. Spring은 애스펙트 빈을 읽어 어드바이저로 만들고, 적용 대상인 빈을 프록시로 감쌉니다. 하지만 클래스에 애노테이션을 붙였다는 사실만으로는 아무 일도 일어나지 않습니다. 자동 프록시 생성기의 활성화, 애스펙트와 대상의 빈 등록, 프록시 경로로 들어온 호출이 모두 있어야 합니다.
준비는 @AspectJ 지원 문서를 따라 Spring AOP와 컨텍스트, AspectJ 라이브러리를 갖추는 것에서 시작합니다. Java 25와 Spring Boot의 누적 프로젝트에서는 웹 MVC 스타터가 Spring AOP와 컨텍스트를 이미 가져오지만 AspectJ 라이브러리는 없으므로, build.gradle의 dependencies에 implementation 'org.aspectj:aspectjweaver'를 추가합니다. 버전은 Boot BOM이 관리하며, 이 의존성이 없으면 앞 글의 OperationAspect와 이 글의 MeasuredAspect는 org.aspectj 패키지를 찾지 못해 컴파일되지 않습니다. 이 글의 예제는 AnnotationConfigApplicationContext만 쓰는 최소 컨텍스트이므로, 구성 클래스의 @EnableAspectJAutoProxy와 @Bean 등록을 함께 읽어야 합니다. 자동 구성을 쓰는 Spring Boot 환경이 이 최소 컨텍스트와 같은 설정이라고 가정하지 않습니다.
@Aspect 구성
활성 구성·빈·대상 조건을 만족한 호출만 측정과 proceed를 거치고 불일치는 해당 Advisor 없이 실행된다.
@ASPECT · SETUP AND INVOCATION
활성화·빈 등록과 범위 교집합이 모두 있어야 적용된다
@Aspect는 정책 메타데이터입니다. 자동 프록시 지원과 빈 등록을 갖춘 뒤 application 공개 작업 ∩ @Measured 조건에 맞는 호출을 관찰합니다.
1 · 설정 시점
- 활성화 + 빈 등록
AspectJ 애노테이션 지원과 aspect·target 빈을 준비합니다. @Aspect 하나만으로 적용되지 않습니다.
- 이름 있는 조건의 교집합
applicationOperation()과 measuredOperation(measured)를 독립적으로 검증합니다.
- Advisor를 최종 프록시에 연결
적합한 Advisor를 찾아 caller가 받는 참조에 연결합니다. 지연 생성 빈도 실제 생성 시 이 경로를 확인합니다.
2 · 프록시 호출 시점
- 일치 메서드가 체인 진입
application 공개 작업이며 @Measured가 있는 메서드가 대상입니다.
- 한 번 proceed · 호출 로컬 상태
started와 outcome은 지역 변수에 둡니다. 공유 수집 대상은 스레드 안전해야 합니다.
- 결과와 실패 계약 확인
관찰 완료 뒤 원본 결과·업무 실패가 유지되게 설계합니다. 수집 실패 격리와 프록시 예외 계약은 별도 확인합니다.
두 조건 모두 일치: 포함. 애노테이션만 있는 helper, 애노테이션 없는 application: 제외. JDK/클래스 방식의 메타데이터 바인딩과 제한된 작업 이름도 함께 검증합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 지원·빈 등록과 상속 가능한 대상 등의 전제가 준비된 선택 흐름이다. 패키지와 애노테이션이 모두 맞으면 proxy의 측정과 proceed를 통과한다. 조건 불일치는 직접 업무 실행으로, 이 Advisor는 붙지 않는다. 다른 별도 Advisor가 없고 기록기가 실패하지 않는 예제다. 원 board.aspect 대상은 불일치 반례다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | @Aspect 구성 |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 지원 대상 조건 활성 구성·빈·대상 조건과 패키지·애노테이션을 확인합니다. | 일치: advisor 적용 · 불일치: 직접 업무 | 대상 조건이 이 advisor의 진입을 결정합니다. |
| 측정과 proceed 일치한 proxy 호출은 측정을 열고 proceed합니다. | 일치: 측정·proceed · 불일치: 이 advice 없음 | 허용된 호출만 측정 경계를 통과합니다. |
| 업무와 종료 대상이 실행되고 기록기 무실패 조건에서 측정이 끝납니다. | 업무: 실행 · 전제: 다른 advisor 없음 | 조건별 호출 경계가 완료됩니다. |
@Aspect 구성
활성 구성·빈·대상 조건을 만족한 호출만 측정과 proceed를 거치고 불일치는 해당 Advisor 없이 실행된다.
@ASPECT · SETUP AND INVOCATION
활성화·빈 등록과 범위 교집합이 모두 있어야 적용된다
@Aspect는 정책 메타데이터입니다. 자동 프록시 지원과 빈 등록을 갖춘 뒤 application 공개 작업 ∩ @Measured 조건에 맞는 호출을 관찰합니다.
1 · 설정 시점
- 활성화 + 빈 등록
AspectJ 애노테이션 지원과 aspect·target 빈을 준비합니다. @Aspect 하나만으로 적용되지 않습니다.
- 이름 있는 조건의 교집합
applicationOperation()과 measuredOperation(measured)를 독립적으로 검증합니다.
- Advisor를 최종 프록시에 연결
적합한 Advisor를 찾아 caller가 받는 참조에 연결합니다. 지연 생성 빈도 실제 생성 시 이 경로를 확인합니다.
2 · 프록시 호출 시점
- 일치 메서드가 체인 진입
application 공개 작업이며 @Measured가 있는 메서드가 대상입니다.
- 한 번 proceed · 호출 로컬 상태
started와 outcome은 지역 변수에 둡니다. 공유 수집 대상은 스레드 안전해야 합니다.
- 결과와 실패 계약 확인
관찰 완료 뒤 원본 결과·업무 실패가 유지되게 설계합니다. 수집 실패 격리와 프록시 예외 계약은 별도 확인합니다.
두 조건 모두 일치: 포함. 애노테이션만 있는 helper, 애노테이션 없는 application: 제외. JDK/클래스 방식의 메타데이터 바인딩과 제한된 작업 이름도 함께 검증합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 지원·빈 등록과 상속 가능한 대상 등의 전제가 준비된 선택 흐름이다. 패키지와 애노테이션이 모두 맞으면 proxy의 측정과 proceed를 통과한다. 조건 불일치는 직접 업무 실행으로, 이 Advisor는 붙지 않는다. 다른 별도 Advisor가 없고 기록기가 실패하지 않는 예제다. 원 board.aspect 대상은 불일치 반례다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | @Aspect 구성 |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 지원 대상 조건 활성 구성·빈·대상 조건과 패키지·애노테이션을 확인합니다. | 일치: advisor 적용 · 불일치: 직접 업무 | 대상 조건이 이 advisor의 진입을 결정합니다. |
| 측정과 proceed 일치한 proxy 호출은 측정을 열고 proceed합니다. | 일치: 측정·proceed · 불일치: 이 advice 없음 | 허용된 호출만 측정 경계를 통과합니다. |
| 업무와 종료 대상이 실행되고 기록기 무실패 조건에서 측정이 끝납니다. | 업무: 실행 · 전제: 다른 advisor 없음 | 조건별 호출 경계가 완료됩니다. |
그림의 위쪽은 설정 시점에 AspectJ 애노테이션 지원을 활성화하고 애스펙트와 대상을 빈으로 등록한 뒤, 이름 붙은 두 조건의 교집합에 맞는 메서드를 골라 어드바이저를 최종 프록시에 연결하는 단계입니다. 아래쪽은 호출 시점에 application 패키지의 공개 작업이면서 @Measured가 붙은 메서드만 체인에 들어가, 측정을 시작하고 proceed를 한 번 한 뒤 finally에서 측정을 한 번 남기는 흐름입니다. 조건 중 하나라도 맞지 않으면 원래 대상이 바로 실행되어 어드바이스는 0회이며, 이 글의 기본 예제는 바로 그 불일치 쪽의 반례입니다.
이름 있는 포인트컷
긴 실행 표현식을 어드바이스마다 복사하면, 패키지를 바꿀 때 일부만 고쳐지기 쉽습니다. 의미가 드러나는 이름을 가진 빈 포인트컷 메서드로 추출하고, 작은 조건들을 조합합니다. 예를 들어 applicationOperation()과 measuredOperation(measured)를 &&로 결합하면, 타입 범위와 명시적인 적용 표시를 따로 테스트할 수 있습니다.
package board.aspect;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Measured {
String value();
}작업 이름을 애노테이션의 값으로 고정하면, 런타임 클래스나 메서드 이름이 바뀌어도 덜 흔들리는 메트릭을 만들 수 있습니다. 이 값에는 구성원 ID처럼 카디널리티가 높은 값을 넣지 않습니다.
애스펙트의 상태 관리
애스펙트는 기본적으로 싱글톤이므로, startedNanos 같은 값을 필드에 두면 병렬 요청이 서로 덮어씁니다. 호출별 상태는 주변 어드바이스의 파라미터나 지역 변수에 보관합니다. 스레드 안전한 MeterRegistry나 수집 대상을 의존성으로 주입하되, 변경 가능한 버퍼를 직접 공유하지 않습니다.
package board.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public final class MeasuredAspect {
@Pointcut("execution(public * board.application..*(..))")
public void applicationOperation() { }
@Pointcut("@annotation(measured)")
public void measuredOperation(Measured measured) { }
@Around("applicationOperation() && measuredOperation(measured)")
public Object time(
ProceedingJoinPoint joinPoint,
Measured measured
) throws Throwable {
long started = System.nanoTime();
String outcome = "success";
try {
return joinPoint.proceed();
} catch (Throwable failure) {
outcome = failure.getClass().getSimpleName();
throw failure;
} finally {
System.out.printf("metric=%s outcome=%s elapsed=%d%n",
measured.value(), outcome,
System.nanoTime() - started);
}
}
}time은 두 이름 있는 포인트컷이 모두 참인 실행만 감쌉니다. 포인트컷 식의 measured는 어드바이스 메서드의 파라미터 이름과 연결되므로, Spring이 그 이름을 알 수 있어야 합니다. 인자 이름 결정 규칙에 따르면 권장 방식은 -parameters 옵션으로 컴파일하는 것이며, Spring Boot의 Gradle 플러그인은 JavaCompile 작업에 이 옵션을 자동으로 추가합니다. 다른 빌드에서는 argNames 속성으로 이름을 직접 지정할 수도 있습니다.
catch가 Throwable을 받는 이유는 proceed()가 그렇게 선언되어 있기 때문입니다. 어드바이스는 결과를 기록한 뒤 같은 객체를 다시 던지며, 새로운 검사 예외 타입을 선언해 애플리케이션 메서드의 시그니처를 바꾸지는 않습니다.
더 깊이: 애스펙트 빈에 따로 적용되는 규칙
애스펙트도 빈이지만 일반 빈과 다른 규칙이 몇 가지 있습니다. Spring 레퍼런스에 따르면 @Aspect만으로는 컴포넌트 스캔에 잡히지 않으므로, 스캔으로 등록하려면 @Component나 그에 준하는 스테레오타입을 함께 붙여야 합니다. 또 @Aspect가 붙은 클래스는 자동 프록시 대상에서 빠지므로, 다른 애스펙트의 어드바이스가 애스펙트 자신에게는 적용되지 않습니다. 아래 '정책 하나에 애스펙트 하나' 절에서 애스펙트가 쓰는 메트릭 수집 대상에 어드바이스가 붙지 않게 하라고 권하는 것과 달리, 애스펙트 클래스 자체에 대해서는 이 재귀를 프레임워크가 막아 줍니다.
애스펙트가 싱글톤이라는 것도 바꿀 수 있는 기본값입니다. 인스턴스 생성 모델 문서는 Spring이 AspectJ의 perthis, pertarget, pertypewithin을 지원하고 percflow, percflowbelow는 지원하지 않는다고 설명합니다. 예를 들어 @Aspect("perthis(...)")로 선언하면, 포인트컷과 맞는 서비스 객체마다 그 객체의 메서드가 처음 호출될 때 애스펙트 인스턴스가 하나씩 만들어집니다. 하지만 그 서비스 객체가 싱글톤이면 애스펙트 인스턴스도 사실상 하나뿐이고 여러 요청이 함께 쓰므로, 호출별 상태를 지역 변수에 두라는 이 절의 원칙은 그대로입니다.
프록시와 어드바이스 검증
AspectJ 애노테이션 지원을 활성화하고 애스펙트와 대상을 빈으로 등록합니다. 다만 아래 대상은 board.aspect 패키지에 있으므로 board.application..* 범위를 벗어납니다. 빈으로 등록하고 @Measured를 붙였어도 이 어드바이스는 적용되지 않으며, 애노테이션만으로는 충분하지 않다는 것을 확인하기 위한 예제입니다.
package board.aspect;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
public final class AspectContextDemo {
static class WeeklyReport {
@Measured("post.weekly")
public int postCount(long authorId) {
return authorId == 41L ? 12 : 0;
}
}
@Configuration(proxyBeanMethods = false)
@EnableAspectJAutoProxy(proxyTargetClass = true)
static class Config {
@Bean MeasuredAspect measuredAspect() { return new MeasuredAspect(); }
@Bean WeeklyReport weeklyReport() { return new WeeklyReport(); }
}
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(Config.class)) {
WeeklyReport report = context.getBean(WeeklyReport.class);
System.out.println("postCount=" + report.postCount(41L));
System.out.println("proxy=" + AopUtils.isAopProxy(report));
}
}
}패키지 조건이 거짓이면 @Measured가 붙어 있어도 이 빈에 맞는 어드바이저가 없으므로, 컨테이너는 원래 객체를 그대로 빈으로 등록합니다. 그래서 postCount(41L)은 어드바이스 없이 12를 돌려주고, 애스펙트의 metric= 줄은 찍히지 않습니다. 두 조건의 교집합이 거짓이므로 프록시가 만들어지지 않고, 소스를 기준으로 예상한 출력은 다음과 같습니다.
postCount=12
proxy=false테스트에서는 콘솔 텍스트를 비교하기보다 메트릭 수집 대상을 주입해 작업 이름, 결과, 횟수를 검증하는 편이 안정적입니다.
애노테이션 위치와 프록시 유형
인터페이스 메서드와 구현 메서드에 애노테이션을 섞어 두면, 포인트컷 바인딩이 프록시 방식과 가장 구체적인 메서드를 찾는 규칙에 영향을 받을 수 있습니다. 애플리케이션 규칙을 나타내는 애노테이션이면 인터페이스에, 구현 세부를 측정하는 애노테이션이면 구체 메서드에 두는 식으로 원칙을 정하고, JDK 프록시와 클래스 프록시 양쪽에서 테스트합니다.
메타 애노테이션(meta-annotation)을 만들어 여러 정책을 한 번에 붙일 수도 있지만, 트랜잭션과 재시도처럼 순서가 중요한 관심사를 그 안에 숨기지 않습니다. 합성 애노테이션의 속성 별칭과 기본값도 분명하게 정합니다.
패키지 전체를 대상으로 하는 실행 포인트컷은 새로 추가된 빈도 자동으로 포함합니다. 보안 애스펙트라면 누락을 막아 준다는 장점이 있지만, 원하지 않은 관리용 메서드나 상태 확인 메서드까지 적용될 수 있으므로 적용되지 않아야 할 경우를 확인하는 부정 테스트가 필요합니다.
정책 하나에 애스펙트 하나
로깅, 재시도, 인가 어드바이스를 한 클래스에 모으면, 이름 있는 포인트컷을 공유한다는 이유로 서로 다른 변경 원인이 섞입니다. 각각 별도의 애스펙트와 순서를 두고, 통합 테스트에서 체인을 검증합니다. 재사용할 포인트컷 라이브러리를 만든다면, 의존성 방향이 애플리케이션 패키지 밖으로 새지 않게 관리합니다.
애스펙트 안에서 어드바이스가 적용된 다른 빈을 호출하면, 추가 프록시 체인과 트랜잭션이 생길 수 있습니다. 메트릭 수집 대상은 가능하면 어드바이스가 붙지 않는 단순한 인프라로 두어 재귀를 막습니다.
정리
@Aspect는 정책 메타데이터일 뿐이며, 자동 프록시 활성화와 애스펙트·대상의 빈 등록, 프록시 경로 호출이 모두 있어야 적용됩니다.- 긴 포인트컷 식은 이름 있는 포인트컷으로 추출하고, 패키지 범위와 애노테이션 같은 작은 조건을
&&로 조합합니다. - 애스펙트는 싱글톤이므로 호출별 상태를 지역 변수에 두고, 결과를 기록한 뒤 같은 예외를 다시 던집니다.
- 포인트컷이 이름으로 인자를 바인딩한다면
-parameters컴파일 옵션이나argNames가 필요합니다.
연습 문제
1. 애노테이션과 패키지를 함께 요구하기
@Measured("post.create")와 애플리케이션 패키지를 동시에 요구하는 애스펙트를 구성하세요. 같은 애노테이션을 테스트 도우미에 붙여도 적용되지 않고, 대상이 실패했을 때 결과 기록과 원래 예외가 모두 보존되는지 컨텍스트 테스트로 확인하세요.
해설 보기
포인트컷을 애노테이션 하나로만 만들지 않고 패키지 범위와 교집합하면 도우미가 빠집니다. 테스트 픽스처의 패키지는 실제 구조를 그대로 반영해야 합니다.
결과를 기록할 때 예외의 클래스 이름을 그대로 쓰면, 하위 예외 타입이 늘어날수록 카디널리티가 커질 수 있습니다. 다음처럼 제한된 분류로 바꿉니다.
package board.aspect;
public final class Outcome {
public static String from(Throwable failure) {
if (failure == null) return "success";
if (failure instanceof IllegalArgumentException) return "invalid";
return "failure";
}
private Outcome() { }
}MeasuredAspect의 outcome을 이 분류로 바꾸면 결과 값은 success, invalid, failure 세 가지로 고정되고, catch는 여전히 같은 실패 객체를 다시 던집니다.
2. 적용되는 대상으로 바꾸기
AspectContextDemo의 파일은 그대로 두고, 이 어드바이스가 적용되는 양성 예제를 만들려고 합니다. 무엇을 어떻게 바꿔야 하며, 그때 예상되는 측정 횟수와 업무 실행 횟수는 얼마인가요?
해설 보기
별도의 board.application.WeeklyReport 클래스를 만들고, final이 아닌 이 클래스에 같은 공개 postCount 메서드와 @Measured("post.weekly")를 둡니다. AspectContextDemo.java는 그대로 두고, board.aspect 패키지에 같은 구성을 가진 새 데모 클래스를 만들어 그 구성 클래스의 @Bean 반환 타입과 생성식을 이 클래스로 바꾸고, 컨텍스트에서도 이 타입으로 꺼내 호출합니다. 새 구성 클래스는 board.aspect에 있으면서 다른 패키지의 WeeklyReport를 참조하므로, WeeklyReport 클래스도 public으로 선언해야 컴파일됩니다.
이제 패키지 조건과 애노테이션 조건이 모두 참이므로, time의 proceed가 업무를 한 번 실행하고 finally가 측정 줄을 한 번 남기며, proxy는 true가 될 것으로 예상합니다. 이 예상은 컨텍스트를 실제로 실행해 확인해야 하는 값입니다.
3. 세 메서드 가운데 대상 고르기
자동 프록시 지원, 빈 등록, 호출할 수 있는 공개이며 final이 아닌 메서드는 모두 갖췄다고 가정합니다. A는 board.application 안에 있고 @Measured가 있습니다. B는 같은 패키지이지만 애노테이션이 없습니다. C는 board.helper 안에 있고 애노테이션이 있습니다. 어느 메서드가 time의 대상인가요? 패키지 조건을 없애면 결과가 어떻게 바뀌나요?
해설 보기
두 조건을 동시에 만족하는 A만 대상입니다. B는 애노테이션 조건에서, C는 패키지 조건에서 빠집니다.
패키지 조건을 빼면 C까지 선택되므로, 도우미를 제외해야 한다는 연습 1의 요구가 깨집니다. 기본 예제의 AspectContextDemo는 C와 비슷한 범위 밖의 반례이고, 양성 픽스처는 연습 2처럼 별도 패키지에 파일을 준비해야 합니다.
다음 글 Advice 종류와 순서에서는 @Before, @AfterReturning, @AfterThrowing, @After, @Around가 각각 언제 실행되는지와, 여러 애스펙트가 한 메서드에 걸릴 때의 순서를 다룹니다.