본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
13장 : Spring AOP

런타임 포인트컷 지시자

인자와 실행 파라미터 패턴의 런타임·시그니처 차이, 애노테이션 바인딩 위치, this 프록시 타입과 대상 실제 타입을 실험하고 민감 인자 노출과 동적 매처 비용을 제한합니다.

포인트컷 지시자는 모두 타입을 고르지만 보는 대상이 다릅니다. execution의 파라미터는 메서드 시그니처, args는 런타임 인자, this는 프록시 객체, target은 실제 대상 객체를 기준으로 합니다. 이 차이를 모르면 JDK 프록시에서 구체 클래스 조건이 사라지거나 넓은 인자 매처가 예상 밖 하위 타입까지 잡습니다.


execution·args

메서드가 register(Command)로 선언되어 있고 런타임에 SpecialCommand가 들어오면 실행은 Command를 보고 인자는 SpecialCommand의 대입 가능성을 봅니다. 정적 매처는 프록시 생성 때 캐시할 수 있지만 인자 조건은 호출마다 검사될 수 있습니다.

src/main/java/board/binding/ArgumentTypes.java
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));
    }
}
ArgumentTypes 실행
declared=Command runtime=Imported

업무 분기를 인자 포인트컷으로 숨기기보다 도메인 디스패치를 사용합니다. 인자는 특정 표시 요청의 관찰이나 레거시 어댑터 범위처럼 횡단 정책에 실제 런타임 타입이 필요한 경우로 제한합니다.

@annotation 바인딩

@annotation(audited)은 일치된 메서드 애노테이션 인스턴스를 어드바이스 파라미터로 전달할 수 있습니다. 애노테이션 값에서 안정적인 작업 코드와 마스킹 정책을 읽습니다. 대상 메서드와 인터페이스 중 어디에 붙이는지 일관되게 정합니다.

src/main/java/board/binding/Audited.java
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)는 실제 대상을 봅니다. 클래스 프록시는 프록시 자체가 대상 하위 타입이라 둘 다 맞을 수 있습니다. 프록시 방식 변경에 정책이 사라지지 않게 테스트합니다.

src/main/java/board/binding/ProxyIdentityDemo.java
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이라 클래스 프록시가 불가능한 상황에서도 포트 기반 포인트컷은 안정적입니다.

this·target pointcut 불일치 실패
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

이 실패는 대상 코드 문제나 애노테이션 누락이 아니라 프록시 객체의 타입을 잘못 기준으로 삼은 결과입니다. 구체 대상 정책이라면 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();
    }
}

인가 자체는 전용 보안 정책이 수행하고 감사 애스펙트는 그 결과 코드만 기록하도록 책임을 나눕니다.