AOP 핵심 용어
Spring AOP의 핵심 용어를 호출 체인에 배치하고 AspectJ와 프록시 AOP의 범위를 구분합니다.
AOP 용어를 정의만 외우면 스택 트레이스에서 무슨 일이 일어났는지 설명하지 못합니다.
PostReportService.weekly() 호출 하나에 각 개념을 배치하면 관계가 분명해집니다.
대상은 업무 객체, 프록시는 클라이언트가 가진 대리 객체, 조인 지점은 메서드 실행, 포인트컷은 선택 규칙, 어드바이스는 실행될 부가 기능입니다.
조인 포인트·포인트컷
Spring 프록시 AOP가 지원하는 핵심 조인 지점은 Spring 빈 메서드 실행입니다.
Pointcut 표현식은 그중 패키지, 타입, 메서드, 애노테이션, 인자 조건에 맞는 부분 집합을 만듭니다.
어드바이스는 선택된 호출에서 이전·이후·around 동작을 수행합니다.
AspectJ 언어 자체는 생성자 호출, 필드 접근, 예외 핸들러 등 더 넓은 조인 지점 모델을 가집니다.
@Aspect 애노테이션을 쓴다고 Spring 프록시가 자동으로 모든 AspectJ 조인 지점을 지원하는 것은 아닙니다.
package board.aopmodel;
import java.util.List;
public final class JoinPointInventory {
record Candidate(String description, boolean springProxySupported) { }
public static void main(String[] args) {
var candidates = List.of(
new Candidate("Spring bean public method execution", true),
new Candidate("same target self invocation", false),
new Candidate("entity field read", false),
new Candidate("constructor call", false));
long supported = candidates.stream()
.filter(Candidate::springProxySupported)
.count();
System.out.printf("candidates=%d proxyJoinPoints=%d%n",
candidates.size(), supported);
}
}candidates=4 proxyJoinPoints=1지원하지 않는 지점이 필요하면 설계를 메서드 경계로 바꾸거나 전체 AspectJ 위빙의 빌드·디버그 비용을 평가합니다.
expected join point = entity field read
Spring proxy supported join point = bean method execution
matched advisors = 0
target field changed = true
audit event emitted = false
result = UNSUPPORTED_JOIN_POINT_ASSUMPTION이 반례에서 포인트컷 문법은 오류가 없어도 프록시가 제공하지 않는 조인 지점라 어드바이스가 0회입니다.
용어 정의를 런타임 지원 범위와 연결하지 않으면 보안 감사 누락이 조용히 발생합니다.
대상·프록시 동일성
BeanPostProcessor가 대상을 감싸 프록시 참조를 컨테이너에 돌려줍니다.
다른 빈은 프록시를 주입받아야 어드바이스가 실행됩니다.
대상을 직접 보관한 코드, new로 만든 객체, 자기-호출은 체인 밖입니다.
JDK 프록시면 노출된 타입이 인터페이스이고 클래스 프록시면 대상 하위 타입입니다.
애플리케이션은 런타임에 생성된 클래스 이름에 의존하지 않고 포트나 공개 대상 규칙으로 호출합니다.
AopUtils.getTargetClass는 진단할 때만 사용합니다.
프록시가 여러 애스펙트를 가지면 인터셉터 체인은 어드바이저 순서에 따라 중첩됩니다.
가장 바깥 어드바이스가 처음 실행되고 마지막에 종료됩니다.
트랜잭션과 재시도 순서가 물리 트랜잭션 수를 바꾸는 이유입니다.
애스펙트·어드바이저
@Aspect 클래스에는 이름 기반 포인트컷과 여러 어드바이스 메서드를 둘 수 있습니다.
Spring은 메타데이터를 어드바이저 형태로 변환해 자동 프록시 생성기에 제공합니다.
하나의 애스펙트가 너무 많은 정책을 소유하면 클래스만 분리했을 뿐 결합도는 남습니다.
package board.aopmodel;
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 OperationAspect {
@Pointcut("execution(public * board.application..*(..))")
public void applicationOperation() { }
@Around("applicationOperation()")
public Object observe(ProceedingJoinPoint joinPoint) throws Throwable {
String operation = joinPoint.getSignature().toShortString();
long started = System.nanoTime();
try {
return joinPoint.proceed();
} finally {
long elapsed = System.nanoTime() - started;
System.out.printf("operation=%s elapsedPositive=%s%n",
operation, elapsed >= 0);
}
}
}애스펙트 객체는 싱글톤인 경우가 많으므로 호출별 상태를 필드에 저장하지 않습니다.
조인 지점과 로컬 변수에 보관하고 공유 의존성은 스레드-안전해야 합니다.
위빙
Spring AOP에서는 런타임 컨테이너가 프록시를 만들어 어드바이저를 연결합니다.
컴파일 시점 위빙은 컴파일러가 바이트코드에 애스펙트 동작을 반영하고 적재 시점 위빙은 클래스 로더 단계에서 변환합니다.
각각 지원 조인 지점, 시작 방식, 도구 구성, 디버깅 방법이 다릅니다.
프록시 방식은 Spring 빈 메서드에 제한되지만 빌드가 단순하고 트랜잭션·보안 같은 인프라에 널리 쓰입니다.
위빙은 도메인 객체와 자기 호출까지 가능하지만 IDE, 테스트, 에이전트, 네이티브 이미지와의 관계를 관리해야 합니다.
같은 프로젝트에서 두 방식을 섞으면 한 애스펙트가 이중 적용되는지 검토합니다.
선택 이유와 적용 범위를 아키텍처 문서에 남깁니다.
JoinPoint 관찰 정보
시그니처, 대상, 프록시, 인자를 읽을 수 있습니다.
인자 원문을 로그하면 비밀번호나 게시글 본문이 노출될 수 있어 타입과 허용된 ID만 추출합니다.
set$ 같은 생성된 메서드나 브리지 메서드가 보이면 포인트컷을 공개 애플리케이션 타입으로 좁힙니다.
ProceedingJoinPoint는 around 어드바이스에서만 진행을 제공합니다.
호출하지 않으면 대상이 실행되지 않고, 다른 인자 배열로 진행하면 입력을 바꿉니다.
캐시나 인가처럼 의도한 경우가 아니면 정확히 한 번 원본 인자로 진행합니다.
프록시 체인 관찰
컨텍스트 테스트에서 대상 클래스, 프록시 여부, 어드바이저 수, 포인트컷 일치 메서드, 어드바이스 카운터, 대상 카운터를 확인합니다.
용어 다이어그램과 런타임 개수가 맞아야 합니다.
애노테이션 존재만 검사하면 위빙이 활성화되지 않은 구성을 놓칩니다.
연습 문제
WeeklyReportService.weekly(long) 호출에 대상, 프록시, 조인 지점, 포인트컷, 어드바이스, 어드바이저, 애스펙트, 위빙을 각각 한 문장으로 매핑하세요.
new WeeklyReportService() 호출과 내부 호출에서 어느 항목이 사라지는지도 표시하세요.
해설 보기
Spring 컨텍스트가 만든 프록시를 호출할 때만 어드바이저 체인이 연결됩니다.
대상을 직접 생성하면 포인트컷 메타데이터가 있어도 자동 프록시 생성기가 볼 기회가 없습니다.
package board.aopmodel;
public final class CallPath {
public enum Entry { SPRING_PROXY, DIRECT_TARGET, SELF_INVOCATION }
public static boolean adviceRuns(Entry entry) {
return entry == Entry.SPRING_PROXY;
}
public static void main(String[] args) {
for (Entry entry : Entry.values()) {
System.out.println(entry + " advice=" + adviceRuns(entry));
}
}
}전체 AspectJ 위빙을 선택하면 이 결과가 달라질 수 있으므로 “Spring 프록시 AOP”라는 전제를 테스트 이름에 포함합니다.