ProxyFactory 구성
ProxyFactory가 포인트컷과 어드바이스로 인터셉터 체인을 만드는 과정을 실행하고 적용 조건과 여러 어드바이저의 순서를 설계합니다.
JDK 핸들러에 메서드 선택과 부가 기능을 같이 쓰면 재사용이 어렵습니다.
Spring AOP는 “어디에 적용할지”를 Pointcut, “무엇을 할지”를 Advice로 분리하고 둘을 어드바이저로 묶습니다.
ProxyFactory는 대상의 인터페이스와 클래스 조건을 보고 프록시를 만들며 호출 때 어드바이저 체인을 실행합니다.
ProxyFactory 구성
pointcut 일치 시 Advice 1/Target 1, 불일치 시 Advice 0/Target 1인 실제 호출 횟수 변화가 있다.
POINTCUT · PER-METHOD CHAIN + TARGET
메서드별 체인이 달라도 대상에는 도달할 수 있다
Pointcut은 적용 위치, Advice는 동작, Advisor는 둘의 결합입니다. 이 교재의 관찰 advice는 proceed를 한 번 호출하고 register에만 일치합니다.
- register · 일치
관찰 advice가 proceed를 한 번 호출합니다. advice 1회·target 1회입니다.
- find · 불일치
이 관찰 advice를 생략하지만 대상은 실행됩니다. advice 0회·target 1회입니다.
Pointcut · class ∩ method
클래스 필터와 메서드 매처의 교집합입니다. 예제 startsWith("board")는 문자열 접두 비교이므로 정확한 패키지 경계가 필요하면 추가로 좁힙니다.
proceed와 복수 Advisor
일반 advice는 0회로 차단하거나 여러 번 호출할 수 있습니다. register/find·Object 메서드·다른 패키지의 적용 수를 확인합니다. retry/transaction 순서는 시도별 물리 경계 요구에 맞게 정합니다.
프록시 생성 여부만으로 적용 범위를 증명하지 않습니다. advisor 수·순서와 메서드별 advice/target 개수를 함께 확인합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 원래 AdvisorDemo의 관찰 advice는 proceed를 한 번 호출한다. register 일치는 Advice 1/Target 1이며 find 불일치는 이 Advice 0/Target 1이다. 두 입력은 각각의 호출 경로이며 find의 대상이 생략되지 않는다. 일반적인 around Advice가 proceed를 0번 또는 여러 번 호출하는 계약은 다른 조건이다.
- register는 pointcut과 일치해 advice 한 번을 거쳐 target 한 번 실행된다. find는 pointcut 불일치로 이 advice를 생략하지만 target 한 번 실행된다. 일반 around advice의 proceed 횟수는 별도 계약이다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | ProxyFactory 구성 |
| 6 | 원래 AdvisorDemo의 관찰 advice는 proceed를 한 번 호출한다. register 일치는 Advice 1/Target 1이며 find 불일치는 이 Advice 0/Target 1이다. 두 입력은 각각의 호출 경로이며 find의 대상이 생략되지 않는다. 일반적인 around Advice가 proceed를 0번 또는 여러 번 호출하는 계약은 다른 조건이다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 두 독립 입력 register와 find 호출을 각 비교 슬롯에 둡니다. | register: pointcut 일치 · find: 불일치 | 같은 proxy의 선택 조건이 달라집니다. |
| advice 선택 register만 이 advice를 실행하고 find도 target은 호출합니다. | register: Advice 1 · find: Advice 0 | 일치 여부가 advice 수만 바꿉니다. |
| target 결과 관찰 advice가 proceed를 한 번 호출하는 조건에서 둘 다 target 한 번입니다. | register: Target 1 · find: Target 1 | 불일치가 target 생략을 뜻하지 않음을 확인합니다. |
ProxyFactory 구성
pointcut 일치 시 Advice 1/Target 1, 불일치 시 Advice 0/Target 1인 실제 호출 횟수 변화가 있다.
POINTCUT · PER-METHOD CHAIN + TARGET
메서드별 체인이 달라도 대상에는 도달할 수 있다
Pointcut은 적용 위치, Advice는 동작, Advisor는 둘의 결합입니다. 이 교재의 관찰 advice는 proceed를 한 번 호출하고 register에만 일치합니다.
- register · 일치
관찰 advice가 proceed를 한 번 호출합니다. advice 1회·target 1회입니다.
- find · 불일치
이 관찰 advice를 생략하지만 대상은 실행됩니다. advice 0회·target 1회입니다.
Pointcut · class ∩ method
클래스 필터와 메서드 매처의 교집합입니다. 예제 startsWith("board")는 문자열 접두 비교이므로 정확한 패키지 경계가 필요하면 추가로 좁힙니다.
proceed와 복수 Advisor
일반 advice는 0회로 차단하거나 여러 번 호출할 수 있습니다. register/find·Object 메서드·다른 패키지의 적용 수를 확인합니다. retry/transaction 순서는 시도별 물리 경계 요구에 맞게 정합니다.
프록시 생성 여부만으로 적용 범위를 증명하지 않습니다. advisor 수·순서와 메서드별 advice/target 개수를 함께 확인합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 원래 AdvisorDemo의 관찰 advice는 proceed를 한 번 호출한다. register 일치는 Advice 1/Target 1이며 find 불일치는 이 Advice 0/Target 1이다. 두 입력은 각각의 호출 경로이며 find의 대상이 생략되지 않는다. 일반적인 around Advice가 proceed를 0번 또는 여러 번 호출하는 계약은 다른 조건이다.
- register는 pointcut과 일치해 advice 한 번을 거쳐 target 한 번 실행된다. find는 pointcut 불일치로 이 advice를 생략하지만 target 한 번 실행된다. 일반 around advice의 proceed 횟수는 별도 계약이다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | ProxyFactory 구성 |
| 6 | 원래 AdvisorDemo의 관찰 advice는 proceed를 한 번 호출한다. register 일치는 Advice 1/Target 1이며 find 불일치는 이 Advice 0/Target 1이다. 두 입력은 각각의 호출 경로이며 find의 대상이 생략되지 않는다. 일반적인 around Advice가 proceed를 0번 또는 여러 번 호출하는 계약은 다른 조건이다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 두 독립 입력 register와 find 호출을 각 비교 슬롯에 둡니다. | register: pointcut 일치 · find: 불일치 | 같은 proxy의 선택 조건이 달라집니다. |
| advice 선택 register만 이 advice를 실행하고 find도 target은 호출합니다. | register: Advice 1 · find: Advice 0 | 일치 여부가 advice 수만 바꿉니다. |
| target 결과 관찰 advice가 proceed를 한 번 호출하는 조건에서 둘 다 target 한 번입니다. | register: Target 1 · find: Target 1 | 불일치가 target 생략을 뜻하지 않음을 확인합니다. |
포인트컷 교집합
클래스 필터가 거절하면 메서드 매처를 볼 필요가 없습니다.
정적 매처는 Method와 대상 클래스만 사용해 프록시 생성 단계에서 체인을 캐시하기 좋습니다.
인자 값까지 보는 런타임 매처는 호출마다 비용이 들고 정책이 데이터에 의존합니다.
메서드 이름 접두사만으로 save를 선택하면 다른 제한된 컨텍스트의 메서드에도 적용될 수 있습니다.
패키지, 애노테이션, 타입, 시그니처를 조합해 의도를 좁힙니다.
업무 파라미터 값으로 인가를 구현하는 것은 별도 보안 인터셉터가 더 명시적입니다.
package board.advisor;
import java.lang.reflect.Method;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.MethodMatcher;
import org.springframework.aop.Pointcut;
public final class BoardPointcut implements Pointcut {
@Override
public ClassFilter getClassFilter() {
return type -> type.getPackageName().startsWith("board");
}
@Override
public MethodMatcher getMethodMatcher() {
return new MethodMatcher() {
@Override
public boolean matches(Method method, Class<?> targetClass) {
return method.getName().startsWith("register");
}
@Override
public boolean isRuntime() {
return false;
}
@Override
public boolean matches(
Method method,
Class<?> targetClass,
Object[] arguments
) {
return matches(method, targetClass);
}
};
}
}포인트컷 객체는 무상태하게 만들면 여러 어드바이저에서 안전하게 재사용할 수 있습니다.
패키지 이동과 메서드 이름 변경은 적용 범위를 바꾸므로 포인트컷 테스트를 아키텍처 테스트처럼 유지합니다.
예시 원 클래스 필터는 패키지 문자열의 접두사를 검사합니다. board.command뿐 아니라 boardwalk.command도 startsWith("board")는 true입니다. registerPost뿐 아니라 registerAudit도 메서드 접두사와 맞습니다. 클래스 조건이 참이고 메서드 조건도 참이어야 이 어드바이스가 붙는다는 교집합을 각 입력에서 따로 계산하세요. 실제 board 패키지와 하위 패키지만 원한다면 패키지 이름이 board와 같거나 board.로 시작하는지를 나눠 검사하는 식으로 범위를 좁힐 수 있습니다. 원 발췌가 이미 그 경계까지 검사하는 것은 아닙니다.
MethodInterceptor 흐름
주변 어드바이스는 MethodInvocation.proceed()를 호출해야 다음 인터셉터 또는 대상으로 이동합니다.
호출하지 않으면 접근 거부나 캐시 적중처럼 체인을 종료합니다.
두 번 호출하면 대상도 두 번 실행될 수 있어 재시도 의도가 아니라면 금지합니다.
package board.advisor;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
public final class AdvisorDemo {
public interface Command {
long register(String title);
long find(long id);
}
static final class Target implements Command {
@Override
public long register(String title) {
return 41L;
}
@Override
public long find(long id) {
return id;
}
}
public static void main(String[] args) {
MethodInterceptor advice = invocation -> {
System.out.println("advised=" + invocation.getMethod().getName());
return invocation.proceed();
};
var advisor = new DefaultPointcutAdvisor(new BoardPointcut(), advice);
var factory = new ProxyFactory(new Target());
factory.addAdvisor(advisor);
Command proxy = (Command) factory.getProxy();
System.out.println("created=" + proxy.register("advisor"));
System.out.println("found=" + proxy.find(7L));
}
}advised=register
created=41
found=7find는 대상에 도달하지만 포인트컷과 맞지 않아 어드바이스 메시지가 없습니다.
프록시 자체는 빈의 모든 호출을 받되 메서드별 체인이 달라집니다.
관찰 결과는 register에서 어드바이스 1회·대상 1회, find에서 어드바이스 0회·대상 1회입니다.
이 개수가 Pointcut 적용 범위를 실행 수준에서 증명하며, 프록시 생성 여부만 확인하는 테스트보다 잘못된 메서드 일치를 직접 드러냅니다.
복수 어드바이저 순서
트랜잭션, 인가, 재시도, 메트릭 어드바이저가 함께 적용되면 순서가 동작을 바꿉니다.
재시도가 트랜잭션 바깥이면 시도마다 새 트랜잭션을 만들 수 있고, 안쪽이면 롤백 전용 트랜잭션을 반복할 수 있습니다.
인가는 비용이 큰 작업보다 먼저 실행되어야 합니다.
Ordered 또는 어드바이저 등록 순서를 명시하고 체인 테스트를 둡니다.
직접 ProxyFactory에 addAdvisor로 추가하는 예제는 등록 위치를 명시해 추적합니다. Advised API의 위치 지정 추가와 getAdvisors를 함께 확인하고, 숫자 order가 있다는 사실만으로 이 수동 팩토리가 자동 생성기의 정렬까지 수행했다고 가정하지 않습니다.
순서 숫자를 여러 모듈에서 임의로 정하면 충돌하므로 의미 있는 상수 범위를 관리합니다.
동일 순서의 상대 순서에 의존하지 않습니다.
호출 전, 정상 반환 이후, 예외 이후 어드바이스는 표현이 간결하지만 주변 어드바이스가 가장 일반적입니다.
반환과 예외를 모두 다루는 메트릭에는 try/catch/finally가 명시적인 MethodInterceptor가 적합합니다.
ProxyFactory 선택
인터페이스가 있으면 기본적으로 JDK 프록시가 가능하고 proxyTargetClass로 클래스 프록시를 강제할 수 있습니다.
optimize, opaque, exposeProxy, 고정 구성 같은 옵션은 필요한 이유가 없으면 기본값을 유지합니다.
exposeProxy는 내부 호출 우회를 가능하게 하지만 애플리케이션 코드를 AOP API에 묶습니다.
대상 소스를 바꾸는 고급 프록시는 요청별 대상이나 풀링을 구현할 수 있지만 싱글톤 생명 주기, 상태, 스레드 안전성이 복잡해집니다.
일반 서비스는 고정 싱글톤 대상을 사용합니다.
프록시를 직렬화하거나 구체 구현 클래스로 찾지 않습니다.
진단할 때는 어드바이저 목록과 대상 클래스를 Spring API로 확인합니다.
포인트컷·동작 테스트
포인트컷 테스트는 대표 Method 객체를 가져와 register는 참, find와 다른 패키지 클래스는 거짓인지 빠르게 검사합니다.
동작 테스트는 프록시를 실제 호출해 어드바이스 수, 대상 수, 예외 동일성, 순서를 확인합니다.
메서드 추가 때 포인트컷이 너무 넓게 일치하는 것도 회귀입니다.
부정 픽스처를 충분히 둡니다.
생성된 브리지 메서드와 합성 메서드가 보이면 가장 구체적인 메서드 해결을 Spring 유틸리티에 맡깁니다.
연습 문제
@Measured가 붙은 공개 애플리케이션 메서드만 선택하는 포인트컷과 타이머 어드바이스를 만드세요.
객체 메서드와 애노테이션 없는 메서드를 제외하고, 실패에서도 지속 시간이 한 번 기록되며 원본 예외가 유지되는 테스트를 작성하세요.
해설 보기
애노테이션 탐색은 인터페이스 메서드와 구현 메서드 위치를 고려해야 합니다.
간단한 구체 클래스 기준 예시는 다음과 같습니다.
package board.advisor;
import java.lang.reflect.Method;
import org.springframework.aop.support.StaticMethodMatcherPointcut;
import org.springframework.core.annotation.AnnotatedElementUtils;
public final class MeasuredPointcut extends StaticMethodMatcherPointcut {
@Override
public boolean matches(Method method, Class<?> targetClass) {
try {
Method specific = targetClass.getMethod(
method.getName(), method.getParameterTypes());
return AnnotatedElementUtils.hasAnnotation(specific, Measured.class);
} catch (NoSuchMethodException failure) {
return false;
}
}
public @interface Measured { }
}실제 애노테이션에는 @Retention(RUNTIME)과 @Target(METHOD)를 선언해야 합니다.
원 코드 블록의 최소 Measured 선언만 그대로 쓰면 기본 retention은 CLASS여서 런타임 애노테이션 탐색에 남지 않습니다. MeasuredPointcut 안의 그 선언을 다음처럼 보완하는 위치를 먼저 확인하세요. 전체 메서드 선택과 시간 기록의 구현은 별개의 책임입니다.
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@java.lang.annotation.Target(java.lang.annotation.ElementType.METHOD)
public @interface Measured { }또한 위 MeasuredPointcut은 targetClass의 공개 메서드에서 애노테이션을 찾지만 애플리케이션 패키지나 Object 메서드 제외를 구현하지 않았습니다. 애노테이션이 붙은 toString 재정의도 그대로는 선택될 수 있습니다. 연습의 완성 조건을 만족하려면 클래스 필터를 원하는 패키지로 좁히고 equals(Object)·hashCode()·toString()의 시그니처를 명시적으로 제외해야 합니다. 인자를 받는 업무 메서드가 이름만 같다는 이유로 함께 제외되지 않도록 이름과 매개변수 목록을 대조합니다.
타이머의 주변 동작은 시작값을 읽고 try에서 proceed를 한 번 호출한 뒤 finally에서 경과값을 한 번 기록하는 순서입니다. 기록기가 실패하지 않는 조건이라면 정상 반환과 대상 실패 모두 기록 한 번이며 원 결과·실패가 그대로 전달됩니다. 기록기가 새 예외를 던지는 문제는 12-3에서 다룬 별도 경계입니다.
독립 확인: 보완한 RUNTIME 애노테이션을 쓰되 원 MeasuredPointcut의 matches만 그대로 두었습니다. boardwalk.command의 @Measured 공개 register, board.command의 애노테이션 없는 find, @Measured 공개 toString 재정의는 각각 일치하나요? 애플리케이션 패키지와 Object 제외 조건을 추가하면 첫째·셋째가 어떻게 달라져야 하나요?
풀이 확인
원 matches는 패키지 조건이 없어 첫째와 셋째가 참이고, 애노테이션 없는 둘째는 거짓입니다. board 및 하위 패키지로 제한하면 boardwalk는 거짓이어야 하고, Object 메서드 시그니처 제외를 더하면 toString도 거짓이어야 합니다. 좋은 부정 fixture는 단순히 프록시가 존재하는지보다 잘못 넓어진 적용 범위를 드러냅니다. 원 BoardPointcut의 이름 기반 규칙과 이 애노테이션 기반 규칙은 서로 다른 선택 기준입니다.