실행·범위 포인트컷
실행의 modifier·반환·declaring 타입·메서드·파라미터 패턴을 분해하고 within의 타입 범위와 차이를 실제 Method 일치로 확인하며, 과대·과소 적용을 부정 픽스처로 막습니다.
포인트컷 표현식 한 줄은 어떤 빈의 어떤 메서드에 정책이 적용되는지 결정합니다.
지나치게 넓으면 상태 확인과 리포지토리 게터까지 트랜잭션이나 로그가 붙고, 좁으면 새 구현에서 보안이 빠집니다.
execution을 왼쪽부터 암기하지 말고 시그니처 조건별로 분해해 대표 메서드에 대입합니다.
execution 패턴
일반 형태는 괄호 안에 선택적인 접근 제한자, 반환형, 선언 타입과 메서드 이름, 파라미터 목록, 선택적인 예외 조건을 차례로 둡니다.
*는 한 구간의 임의 문자열, ..는 패키지 하위 또는 파라미터 0개 이상처럼 위치에 따라 의미가 달라집니다.
각 자리는 Java 리플렉션에서 확인할 수 있는 서로 다른 시그니처 사실을 제한합니다. 표현식 한 덩어리로 외우지 말고 접근 제한자, 반환 타입, 소유 타입, 메서드 이름, 인자 목록이 어떤 픽스처를 제외했는지 일치 표에 남깁니다.
execution(public * board.application..*Service.*(..))는 애플리케이션 하위 패키지의 이름이 Service로 끝나는 타입에서 공개인 모든 반환 타입·메서드·파라미터를 선택합니다.
이름 관례가 정책 계약으로 충분한지 검토합니다.
package board.pointcut;
public final class ExecutionExpressions {
public static final String APPLICATION_PUBLIC =
"execution(public * board.application..*(..))";
public static final String REGISTER_ONE_ARGUMENT =
"execution(* board.application..*.register(*))";
public static final String LONG_RESULT =
"execution(long board.application..*(..))";
private ExecutionExpressions() { }
}원시 long과 래퍼 Long은 반환 패턴에서 다릅니다.
오버로드된 메서드는 파라미터 패턴으로 분리하고 가변 인자가 리플렉션에서 배열로 보이는 점도 테스트합니다.
표현식 포인트컷 검증
Spring 유틸리티를 사용하면 컨텍스트 전체 없이 표현식과 Method를 검증할 수 있습니다. 긍정과 부정 메서드를 같이 출력합니다.
package board.pointcut;
import java.lang.reflect.Method;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
public final class PointcutMatchDemo {
static class ApplicationService {
public long register(String title) { return 41L; }
public long find(long id) { return id; }
protected void internal() { }
}
public static void main(String[] args) throws Exception {
var pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(public long *..*.register(String))");
Method register = ApplicationService.class
.getMethod("register", String.class);
Method find = ApplicationService.class.getMethod("find", long.class);
System.out.printf("register=%s find=%s%n",
pointcut.matches(register, ApplicationService.class),
pointcut.matches(find, ApplicationService.class));
}
}register=true find=false문자열 표현식이 파싱된다고 애플리케이션 패키지에 실제 일치가 있다는 뜻은 아닙니다. 아키텍처 리팩터링 뒤 픽스처 클래스와 운영 환경의 대표 메서드를 모두 확인합니다.
within 타입 범위
within(board.application..*)는 해당 패키지의 타입 안에서 실행되는 Spring AOP 메서드 조인 지점 후보를 선택합니다.
메서드 이름이나 반환 타입 차이를 직접 표현하지 않습니다.
넓은 모듈 스코프를 잡은 뒤 애노테이션 또는 실행과 결합하기 좋습니다.
within(board.application..*Service)의 타입 패턴과 execution(* board.application..*Service.*(..))는 비슷해 보여도 상속과 프록시 메서드 해결에서 차이를 테스트해야 합니다.
인터페이스에 선언된 메서드와 구현 패키지가 다르면 대상 클래스 기준 일치를 명확히 합니다.
Spring 빈이 아니면 within에 타입이 맞아도 프록시가 생성되지 않습니다.
포인트컷 언어 일치와 런타임 위빙 적용 가능성을 별개로 봅니다.
bean() 빈 이름 선택
bean(namePattern)은 Spring AOP가 제공하는 포인트컷 지시자입니다.
AspectJ 언어 자체의 타입 패턴이 아니라 현재 컨테이너에 등록된 빈 이름을 기준으로 프록시 적용 대상을 고릅니다.
같은 클래스로 만든 빈 둘 가운데 하나만 선택하거나, 조직이 관리하는 이름 규칙을 정책 경계로 쓸 때 의미가 있습니다.
package board.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public final class RepositoryObservationAspect {
private static final Logger log = LoggerFactory.getLogger(
RepositoryObservationAspect.class);
@Pointcut("bean(*Repository)")
void repositoryBean() {
}
@Pointcut("execution(public * *(..))")
void publicOperation() {
}
@Around("repositoryBean() && publicOperation()")
Object observe(ProceedingJoinPoint joinPoint) throws Throwable {
long started = System.nanoTime();
try {
return joinPoint.proceed();
} finally {
long elapsed = System.nanoTime() - started;
log.debug(
"repository={} elapsedNanos={}",
joinPoint.getTarget().getClass().getSimpleName(),
elapsed);
}
}
}기본 컴포넌트 이름이 boardPostRepository라면 bean(*Repository)에 일치합니다.
@Bean("primaryPostStore")처럼 명시한 이름은 패턴에 맞지 않으므로 이름을 바꾸는 리팩터링이 어드바이스 적용 범위도 바꿉니다.
타입 전체가 정책 책임 주체라면 within이나 target, 명시적인 애노테이션이 이름 규칙보다 안전합니다.
bean()은 컨테이너의 빈 동일성이 필요한 예외적인 선택으로 제한합니다.
별칭, 생성된 이름, FactoryBean이 노출하는 객체, 인프라 빈이 예상 패턴에 들어오는지 긍정·부정 픽스처로 확인합니다.
AspectJExpressionPointcut에 Method만 전달하는 단위 검사는 빈 이름을 알 수 없으므로 작은 ApplicationContext에서 실제 프록시 생성과 호출 횟수를 검증합니다.
포인트컷 교집합
within(application..) && execution(public *(..)) && @annotation(Measured)처럼 모듈, 가시성, 명시적 적용 메타데이터를 교집합으로 쌓으면 각 조건의 이유를 설명할 수 있습니다.
OR로 여러 패키지를 합치면 새 모듈 누락과 중복 일치를 테스트합니다.
부정 연산자 !within(..config..)는 편리하지만 제외 목록이 늘면 기본 스코프가 지나치게 넓다는 신호입니다.
허용 목록 포인트컷을 우선합니다.
!execution(* get*(..)) 같은 이름 제외는 중요한 게터까지 빠뜨릴 수 있습니다.
합성 메서드 고려
일반 인터페이스 구현은 컴파일러가 브리지 메서드를 만들 수 있습니다. 포인트컷과 어드바이스가 두 번 실행되는지, Spring이 가장 구체적인 메서드를 해석하는지 통합 테스트로 확인합니다. 런타임 생성된 클래스 이름을 표현식에 넣지 않습니다.
클래스 프록시의 protected 메서드 지원 가능성보다 애플리케이션 경계를 공개 메서드로 두는 원칙이 더 읽기 쉽습니다.
private와 자기 호출은 표현식이 맞아도 프록시 조인 지점이 아닙니다.
반환 타입의 공변 재정의와 인터페이스 애노테이션 위치도 포인트컷 결과에 영향을 줄 수 있습니다. 대표적인 프록시 방식 둘 다 간이 검증 테스트합니다.
포인트컷 회귀 테스트
보안이나 트랜잭션 포인트컷에는 반드시 포함되어야 할 메서드 목록과 절대 포함되면 안 되는 목록을 둡니다. 패키지 이동 PR에서 테스트가 실패하면 표현식 또는 아키텍처 의도를 함께 검토합니다. 단순 스냅샷 갱신으로 넘기지 않습니다.
운영 환경 시작에서 일치된 빈 수가 갑자기 0 또는 두 배가 되면 경보할 수 있지만 모든 메서드 이름을 메트릭 태그로 내보내지 않습니다.
연습 문제
애플리케이션 명령의 공개 register*(Command)만 선택하고 쿼리·protected 도우미·다른 패키지를 제외하는 표현식을 작성하세요.
오버로드 네 개를 픽스처로 만들어 참·거짓 표를 검증하세요.
해설 보기
파라미터 타입의 완전한 이름과 패키지 범위를 함께 사용하면 의도를 좁힐 수 있습니다.
package board.pointcut;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
public final class RegisterCommandPointcut {
public static AspectJExpressionPointcut create() {
var pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(public * board.application..*.register*(board.application.RegisterCommand))");
return pointcut;
}
private RegisterCommandPointcut() { }
}하위 타입 파라미터도 포함해야 한다면 + 타입 패턴의 의미를 별도 픽스처로 검증합니다.
메서드 인자 런타임 타입과 선언된 시그니처를 혼동하지 않습니다.