부가기능을 어디에 적용할껀데?


들어가기전

포인트 컷(Pointcut)이란 AOP에서 어드바이스(Advice, 부가기능 또는 횡단 코드)를 타깃의 코드 중, 어디에 적용할지를 뜻하는 AOP에서 사용되는 용어로써, AOP를 한다면 반드시 알아야 할 개념입니다.

AspectJ 5 라이브러리에서 지원하는 포인트 컷의 표현식을 기준으로 본 포스팅에선 Spring AOP에서 포인트 컷의 다양한 표현식에 대해 소개해 드리려고 합니다.

학습목표

  1. 포인트 컷의 다양한 지정자에 대한 이해
  2. 포인트 컷의 다양한 활용법

1. Spring AOP에서 의미하는 Pointcut

포인트 컷은 조인 포인트(JoinPoint)의 한 부분이라고 생각하시면 됩니다.

우선 조인 포인트란 타깃의 코드가 실행할 때 나타날 수 있는 여러 시점을 의미합니다.

join-point

  • 예외가 발생되는 시점
  • 필드(attribute)가 수정되는 시점
  • 객체가 생성(constructor)되는 시점
  • 특정 메소드가 호출되는 시점

보시다싶이 사용자에 의해 타깃의 코드가 실행될 때 발생할 수 있는 여러 시점들은 다양한데요.

그중에서, Spring AOP에서 사용되는 조인 포인트는 항상 메소드 호출을 의미합니다. 따라서 사용할 수 있는 포인트 컷의 표현 식으론 메소드 패턴을 지원하는 표현 식만을 사용할 수 있습니다.

2. 포인트 컷의 지정자

포인트 컷의 표현식은 항상 특정 지정자를 선언하여 시작합니다. 예를 들어 다음과 같은데요.

  • @Pointcut(execution( … ))
  • @Pointcut(@annotation( … ))

흔히 우리가 사용하고 있는 execution이라든지 @annotation 지정자를 PointCut Designator라 하여 통상 “PCD“라 불리고 있습니다.

이 PCD는 타깃의 여러 조인 포인트 중에 어드바이스를 어디에 적용을 시킬지, AOP에게 알려주는 키워드라 할 수 있습니다.

2.1. execution - 기본적인 PCD

먼저 소개해드릴 PCD는 가장 기본적으로 사용되고 있는 execution입니다.

package com.moong.ahea;

@Service
public class UserService{  
  public long findUserId(String name) throws RuntimeException{
    isLogic...
  };
}

만약, 다음과 같이 사용자 클래스의 findUserId 메소드에 특정 어드바이스를 주입을 하고 싶다면 다음과 같이 지정을 해주면 됩니다.

@Pointcut(
          "execution("
        + "public "
        + "long "
        + "com.moong.ahea.UserService"
        + ".findUserId(String) "
        + "throws RuntimeException"
        + ")"
          )
public void executionOfPCD(){};

다음과 같이 execution을 사용한다면 UserService.findUserId 메소드에 어드바이스가 주입이 됩니다. 물론 다음과 같이 명확한 표현 식을 통해 부가기능을 적용할 수 있지만, 이 방식은 유연하지 않습니다.

2.1.1 execution의 규칙을 활용한 우아한 사용법

예를 들어 리턴타입이 바뀐다거나, 또는 다른 클래스에 있는 findUserId 메소드에도 동일한 어드바이스를 적용하고 싶다면 방금 작성한 표현 식으론 불가능 합니다.

하지만 execution의 표현 규칙을 인지하고, 와일드 카드를 사용한다면 좀 더 우아하게 execution PCD를 사용할 수 있습니다.

  • 각 패턴은 * 표현으로 가능
  • .. 0개 이상의 의미

execution의 표현 식은 다음과 같은 규칙이 있습니다.

@Pointcut(
          "execution("    // PCD execution 지정
        + "[접근제한자 패턴] "  // public
        + "리턴타입 패턴"       // long
        + "[패키지명, 클래스 경로 패턴]"          // com.moong.ahea.UserService
        + "메소드명 패턴(파라미터 타입 패턴|..)"  // .findUserId(String)
        + "[throws 예외 타입 패턴]"             // throws RuntimeException
        +")"   
          )

[]은 생략이 가능한 옵션을 의미하고 | 는 OR 조건을 의미합니다. 따라서 기존의 Pointcut 코드를 다음과 같이 변경할 수 있습니다.

// 모든 패키지에 포함된 클래스 중에, 클래스 이름이 Service로 끝나는 클래스
@Pointcut("execution( * *..*Service.findUserId(..) )")

// 최소한의 필수 규칙들 → 리턴타입 메소드명(파라미터)
@Pointcut("execution( * findUserId(..) )")

// find로 시작하는 메소드
@Pointcut("execution( * find*(..) )")

2.2. within - 패키지, 클래스를 제한하자

두 번째 소개해 드릴 PCD는 within입니다.

execution과 비슷하지만, within은 메소드가 아닌 특정 타입에 속한 메소드를 포인트 컷으로 설정할 때 사용합니다.

@Pointcut("within(com.moong.ahea.UserService)")

예를 들어 다음과 같은 PCD가 설정 되었다면 UserService 안에 있는 모든 메소드에 어드바이스를 주입시킬 수 있습니다. 이 역시도 와일드 카드를 사용한다면 다음과 같습니다.

@Pointcut("within(*..*Service)")

2.3. this 와 target - 생성된 Proxy에 대한 PCD

다음은 this와 target 입니다. 이 둘의 차이점은 되게 비슷하지만 다릅니다.

  • this → CGLib
  • target → JDK Dynamic Proxy

우선 this는 Proxy에 대한 조인 포인트이고, target은 타깃에 대한 조인 포인트를 사용할 수 있습니다. this는 CGLib 방식으로 생성된 Proxy일때 사용되고, target은 JDK Dynamic Proxy로 Proxy가 생성될 때 사용됩니다.

예를 들어 다음과 같은 클래스가 있다고 가정해봅시다.

class UserService implements DefaultService{
  ...
}

다음과 같이 타깃인 UserService는 인터페이스를 상속 받기 때문에, Spring은 JDK Dynmaic Proxy를 사용하여 Proxy bean을 생성합니다. 이 경우엔 target PCD를 사용해야 됩니다.

왜냐하면 JDK Dynmaic Proxy는 DefaultService 인터페이스를 구현한 Proxy 클래스를 생성되고 실제적인 비즈니스 로직은 타깃 클래스인 UserService 클래스에 존재합니다.

@Pointcut("this(com.moong.ahea.UserService)")
@Pointcut("target(com.moong.ahea.UserService)")

2.4. args - 파라미터 값이 궁금하다면?

만약 타깃의 메소드 호출에서 사용되는 전달받은 파라미터 값이 궁금하다면 args PCD를 사용하면 됩니다.

@Service
public class UserService{  
  public long findUserId(String name) throws RuntimeException{
    isLogic...
  };
}

예를 들어 UserService의 String 타입의 name 파라미터의 전달받은 값을 추적한다고 가정한다면 다음과 같이 포인트 컷과 어드바이스를 구성해줘야 합니다.

@Pointcut("execution(* findUserId(..)) && args(name)")
public void argOfPCD(Object name) {};

@Around("argOfPCD(name)")
public Object advice(ProceedingJoinPoint jp, Object name){
  ...
}
  1. args PCD 사용
    • execution( …. args(name))
  2. 포인트 컷 메소드 지정
    • simplePCD(Object name)
  3. 어드바이스 애노테이션 지정
    • @Around(“findUserId(name)”)
  4. 어드바이스 메소드에서도 지정
    • advice(ProceedingJoinPoint jp, Object name)

3. 애노테이션과 PCD

지금까지 기본적인 PCD에 대해 소개해드렸습니다.

Spring에선 애노테이션을 빼놓고선 말을 할 수 없습니다. 예를들어 @Controller, @Service, @Repository 등, 이와 같이 애노테이션을 사용하여 모듈을 개발합니다. 이와 관련하여 지금부터 애노테이션과 관련된 PCD를 소개해드리겠습니다.

3.1. @target - 애노테이션이 부착된 클래스를 제한

먼저 @target PCD는 이전에 소개해드린 target PCD와는 다릅니다. @target은 타깃 클래스에 포함된 애노테이션에 대한 조인 포인트를 사용합니다.

@Pointcut("@target(org.springframework.stereotype.Service)")

다음 코듣는 @Service 애노테이션이 부착된 모든 클래스의 메소드에 어드바이스를 부여할 수 있습니다.

3.2. @args - 특정 클래스를 사용되고 있는 파라미터를 추적하기

다음은 @args PCD 입니다. 이 PCD는 런타임시 특정 타입의 클래스가 파라미터로 사용되고 있을 때, 추적하기 위한 목적으로 주로 사용되는 PCD 입니다.

예를 들어 다음과 같습니다.

@MyEntity // 임의로 만든 애노테이션
@Getter
public class UserVO{
  private long id;
}

다음과 같이 VO 객체가 있다고 가정해봅시다. 이때 @MyEntity 애노테이션은 @args PCD를 테스트하기 위해 만들었다고 생각하시면 되겠습니다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyEntity { }
  • Type
  • Runtime

특히 @args PCD를 사용하기 위해선 @MyEntity 애노테이션은 @Target이 메소드가 아닌 Type으로 지정하고 @Retention은 Runtime으로 지정해주셔야 합니다.

@Service
public class UserService{  
  public long findUserId(UserVO user) throws RuntimeException{
    ...
  }
}

다음 코드처럼 findUserId 메소드의 파라미터가 UserVO 객체를 사용한다면 이 VO 객체를 추적하기 위해 다음과 같이 PCD를 구현해주시면 됩니다.

@Pointcut("@args(com.moong.ahea.MyEntity)")
@Pointcut("@args(com.moong.ahea.MyEntity, 다른 애노테이션 추가 지정)")
@Pointcut("@args(com.moong.ahea.MyEntity, ..)")

3.3. @within - 특정 애노테이션을 통합 관리해볼까

다음 @within PCD는 클래스에 특정 애노테이션이 지정되어 있다면, 해당 클래스의 모든 메소드에 어드바이스를 부가합니다.

@Pointcut("@within(org.springframework.stereotype.Service)")
@Pointcut("within(@org.springframework.stereotype.Service UserService)")
@Pointcut("within(@org.springframework.stereotype.Service *)")

예를 들어 다음과 같이 @Service 애노테이션이 붙은 클래스에 @within PCD를 지정하여 어드바이스를 부가할 수 있습니다.

이 점은 @target PCD와 비슷해 보이지만 다릅니다. 자세한 사항은 StackOverflow(Difference-between-target-and-within-spring-aop)를 참조해주시기 바랍니다.

3.4. @annotation - 흔히 사용되는 PCD

마지막으로 소개해드릴 PCD는 바로 @annotation PCD입니다.

현재 여러 PCD중 가장 흔히 많이 사용되고 있는 PCD가 아닐까 생각이 듭니다. 사용법을 보자면 다음과 같습니다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SimpleProfiling { }

우선 하나의 애노테이션을 생성하고, 다음으로 특정 메소드에 애노테이션을 부착합니다.

@Service
public class UserService{  
  @SimpleProfiling
  public long findUserId(UserVO user) throws RuntimeException{
    ...
  }
}

마지막으로 @annotation PCD를 지정해주면 됩니다.

@Pointcut("@annotation(com.moong.ahea.SimpleProfiling )")
@Pointcut("@annotation(SimpleProfiling)")

4. 조합하여 사용하기

  • &&
  • ||
  • !

여러 지정된 PCD를 3가지의 연산자를 사용하여, PCD를 조합하여 사용할 수 있습니다.

@Pointcut("@annotation(com.moong.ahea.SimpleProfiling )")
public void simpleProfiling(){}

@Pointcut("@args(com.moong.ahea.MyEntity)")
public void myEntity(){}

@Pointcut("myEntity() && simpleProfiling()")
public void myEntityAndSimpleProfiling(){}

마무리

지금까지 Pointcut의 지정자인 여러 PCD에 대해 소개해드렸습니다.

아마 대부분의 개발자분이 execution과 @annotation PCD를 주로 사용하여 개발하고 있을 거라 생각이 듭니다. 이외에도 다양한 PCD가 있다는 걸 인지하셨으면 좋겠다 싶어 포스팅을 작성하게 되었습니다.

  • execution
  • within
  • args
  • this
  • target
  • @target
  • @args
  • @within
  • @annotation

참고