AOP 핵심 용어
Spring AOP의 핵심 용어를 호출 체인에 배치하고 AspectJ와 프록시 AOP의 범위를 구분합니다.
AOP 용어를 정의만 외우면 스택 트레이스에서 무슨 일이 일어났는지 설명하지 못합니다.
PostReportService.weekly() 호출 하나에 각 개념을 배치하면 관계가 분명해집니다.
대상은 업무 객체, 프록시는 클라이언트가 가진 대리 객체, 조인 지점은 메서드 실행, 포인트컷은 선택 규칙, 어드바이스는 실행될 부가 기능입니다.
AOP TERMS · ONE PROXY CALL
프록시 참조와 대상 사이에 선택된 Advisor가 연결된다
조인 지점은 지원되는 메서드 실행이고 Pointcut은 적용 조건, Advice는 동작입니다. Advisor는 조건과 동작을 연결하며 Aspect는 이 정책들의 정의를 모읍니다.
- Client → Proxy 안의 Advisor → Target
클라이언트가 최종 프록시 참조를 호출합니다. Pointcut에 맞는 Advice가 연결되어 대상 전후를 감싸며, 지원 메서드 실행 자체가 join point입니다.
설정 시점 · Aspect → Advisor
@Aspect 메타데이터를 Advisor로 변환하고 대상에 적합한 체인을 프록시에 연결합니다. 호출별 시작·결과는 지역 변수에 둡니다.
프록시와 위빙의 범위
직접 new·원본 참조·this 내부 호출은 프록시 체인을 통과하지 않습니다. 생성자·필드·내부 호출의 더 넓은 조인 지점은 AspectJ 위빙을 별도로 평가합니다.
Around는 proceed 0/1/N회와 인자·반환 계약을 소유합니다. 애노테이션 존재에 더해 실제 프록시·advisor·대상 호출 수를 확인합니다.
조인 포인트·포인트컷
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는 진단할 때만 사용합니다.
프록시가 여러 애스펙트를 가지면 인터셉터 체인은 어드바이저 순서에 따라 중첩됩니다.
가장 바깥 어드바이스가 처음 실행되고 마지막에 종료됩니다.
예시 클라이언트가 컨테이너에서 받은 프록시로 weekly를 호출했다고 가정합니다. 선택된 Advisor의 Pointcut이 이 실행에 맞으면 Advice가 먼저 들어가고, proceed가 원 Target의 weekly를 실행한 뒤 반환 또는 예외가 체인을 거슬러 올라옵니다. Proxy와 Target은 이 전달에서 서로 다른 참조이고 Join point는 그 위에 추가된 객체가 아니라 관찰하는 메서드 실행입니다.
Aspect는 이 정책을 정의하는 구성이고 Advisor는 조건과 동작을 연결한 항목입니다. 하나의 Aspect가 여러 Advice를 정의할 수 있으므로 Aspect 개수·Advisor 개수·대상 호출 수를 서로 같은 숫자로 세지 않습니다. 앞 절의 콜백은 이 관계 중 Around 동작만 수동으로 흉내 냅니다.
트랜잭션과 재시도 순서가 물리 트랜잭션 수를 바꾸는 이유입니다.
애스펙트·어드바이저
@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 어드바이스에서만 진행을 제공합니다.
호출하지 않으면 대상이 실행되지 않고, 다른 인자 배열로 진행하면 입력을 바꿉니다.
캐시나 인가처럼 의도한 경우가 아니면 정확히 한 번 원본 인자로 진행합니다.
프록시 체인 관찰
컨텍스트 테스트에서 대상 클래스, 프록시 여부, 어드바이저 수, 포인트컷 일치 메서드, 어드바이스 카운터, 대상 카운터를 확인합니다.
용어 다이어그램과 런타임 개수가 맞아야 합니다.
애노테이션 존재만 검사하면 위빙이 활성화되지 않은 구성을 놓칩니다.
독립 확인: 프록시를 통과한 공개 outer와 inner가 같은 대상 안에 있고, 두 메서드는 포인트컷과 맞습니다. outer의 업무 본문은 inner를 this.inner()로 정확히 한 번 호출합니다. Around는 진입마다 proceed를 한 번 하고 실패하지 않습니다. 외부에서 proxy.outer() 한 번을 부르면 업무 메서드 실행은 몇 번, Advice 진입은 몇 번인가요? 외부에서 proxy.inner()를 따로 한 번 더 부른 경우까지 나눠 세세요. 전체 AspectJ 위빙은 사용하지 않습니다.
풀이 확인
첫 호출의 업무 실행은 outer 1회와 inner 1회지만 Advice는 외부의 proxy.outer 진입 1회입니다. this.inner는 같은 대상 참조에서 이어지므로 새로운 프록시 진입을 만들지 않습니다. proxy.inner를 외부에서 한 번 더 부르면 그 호출에는 Advice가 한 번 더 붙어 총 업무 실행 3회·Advice 진입 2회입니다. 포인트컷이 맞는다는 조건과 프록시 경로를 실제 통과했다는 조건을 함께 확인해야 합니다.
연습 문제
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”라는 전제를 테스트 이름에 포함합니다.