런타임 포인트컷 지시자
execution·args·annotation·this·target이 보는 타입과 실행 시점의 차이를 실험하고 동적 조건의 노출과 비용을 제한합니다.
포인트컷 지시자는 모두 타입을 고르지만 보는 대상이 다릅니다.
execution의 파라미터는 메서드 시그니처, args는 런타임 인자, this는 프록시 객체, target은 실제 대상 객체를 기준으로 합니다.
이 차이를 모르면 JDK 프록시에서 구체 클래스 조건이 사라지거나 넓은 인자 매처가 예상 밖 하위 타입까지 잡습니다.
POINTCUT VIEWS · FOUR DISTINCT FACTS
선언·실제 인자·프록시·대상을 따로 본다
예: register(Command)에 SpecialCommand 값을 전달합니다. execution·args는 메서드와 값을, this·target은 서로 다른 객체를 기준으로 판단합니다.
execution · 선언 Method
register(Command)의 파라미터 선언은 Command입니다. 하위 타입 값을 전달해도 이 시그니처 자체가 바뀌지 않습니다.
args · 런타임 인자
실제 SpecialCommand 값의 대입 가능성을 봅니다. 기반 인자·null·가변 인자와 컬렉션 요소를 구분하고 호출별 매처 비용을 확인합니다.
this · 프록시 객체
JDK + this(ConcreteService): false
JDK 프록시는 노출 인터페이스 타입입니다. 클래스 프록시가 ConcreteService의 적격 하위 타입이면 this 조건은 맞을 수 있습니다.
target · 실제 대상 객체
target(ConcreteService): 대상이 해당 타입이면 true
JDK 방식에서도 실제 업무 대상을 봅니다. 프록시 방식 전환에 정책이 빠지지 않도록 두 방식을 대조합니다.
@annotation은 메서드 정책 메타데이터를 바인딩합니다. 안정적 작업 코드·마스킹 정책만 추출하며 본문·토큰·임의 toString을 기록하지 않습니다. 관찰 advice는 입력을 바꾸지 않습니다.
execution·args
메서드가 register(Command)로 선언되어 있고 런타임에 SpecialCommand가 들어오면 execution은 Command 선언을 보고 args는 SpecialCommand의 대입 가능성을 봅니다.
정적 매처는 프록시 생성 때 캐시할 수 있지만 인자 조건은 호출마다 검사될 수 있습니다.
package board.binding;
public final class ArgumentTypes {
public sealed interface Command permits Standard, Imported { }
public record Standard(String title) implements Command { }
public record Imported(String title, String source) implements Command { }
public static String declared(Command command) {
return command.getClass().getSimpleName();
}
public static void main(String[] args) {
Command value = new Imported("AOP", "csv");
System.out.printf("declared=Command runtime=%s%n", declared(value));
}
}declared=Command runtime=Imported업무 분기를 인자 포인트컷으로 숨기기보다 도메인 디스패치를 사용합니다.
인자는 특정 표시 요청의 관찰이나 레거시 어댑터 범위처럼 횡단 정책에 실제 런타임 타입이 필요한 경우로 제한합니다.
예시 원 value 변수와 declared 메서드의 매개변수는 Command로 선언됩니다. 저장된 객체는 Imported이고, getClass는 그 실제 객체의 클래스를 읽어 Imported를 반환합니다. 출력의 declared=Command 부분은 코드에 직접 적은 문자열이며 리플렉션으로 선언을 검사한 결과는 아닙니다. new Standard("AOP")로 값만 바꾸면 매개변수 선언은 그대로이고 출력의 runtime 부분만 Standard가 됩니다.
@annotation 바인딩
@annotation(audited)은 일치된 메서드 애노테이션 인스턴스를 어드바이스 파라미터로 전달할 수 있습니다.
애노테이션 값에서 안정적인 작업 코드와 마스킹 정책을 읽습니다.
대상 메서드와 인터페이스 중 어디에 붙이는지 일관되게 정합니다.
package board.binding;
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 Audited {
String operation();
boolean includeActor() default true;
}@within은 애노테이션이 붙은 타입 안의 메서드, @target은 런타임 대상 클래스 애노테이션, @args는 런타임 인자 타입 애노테이션을 봅니다.
비슷한 이름을 교체 가능하게 보지 말고 대상·메서드·인자 중 정책 책임 주체가 어디인지 선택합니다.
this·target
JDK 프록시는 인터페이스 타입이므로 this(ConcreteService)가 일치하지 않을 수 있지만 target(ConcreteService)는 실제 대상을 봅니다.
클래스 프록시는 프록시 자체가 대상 하위 타입이라 둘 다 맞을 수 있습니다.
프록시 방식 변경에 정책이 사라지지 않게 테스트합니다.
package board.binding;
import org.springframework.aop.framework.ProxyFactory;
public final class ProxyIdentityDemo {
interface ReportPort { int postCount(); }
static final class ReportTarget implements ReportPort {
@Override public int postCount() { return 12; }
}
public static void main(String[] args) {
var target = new ReportTarget();
var factory = new ProxyFactory(target);
ReportPort proxy = (ReportPort) factory.getProxy();
System.out.printf("thisIsPort=%s thisIsTarget=%s targetIsTarget=%s%n",
proxy instanceof ReportPort,
proxy instanceof ReportTarget,
target instanceof ReportTarget);
}
}JDK 프록시를 선택하면 두 번째 값은 false입니다.
구체 클래스가 final이라 클래스 프록시가 불가능한 상황에서도 포트 기반 포인트컷은 안정적입니다.
proxy implementation = JDK dynamic proxy
this(ReportTarget) matches = false
target(ReportTarget) matches = true
expected advice calls = 1
observed advice calls with this condition = 0
result = CONCRETE_THIS_POINTCUT_MISSED이 진단 형식은 구체 타입의 this 조건을 사용한 경우를 설명한 예시입니다. 위 코드는 객체의 타입만 출력하며 포인트컷 호출 횟수를 직접 측정하지 않습니다.
위 ProxyFactory 예제의 포트는 ReportPort이고 postCount에는 명령 인자가 없습니다. 앞 ArgumentTypes의 Command를 이 프록시의 인자처럼 끼워 넣지 마세요. 인터페이스 프록시를 사용한 경우 타입 출력은 thisIsPort=true·thisIsTarget=false·targetIsTarget=true입니다. 이 세 값에서 얻는 것은 노출 참조와 대상의 구별이며, postCount의 12나 Advice 개수를 위 main이 출력하는 것은 아닙니다.
구체 대상 정책이라면 target을 쓰거나 더 안정적인 포트·애노테이션 포인트컷으로 바꿉니다.
인자 바인딩 제한
어드바이스 파라미터로 명령을 받았다고 toString() 전체를 로그하지 않습니다.
레코드에는 비밀번호, 게시글 본문, 토큰이 포함될 수 있습니다.
허용 목록 추출기가 구성원 ID나 요청 ID 같은 내부 식별자만 가져오고 값 자체를 메트릭 태그로 넣지 않습니다.
null 인자는 타입 기반 인자 일치에 영향을 줍니다.
메서드 시그니처로 범위를 잡고 어드바이스 내부에서 null을 안전하게 다루는 편이 예측 가능할 수 있습니다.
가변 인자는 배열로 전달되며 컬렉션 요소 타입은 런타임의 제네릭 정보만으로 바로 일치 여부를 판단할 수 없습니다.
인자를 어드바이스에서 변경해 proceed(newArgs)로 전달하면 검증·인가 이전 값과 달라질 수 있습니다.
정규화는 명시적 어댑터나 애플리케이션 코드에서 수행하고 관찰 애스펙트는 값을 변경하지 않습니다.
동적 포인트컷 비용
런타임 인자 매처는 호출마다 조건식을 실행합니다.
아주 짧은 게터 수백만 건에 적용하면 어드바이스 본문보다 일치 비용이 커질 수 있습니다.
먼저 클래스와 메서드 정적 조건으로 후보를 좁힌 뒤 꼭 필요한 인자 조건만 추가합니다.
null과 하위 타입을 섞은 부정 픽스처도 둡니다.
args(SpecialCommand)가 기반 명령과 null에서는 거짓이고 의도한 하위 타입에서만 참인지, 애노테이션 바인딩 파라미터 이름이 포인트컷 변수와 정확히 같은지 컨텍스트 시작과 호출 개수로 확인합니다.
바인딩 이름 오타는 애스펙트 생성 실패로 빠르게 드러내야 하며 예외를 삼키고 어드바이저 없이 시작해서는 안 됩니다.
JIT 준비 작업, 프록시 체인, 객체 할당을 포함한 작업 부하 벤치마크로 판단합니다.
표현식 자체 파싱은 시작할 때 일어나더라도 런타임 바인딩과 애노테이션 해결 비용은 남을 수 있습니다.
지시자별 프록시 검증
JDK 프록시와 클래스 프록시 컨텍스트를 각각 만들고 this·대상 일치 개수를 비교합니다.
선언된 기반 인자와 런타임 하위 타입, 애노테이션 위치, null 입력을 표로 고정합니다.
결과가 프레임워크 업그레이드에서 달라지면 보안이나 트랜잭션 적용 누락으로 이어지는지 평가합니다.
연습 문제
@Audited 애플리케이션 메서드에서 행위자 ID를 가진 명령만 바인딩하는 포인트컷을 설계하세요.
JDK 프록시에서도 적용되고 명령 본문은 로그하지 않으며 null 행위자는 대상 호출 전 명확한 실패로 처리해야 합니다.
해설 보기
메서드 애노테이션으로 정적 범위를 잡고 어드바이스가 명령 인터페이스를 파라미터로 바인딩하면 구체 구현 이름에 덜 의존합니다.
package board.binding;
public interface ActorCommand {
long actorId();
static long requireActor(ActorCommand command) {
if (command == null || command.actorId() < 1) {
throw new IllegalArgumentException("valid actor is required");
}
return command.actorId();
}
}인가 자체는 전용 보안 정책이 수행하고 감사 애스펙트는 그 결과 코드만 기록하도록 책임을 나눕니다.
해설의 actorId는 원시 long이므로 그 값 자체가 null일 수는 없습니다. 원 연습의 null 행위자 입력은 명령 객체의 null 또는 유효하지 않은 ID로 구체화해서 시험합니다. requireActor가 대상 전에 실행된다는 계약을 지키려면 @Audited와 메서드 시그니처로 진입 범위를 고정하고, 어드바이스 안에서 nullable 명령을 안전하게 꺼내 검사하는 경로가 필요합니다. 타입 기반 args 조건이 null 호출을 제외한 뒤에는 그 어드바이스의 검사도 실행되지 않으므로, 바인딩만 선언했다고 모든 거부가 보장되지는 않습니다.
독립 확인: 세 입력 모두 같은 @Audited 메서드 범위에 들어오고, 어드바이스가 proceed 전에 requireActor를 정확히 한 번 호출한다고 가정합니다. A는 null 명령, B는 actorId=0인 명령, C는 actorId=7인 명령입니다. 검사 결과와 대상 호출 수를 각각 구하세요. 정책 판단·기록기는 실패하지 않으며 본문 전체를 출력하지 않습니다.
풀이 확인
A는 첫 null 조건, B는 ID < 1 조건에서 같은 종류의 명확한 입력 실패를 내고 대상 호출은 각각 0회입니다. C는 7을 반환하여 후속 인가가 허용한 경우에만 대상 호출 1회로 이어집니다. 7이라는 양수 검사는 입력 유효성일 뿐 소유권이나 권한 허용을 대신하지 않습니다. 세 경우를 모두 어드바이스가 받는다는 전제가 빠지면 거부 개수도 이 표로 보장할 수 없습니다.