@Aspect 구성
Aspect 메타데이터가 어드바이저로 변환되는 과정을 실행하고 이름 있는 포인트컷·적용 범위·상태 관리 원칙을 설계합니다.
@Aspect는 포인트컷 표현식과 어드바이스 메서드를 한 정책 모듈에 모읍니다.
Spring은 애스펙트 빈을 읽어 어드바이저로 만들고 적용 대상 빈을 프록시로 감쌉니다.
애노테이션을 클래스에 붙였다는 사실만으로 활성화되지 않으며 자동 프록시 생성기와 Spring 빈 등록, 프록시 경로 호출이 모두 필요합니다.
이름 있는 포인트컷
긴 실행 표현식을 어드바이스마다 복사하면 패키지 변경 때 일부만 수정됩니다.
의미 있는 메서드 이름의 빈 포인트컷 메서드로 추출하고 작은 조건을 조합합니다.
applicationOperation()과 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);
}
}
}Throwable을 받는 이유는 진행 규칙을 보존하기 위해서입니다.
어드바이스가 검사 타입을 새로 선언해 애플리케이션 시그니처를 바꾸지는 않으며 같은 객체를 재전파합니다.
프록시·어드바이스 검증
AspectJ 애노테이션 지원을 활성화하고 애스펙트와 대상을 빈으로 등록합니다.
대상을 new로 직접 호출하면 메트릭이 나오지 않습니다.
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));
}
}
}metric=post.weekly outcome=success elapsed=(0 이상의 값)
postCount=12
proxy=true테스트에서는 콘솔 텍스트 대신 기록 수집 대상을 주입해 작업, 결과, 개수를 검증하는 편이 안정적입니다.
애노테이션·프록시 유형
인터페이스 메서드에 애노테이션을 둘지 구현에 둘지 섞으면 포인트컷 바인딩이 프록시 방식과 가장 구체적인 메서드 해결에 영향을 받을 수 있습니다.
애플리케이션 규칙 애노테이션이면 인터페이스, 구현 세부 관찰이면 구체 메서드처럼 원칙을 정하고 JDK·클래스 프록시 양쪽 테스트를 둡니다.
메타 애노테이션을 만들어 여러 정책을 한 번에 붙일 수 있지만 트랜잭션과 재시도처럼 순서가 중요한 관심사를 숨기지 않습니다.
합성 애노테이션의 속성 별칭과 기본값을 명확히 합니다.
패키지 전체를 대상으로 하는 실행 포인트컷은 신규 빈도 자동 포함합니다.
보안 애스펙트라면 누락 방지 장점이 있지만 원하지 않은 관리 또는 상태 메서드까지 적용될 수 있어 부정 테스트가 필요합니다.
단일 정책 애스펙트
로깅, 재시도, 인가 어드바이스를 한 클래스에 모으면 이름 기반 포인트컷을 공유한다는 이유로 변경 원인이 섞입니다.
각각 별도 애스펙트와 순서를 두고 통합 테스트에서 체인을 검증합니다.
재사용 가능한 포인트컷 라이브러리는 의존성 방향을 애플리케이션 패키지 밖으로 새지 않게 관리합니다.
애스펙트 내부에서 어드바이스가 적용된 다른 빈을 호출하면 추가 프록시 체인과 트랜잭션이 생길 수 있습니다.
메트릭 수집 대상은 가능하면 어드바이스가 없는 단순한 인프라로 두고 재귀를 방지합니다.
연습 문제
@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() { }
}메트릭 결과를 예외의 단순 이름 그대로 쓰면 하위 타입 증가로 카디널리티가 커질 수 있어 제한된 분류로 바꿉니다.