스프링 부트에서의 AOP(관점 지향 프로그래밍) 및 포인트컷 표현식 활용

AOP 핵심 개념

  • 관점(Aspect): 로깅이나 권한 검사와 같은 공통 관심사를 모듈화한 구성요소로, @Aspect 어노테이션을 통해 클래스를 관점으로 선언한다.
  • 조인포인트(Join Point): 메서드 호출이나 예외 발생과 같은 실행 시점의 특정 위치이며, 스프링 AOP는 메서드 수준만 지원한다.
  • 어드바이스(Advice): 조인포인트에서 수행되는 동작으로 다섯 가지 유형이 있다:
    • @Before: 메서드 실행 전에 동작
    • @After: 성공 여부와 관계없이 메서드 실행 후 동작
    • @AfterReturning: 정상 반환 후 동작
    • @AfterThrowing: 예외 발생 시 동작
    • @Around: 메서드 전체를 감싸며 실행 흐름 제어 가능
  • 포인트컷(Pointcut): 어떤 메서드에 어드바이스를 적용할지를 결정하는 표현식으로, AspectJ 문법을 사용하여 정의한다.

포인트컷 표현식 예제

AspectJ 기반 포인트컷 표현식의 기본 구조는 다음과 같다:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

실제 사용 예시:

  1. 서비스 계층 모든 메서드 대상
@Pointcut("execution(* com.example.demo.service.*.*(..))")
  • *: 임의의 반환 타입
  • com.example.demo.service.*: service 패키지 내 모든 클래스
  • * *(..): 임의의 메서드명 및 파라미터
  1. 특정 어노테이션이 붙은 메서드
@Pointcut("@annotation(com.example.demo.annotation.Log)")
  • @Log 어노테이션이 적용된 메서드만 선택
  1. 복합 조건식
@Pointcut("execution(* com.example.demo.controller.*.*(..)) && @annotation(com.example.demo.annotation.Auth)")
  • 컨트롤러 패키지 중 @Auth 어노테이션이 있는 메서드 대상

전체 구현 예시

요구사항: 컨트롤러 메서드 호출 시 메서드 이름, 입력값, 소요 시간, 결과 값을 로그로 기록한다.

  1. 커스텀 어노테이션 정의
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
    String desc() default "";
}
  1. 관점 클래스 생성
@Aspect
@Component
public class LoggingAspect {

    // @Log 어노테이션이 붙은 메서드를 위한 포인트컷
    @Pointcut("@annotation(com.example.demo.annotation.Log)")
    public void logPointcut() {}

    // 메서드 실행 전 로그 출력
    @Before("logPointcut()")
    public void beforeLog(JoinPoint joinPoint) {
        String method = joinPoint.getSignature().getName();
        Object[] params = joinPoint.getArgs();
        System.out.println("[LOG-BEFORE] Method: " + method + ", Args: " + Arrays.toString(params));
    }

    // 메서드 실행 시간 측정
    @Around("logPointcut()")
    public Object measureTime(ProceedingJoinPoint pjp) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = pjp.proceed(); // 실제 메서드 실행
        long endTime = System.currentTimeMillis();
        System.out.println("[LOG-AROUND] Duration: " + (endTime - startTime) + "ms");
        return result;
    }

    // 메서드 리턴 후 로그 출력
    @AfterReturning(pointcut = "logPointcut()", returning = "output")
    public void afterReturnLog(Object output) {
        System.out.println("[LOG-AFTER] Result: " + output);
    }
}
  1. 컨트롤러 메서드에 어노테이션 적용
@RestController
@RequestMapping("/api/user")
public class UserController {

    @Log(desc = "사용자 정보 조회")
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
}

동작 원리

  1. 프록시 메커니즘: 스프링 AOP는 JDK 동적 프록시 또는 CGLIB를 이용해 런타임에 대상 객체의 프록시를 생성하고, 메서드 호출 시점에 관점 로직을 삽입한다.
  2. 실행 순서 (@Around 중심):
    • 프록시가 메서드 호출 가로챔
    • @Before 어드바이스 실행
    • ProceedingJoinPoint.proceed()로 실제 메서드 실행
    • @AfterReturning/@AfterThrowing 어드바이스 실행
    • @After 어드바이스 실행

권장 사항

  1. 관점 범위 조절:
    • 복잡한 비즈니스 로직은 관점 외부로 분리
    • 커스텀 어노테이션을 활용하여 정확한 포인트컷 설정
  2. 성능 최적화:
    • @Around 내부에서 무거운 연산 피하기
    • 고빈도 호출 메서드에는 AOP 적용 신중하게 고려
  3. 예외 처리:
    • @Around에서는 원본 예외 누락 방지
    • @AfterThrowing을 통해 통합된 오류 로깅
  4. 응용 시나리오:
    • 로깅 + 성능 모니터링: @Around로 시간 측정 및 로깅
    • 권한 체크 + 트랜잭션 관리: @Before에서 권한 확인, @Around에서 트랜잭션 제어

AspectJ 포인트컷 심화 설명

AspectJ 포인트컷 표현식은 AOP의 핵심으로, 어떤 메서드에 어드바이스를 적용할지를 결정한다. 스프링 부트에서는 AspectJ의 문법을 차용하여 사용한다.

기본 표현식 형식

execution(<접근제한자>? <반환타입> <클래스경로>.<메서드명>(<파라미터>) <예외>?)

주요 와일드카드:

  • *: 임의의 문자열 또는 타입
  • ..: 임의 개수의 파라미터 또는 하위 패키지 경로
  • +: 해당 클래스 및 서브클래스

표현식 예제

  1. 메서드 실행 대상
    • 모든 public 메서드: execution(public * *(..))
    • 특정 패키지 내 모든 메서드: execution(* com.example.service.*.*(..))
    • 특정 메서드 이름: execution(* com.example.service.*.add*(..))
    • 특정 파라미터: execution(* com.example.service.*.*(String, int))
  2. 어노테이션 기반 매칭
    • 특정 어노테이션이 붙은 메서드: @annotation(com.example.annotation.Log)
    • 특정 어노테이션이 붙은 클래스 내 메서드: @within(com.example.annotation.Service)
  3. 복합 조건
    • AND 조건: execution(...) && @annotation(...)
    • OR 조건: execution(...) || execution(...)

고급 표현식

  • this(UserService): 프록시 객체 타입 매칭
  • target(UserService): 실제 대상 객체 타입 매칭
  • get(* com.model.User.name): 필드 접근 시점
  • execution(com.model.User.new(..)): 생성자 호출 시점

최선의 활용 방법

  1. 과도한 매칭 방지: *(..)보다는 구체적인 메서드 명세 사용
  2. 커스텀 어노테이션 활용: @annotation과 함께 유연하게 제어
  3. 복합 표현식 조합: 가독성 향상을 위해 AND/OR 연산자 적극 활용
  4. 성능 모니터링 + 로깅: @Around로 메서드 실행 시간 측정

종합 예제

  1. 관점 정의
@Aspect
@Component
public class PerformanceLogger {

    // 컨트롤러 public 메서드 포인트컷
    @Pointcut("execution(public * com.example.demo.controller.*.*(..))")
    public void controllerMethods() {}

    // @Log 어노테이션이 붙은 메서드 포인트컷
    @Pointcut("@annotation(com.example.demo.annotation.Log)")
    public void annotatedWithLog() {}

    // 두 조건 모두 만족하는 경우
    @Before("controllerMethods() && annotatedWithLog()")
    public void preLog(JoinPoint jp) {
        System.out.println("Executing method: " + jp.getSignature().getName());
    }

    // 실행 시간 측정
    @Around("controllerMethods()")
    public Object timeCheck(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed();
        System.out.println("Elapsed Time: " + (System.currentTimeMillis() - start) + " ms");
        return result;
    }
}
  1. 어노테이션 정의
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
    String value() default "";
}
  1. 컨트롤러 적용
@RestController
@RequestMapping("/api/user")
public class UserController {

    @Log("사용자 정보 가져오기")
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
}

Java 포인트컷 지시자(PCD)

Java AOP에서 주요 포인트컷 지시자는 다음과 같다:

1. execution

메서드 실행 지점을 나타내며 가장 일반적으로 사용됨:

// service 패키지 내 모든 메서드
execution(* com.example.service.*.*(..))

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

// 특정 메서드명 패턴
execution(public * *(..))
execution(* set*(..))

2. 어노테이션 관련

  • @within: 특정 어노테이션이 붙은 클래스 내 모든 메서드
  • @target: 대상 객체가 특정 어노테이션이 붙은 클래스
  • @annotation: 메서드에 특정 어노테이션이 붙은 경우
  • @args: 메서드 파라미터에 특정 어노테이션이 붙은 경우

3. 파라미터 관련

  • args: 파라미터 타입이나 개수에 따라 매칭

4. 기타

  • within: 특정 패키지나 클래스 내 연결점
  • this / target: 프록시 또는 실제 객체 타입 매칭
  • bean: 스프링 빈 이름으로 매칭(Spring 전용)

결합 연산자

  • &&: AND
  • ||: OR
  • !: NOT

예시

@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}

@Pointcut("within(com.example.dao..*)")
public void daoLayer() {}

@Pointcut("serviceLayer() || daoLayer()")
public void allLayers() {}

@Before("allLayers() && @annotation(com.example.Auditable)")
public void audit(JoinPoint jp) {
    // 감사 로직
}

JoinPoint와 ProceedingJoinPoint 비교

1. JoinPoint

  • 역할: 메서드 호출 시점의 정보 제공
  • 사용 대상: @Before, @After, @AfterReturning, @AfterThrowing
  • 주요 메서드:
    • getArgs(): 파라미터 배열 반환
    • getSignature(): 메서드 시그니처 정보
    • getTarget(): 실제 대상 객체
    • getThis(): 프록시 객체
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint jp) {
    String method = jp.getSignature().getName();
    Object[] args = jp.getArgs();
    System.out.println("Calling method: " + method + ", Args: " + Arrays.toString(args));
}

2. ProceedingJoinPoint

  • 역할: 메서드 실행 흐름 제어 (JoinPoint 확장)
  • 사용 대상: @Around 전용
  • 핵심 메서드:
    • proceed(): 실제 메서드 실행
    • proceed(Object[] args): 새로운 파라미터로 메서드 실행
@Around("execution(* com.example.service.*.*(..))")
public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("Before method: " + pjp.getSignature().getName());

    Object result = pjp.proceed(); // 또는 pjp.proceed(newArgs);

    System.out.println("After method, Result: " + result);
    return result;
}

차이 요약

속성 JoinPoint ProceedingJoinPoint
용도 정보 취득 실행 흐름 제어
적용 어드바이스 @Before, @After @Around 전용
핵심 메서드 getArgs(), getSignature() proceed(), proceed(Object[] args)
실행 흐름 변경 불가능 가능

권장 사항

  1. 필요한 정보만 얻을 때는 JoinPoint 사용
  2. @Around에서는 반드시 proceed() 호출 필요
  3. 고성능 요구 시 @Around 사용 자제

파라미터 바인딩

AOP에서 파라미터 바인딩은 포인트컷 표현식을 통해 어드바이스 메서드의 파라미터에 실제 메서드 파라미터 값을 전달하는 기술이다.

1. 바인딩 메커니즘

  • args() 사용: 파라미터 이름 또는 타입으로 매칭
@Before("execution(* com.example.service.*.*(..)) && args(param)")
public void logParam(JoinPoint jp, Object param) {
    System.out.println("Parameter value: " + param);
}
  • 여러 파라미터 바인딩:
@Before("execution(* com.example.service.*.*(..)) && args(id, name)")
public void logMultiple(Long id, String name) {
    System.out.println("ID: " + id + ", Name: " + name);
}

2. 활용 시나리오

  1. 로깅:
@Around("execution(* com.example.service.*.*(..))")
public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
    Object[] args = pjp.getArgs();
    System.out.println("Calling method: " + pjp.getSignature().getName() + ", Args: " + Arrays.toString(args));
    return pjp.proceed();
}
  1. 파라미터 유효성 검사:
@Before("execution(* com.example.service.*.*(..)) && args(param)")
public void validate(Object param) {
    if (param == null) {
        throw new IllegalArgumentException("Parameter cannot be null");
    }
}
  1. 권한 체크:
@Before("execution(* com.example.service.*.*(..)) && args(userId)")
public void checkPermission(Long userId) {
    if (!permissionService.hasAccess(userId)) {
        throw new SecurityException("Access denied");
    }
}

3. 고급 기법

  1. 커스텀 어노테이션 결합
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
    String value() default "";
    boolean logParams() default true;
}

@Aspect
@Component
public class LoggingAspect {
 
    @Before("@annotation(loggable)")
    public void handleLoggable(JoinPoint jp, Loggable loggable) {
        System.out.println("Annotation value: " + loggable.value());
    }
}
  1. 파라미터 수정
@Around("execution(* com.example.service.*.*(..))")
public Object modifyArg(ProceedingJoinPoint pjp) throws Throwable {
    Object[] args = pjp.getArgs();
    if (args.length > 0 && args[0] instanceof String) {
        args[0] = "modified value"; // 첫 번째 파라미터 수정
    }
    return pjp.proceed(args); // 새 파라미터로 실행
}
  1. 제네릭 처리
@Before("execution(* com.example.service.*.*(..)) && args(list)")
public void logList(List<String> list) {
    System.out.println("List size: " + list.size());
}

4. 문제 해결 팁

  • 바인딩 실패: 파라미터 이름 또는 타입 불일치 확인
  • 성능 이슈: 고빈도 호출 메서드에서는 바인딩 로직 최소화
  • 내부 호출 무시: 같은 클래스 내 메서드 호출은 프록시를 우회하므로 직접 프록시 접근 필요

태그: spring-boot AOP aspectj Pointcut advice

6월 13일 19:54에 게시됨