Spring Framework

[Spring Core] AOP(Aspect Oriented Programming) - 3편 (Pointcut, execution 지시자, within 지시자, named pointcut)

kgvovc 2021. 4. 5. 17:08
반응형

AOP - 3편 (Pointcut)

 

[Pointcut 표현식]

지금까지 살펴본 코드에서는 Pointcut을 선택하기 위해 'execution(* *..* ServiceImpl.*(..))'과 같은 표현을 사용해 왔다. 이처럼 표현식을 이용한 JoinPoint 선택 기능은 AspectJ가 제공하며, 스프링 AOP는 AspectJ가 제공하는 Pointcut 표현식을 상당수 지원한다.

 

Pointcut은 일치시킬 패턴에 따라 지시자(designator)의 형식이 달라지는데, 지금부터 대표적인 표현식 패턴을 하나씩 살펴보자.

 

 

메서드명으로 Join Point 선택

메서드명의 패턴으로 JoinPoint를 선택하는 방식으로, 지금까지 봐온 execution 지시자를 사용한다. 특히 execution 지시자는 다른 지시자에 비해 상대적으로 많이 활용되는 기본적인 지시자다.

 

다음은 execution 지시자를 활용해 Pointcut을 표현한 것이다.

 

 

 

이제 execution 지시자를 활용하는 예를 살펴보자.

  • execution(* com.example.user.UserService.*(..))

    com.example.user.UserService 클래스에서 임의의 메서드를 대상으로 한다.

 

  • execution(* com.example.user.UserService.find*(..))

    com.example.user.UserService 클래스에서 이름이 find로 시작하는 메서드를 대상으로 한다.

 

  • execution(String com.example.user.UserService.*(..))

    com.example.user.UserService 클래스에서 반환값의 타입이 String인 메서드를 대상으로 한다.

 

  • execution(* com.example.user.UserService.*(String, ..))

    com.example.user.UserService 클래스에서 첫 번째 매개변수의 타입이 String인 메서드를 대상으로 한다.

 

예에서 알 수 있듯이 Pointcut 표현식에는 와일드카드(wildcard)를 사용할 수 있다. 사용할 수 있는 종류에는 *, .., +의 세 가지가 있으며, 각각의 의미는 다음과 같다.

 

Pointcut 표현식에 사용되는 Wildcard

Wildcard설명
*기본적으로 임의의 문자열을 의미한다. 패키지를 표현할 때는 임의의 패키지 1개 계층을 의미한다. 메서드의 매개변수를 표현할 때는 임의의 인수 1개를 의미한다.
..패키지를 표현할 때는 임의의 패키지 0개 이상 계층을 의미한다. 메서드의 매개변수를 표현할 때는 임의의 인수 0개 이상을 의미한다.
+클래스명 뒤에 붙여 쓰며, 해당 클래스와 해당 클래스의 서브클래스, 혹은 구현 클래스 모두를 의미한다.

 

 

이번에는 Wildcard의 의미에 유념하면서 execution 지시자의 사용 예를 조금 더 살펴보자.

  • execution(* com.example.service.*.*(..))

    임의의 클래스에 속한 임의의 메서드를 대상으로 한다. 단, 임의의 클래스는 service 패키지에 속한다.

 

  • execution(* com.example.service..*.*(..))

    임의의 클래스에 속한 임의의 메서드를 대상으로 한다. 단, 임의의 클래스는 service 패키지나 그 서브 패키지에 속한다.

 

  • execution(* com.example.*.user.*.*(..))

    임의의 클래스에 속한 임의의 메서드를 대상으로 한다. 단, 임의의 클래스는 user 패키지에 속하고 com.example과 user 사이에는 임의의 패키지가 한 단계 더 있다.

 

  • execution(* com.example.user.UserService.*(*))

    UserService 클래스의 메서드 중 매개변수의 개수가 하나인 메서드를 대상으로 한다. 단, UserService는 com.example.user 패키지에 속한다.

 

 

 

타입으로 Join Point 선택

타입 정보를 활용해 Join Point를 선택할 수도 있는데 이때는 within 지시자를 사용한다. within 지시자는 클래스명의 패턴만 사용하기 때문에 execution 지시자에 비해 상대적으로 표현식이 간결하다.

  • within(com.example.service..*)

    임의의 클래스에 속한 임의의 메서드를 대상으로 한다. 단, 임의의 클래스는 service 패키지나 이 패키지의 서브 패키지에 속한다.

 

  • within(com.example.user.UserServiceImpl)

    UserServiceImpl 클래스의 메서드를 대상으로 한다. 단, UserServiceImpl 클래스는 com.example.user 패키지에 속한다.

 

  • within(com.example.password.PasswordEncoder+)

    PasswordEncoder 인터페이스를 구현한 클래스의 메서드를 대상으로 한다. 단, PasswordEncoder 인터페이스는 com.example.user 패키지에 속한다.

 

 

그 밖의 기타 방법으로 Join Point 선택

스프링 AOP에는 그 밖에도 다양한 지시자가 준비돼 있다. 그 중에서 몇 가지 유용한 사용 형태를 소개하자면 다음과 같다.

  • bean(*Service)

    DI 컨테이너에 관리되는 빈 가운데 이름이 'Service'로 끝나는 빈의 메서드를 대상으로 한다.

 

  • @annotation(com.example.annotation.TraceLog)

    @TraceLog 애너테이션(com.example.annotation.TraceLog)이 붙은 메서드를 대상으로 한다.

 

  • @within(com.example.annotation.TraceLog)

    @TraceLog 애너테이션(com.example.annotation.TraceLog)이 붙은 클래스의 메서드를 대상으로 한다.

 

이와 같이 특정 기능을 구현한 후, 그와 관련된 애너테이션을 만들어 둔 것이 있다면 @annotation 지시자@within 지시자를 활용하는 편이 표현이 간결해서 사용하기 편리할 것이다.

 

 

 

네임드 Pointcut 활용

Pointcut에 이름을 붙여두면 나중에 그 이름으로 pointcut을 재사용할 수 있다. 이렇게 이름이 붙여진 pointcut네임드 포인트컷(Named Pointcut)이라 한다. Named Pointcut@Pointcut 애너테이션(org.aspectj.lang.annotation.Pointcut)으로 정의할 수 있는데, 이 애너테이션이 붙은 메서드 이름이 pointcut의 이름이 된다. 참고로 이때 메서드의 반환값은 void로 한다.

 

  • Named Pointcut 정의
@Component
@Aspect
public class NamedPointCuts {
    @Pointcut("within(com.example.web..*)")
    public void inWebLayer() {}
    
    @Pointcut("within(com.example.domain..*)")
    public void inDomainLayer() {}
    
    @Pointcut("execution(public * *(..))")
    public void anyPublicOperation() {}
}

 

이렇게 만들어진 Named Pointcut은 나중에 AdvicePointcut을 지정할 때 활용할 수 있다.

 

 

  • Named Pointcut 활용
@Aspect
@Component
public class MethodLoggingAspect {
	@Around("inDomainLayer()")
    public Object log(ProceedingJoinPoint jp) throws Throwable {
        // 생략
    }
}

 

한편 다음과 같이 &&, ||, !과 같은 논리곱, 논리합, 논리부정 연산자를 활용해 다양한 형태로 조합할 수도 있다.

@Around("inDomainLayer() || inWebLayer()")

 

 

Advice 대상 객체와 인수 정보 가져오기

JoinPoint 타입의 인수(org.aspectj.lang.JoinPoint)를 활용하면 Advice 대상 객체나 메서드를 호출할 때 전달된 인수의 정보를 가져올 수 있다.

getTarget 메서드를 이용하면 Proxy가 입혀지기 전의 원본 대상 객체 정보를, getThis 메서드를 이용하면 Proxy를, getArgs 메서드를 이용하면 인수 정보를 가져올 수 있다.

 

  • JoinPoint 객체를 통해 대상 객체와 인수 정보 가져오기
@Around("execution(* *..*ServiceImpl.*(..))")
public Object log(JoinPoint jp) throws Throwable {
    Object targetObject = jp.getTarget(); // 프락시가 입혀지기 전의 원본 대상 객체를 가져온다.
    Object thisObject = jp.getThis(); // 프락시를 가져온다.
    Object[] args = jp.getArgs(); // 인수를 가져온다.
    
    
    
    // 생략
}

 

단, JoinPoint 인터페이스의 메서드는 반환값이 Object 타입이기 때문에 실제로 사용하기 전에는 형변환해야 한다. 그래서 이 과정에서 타입이 호환되지 않으면 ClassCastException이 발생할 수 있다. 이럴 때는 Pointcut 지시자target이나 this, args 등을 활용해 대상 객체나 인수를 Advice 메서드에 파라미터로 바로 바인딩하면 된다.

 

  • target, args 지시자를 활용해 대상 객체와 인수 정보 가져오기
@Around("execution(* com.example.CalcService.*(com.example.CalcInput)) && target(service) && args(input)")
public Object log(CalcService service, CalcInput input) throws Throwable {
    // 생략
}

 

이 방법을 사용하면 getTarget 메서드나 getArgs 메서드의 결과를 형변환할 필요가 없어지고 타입이 맞지 않는 경우는 자연스럽게 Advice 대상에서 제외된다. 결국 타입 불일치로 인한 오동작을 미연에 방지할 수 있는데 이런 상황을 '타입 안전(type safe)하다'라고 말한다. 상당히 유용한 기법이니 필요한 곳에 적절히 활용하자.

 

반응형