Spring AOP Pointcut 표현식

Spring AOP Pointcut이란?

Spring AOP(Aspect-Oriented Programming)에서 Pointcut은 Advice(횡단 관심사 로직)가 적용될 Join Point를 선택하는 표현식이다. 로깅, 트랜잭션, 보안, 캐싱 등 횡단 관심사를 비즈니스 로직에서 분리할 때 Pointcut이 “어디에 적용할지”를 결정한다.

@Aspect
@Component
public class LoggingAspect {

    // Pointcut: service 패키지의 모든 public 메서드
    @Before("execution(public * com.example.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        log.info("[CALL] {}", joinPoint.getSignature().toShortString());
    }
}

Spring AOP는 메서드 실행(method execution) Join Point만 지원한다. AspectJ와 달리 필드 접근, 생성자 호출 등은 불가하지만, 대부분의 실무 요구사항을 충족한다.

execution 표현식 완전 해부

execution은 가장 많이 쓰이는 Pointcut 지시자다. 문법을 정확히 이해하면 정밀한 타겟팅이 가능하다.

execution(접근제어자? 반환타입 패키지.클래스.메서드(파라미터) throws예외?)

// 각 부분 설명:
execution(
  public                           // 접근제어자 (생략 가능)
  String                           // 반환 타입 (* = 모든 타입)
  com.example.service.UserService  // 패키지.클래스
  .findByEmail                     // 메서드명
  (String)                         // 파라미터 타입
  throws RuntimeException          // 예외 (생략 가능)
)
패턴 의미 예시
* 임의의 한 단어 * find*(..) → findById, findAll 등
.. 0개 이상 (파라미터 또는 패키지) com.example..* → 하위 모든 패키지
(..) 파라미터 무관 save(..) → save(), save(User), save(User, boolean)
(*) 정확히 파라미터 1개 findById(*)
(*, String) 2개, 두 번째가 String update(*, String)
// 실전 예시 모음
// 1. service 패키지의 모든 메서드
execution(* com.example.service.*.*(..))

// 2. service 및 하위 패키지의 모든 메서드
execution(* com.example.service..*.*(..))

// 3. Service로 끝나는 클래스의 모든 메서드
execution(* com.example..*Service.*(..))

// 4. save로 시작하는 메서드
execution(* com.example..*.save*(..))

// 5. void 반환 메서드만
execution(void com.example.service.*.*(..))

// 6. 파라미터가 정확히 Long 하나인 메서드
execution(* com.example..*.*(Long))

@annotation: 커스텀 어노테이션 타겟팅

실무에서 가장 강력한 패턴은 커스텀 어노테이션과 Pointcut을 결합하는 것이다. 적용 대상을 명시적으로 제어할 수 있다.

// 1. 커스텀 어노테이션 정의
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Timed {
    String value() default "";
}

// 2. Aspect에서 @annotation으로 타겟팅
@Aspect
@Component
public class TimingAspect {

    @Around("@annotation(timed)")
    public Object measureTime(ProceedingJoinPoint pjp, Timed timed) throws Throwable {
        String label = timed.value().isEmpty()
            ? pjp.getSignature().toShortString()
            : timed.value();

        long start = System.nanoTime();
        try {
            return pjp.proceed();
        } finally {
            long elapsed = (System.nanoTime() - start) / 1_000_000;
            log.info("[TIMING] {} - {}ms", label, elapsed);
        }
    }
}

// 3. 사용: 원하는 메서드에만 적용
@Service
public class OrderService {

    @Timed("주문생성")
    public Order createOrder(OrderDto dto) {
        // 이 메서드만 시간 측정됨
    }
}

Pointcut 조합: &&, ||, !

여러 Pointcut을 논리 연산자로 조합하면 세밀한 타겟팅이 가능하다. @Pointcut으로 재사용 가능한 이름을 붙이는 것이 핵심이다.

@Aspect
@Component
public class AuditAspect {

    // 재사용 가능한 Pointcut 정의
    @Pointcut("execution(* com.example.service..*.*(..))")
    public void serviceLayer() {}

    @Pointcut("execution(* com.example.repository..*.*(..))")
    public void repositoryLayer() {}

    @Pointcut("@annotation(com.example.annotation.Auditable)")
    public void auditable() {}

    // 조합: service 레이어이면서 @Auditable이 붙은 메서드
    @AfterReturning(
        pointcut = "serviceLayer() && auditable()",
        returning = "result"
    )
    public void auditServiceCall(JoinPoint jp, Object result) {
        log.info("[AUDIT] {} returned {}", jp.getSignature(), result);
    }

    // 조합: service 또는 repository 레이어
    @Before("serviceLayer() || repositoryLayer()")
    public void logAllDataAccess(JoinPoint jp) {
        log.debug("[ACCESS] {}", jp.getSignature());
    }

    // 부정: repository 제외한 모든 service 메서드
    @Before("serviceLayer() && !repositoryLayer()")
    public void serviceOnly(JoinPoint jp) {
        log.info("[SERVICE] {}", jp.getSignature());
    }
}

within, bean, args 지시자

지시자 용도 예시
within 특정 타입 내 모든 메서드 within(com.example.service..*)
bean Bean 이름으로 타겟팅 bean(*Service)
args 런타임 파라미터 타입 args(com.example.dto.OrderDto,..)
@within 클래스 어노테이션 @within(org.springframework.stereotype.Service)
@args 파라미터의 어노테이션 @args(jakarta.validation.Valid)
// bean 지시자: Spring 전용 (AspectJ에는 없음)
@Before("bean(*Controller)")
public void beforeAllControllers(JoinPoint jp) {
    log.info("[CTRL] {}", jp.getSignature());
}

// args로 파라미터 바인딩
@Before("execution(* com.example..*.*(..)) && args(dto,..)")
public void logDto(JoinPoint jp, Object dto) {
    log.info("[DTO] {} → {}", jp.getSignature(), dto);
}

// @within: @Service가 붙은 모든 클래스의 메서드
@Around("@within(org.springframework.stereotype.Service)")
public Object wrapService(ProceedingJoinPoint pjp) throws Throwable {
    return pjp.proceed();
}

Pointcut 성능 최적화

Pointcut 표현식은 Bean 생성 시점에 평가되므로 애플리케이션 시작 시간에 영향을 준다. BeanPostProcessor가 모든 Bean에 대해 Pointcut 매칭을 수행하기 때문이다.

// ❌ 느림: 모든 Bean의 모든 메서드를 검사
@Before("execution(* *(..))")
public void tooWide() {}

// ✅ 빠름: 패키지 범위를 좁힘
@Before("execution(* com.example.service..*.*(..))")
public void scoped() {}

// ✅ 더 빠름: within으로 1차 필터링 후 execution으로 세밀화
@Before("within(com.example.service..*) && execution(public * *(..))")
public void optimized() {}

within은 타입 레벨에서 빠르게 필터링하므로, execution과 조합하면 매칭 비용을 줄일 수 있다.

프록시 방식의 한계

Spring AOP는 Transaction 전파와 마찬가지로 프록시 기반이므로 Self-Invocation에서 Aspect가 동작하지 않는다.

@Service
public class OrderService {

    @Timed
    public void processOrder(OrderDto dto) {
        // ...
        this.sendNotification(dto); // ⚠️ AOP 미적용!
    }

    @Timed
    public void sendNotification(OrderDto dto) {
        // 직접 호출이므로 프록시를 거치지 않음
    }
}

// 해결: 별도 서비스로 분리하거나 self-injection 사용

정리

Spring AOP Pointcut은 횡단 관심사의 적용 범위를 결정하는 핵심 표현식이다. execution으로 메서드 시그니처를 타겟팅하고, @annotation으로 명시적 제어를 하며, within/bean/args로 보조 필터링을 조합하면 정밀하면서도 유지보수 가능한 AOP 설계가 된다. @Pointcut으로 이름을 붙여 재사용하고, 패키지 범위를 좁혀 성능을 최적화하는 것이 프로덕션 필수 전략이다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux