공통 로직과 서비스 로직 분리 해보기. #4 AspectJ 표현식, Annotation 기반 AOP 생성

BaekGwa·2024년 9월 10일
0

Spring

목록 보기
7/9

공통 로직과 서비스 로직

이전까지, Spring AOP를 사용하는 방법에 대해서 알아보았습니다.
이전 글

  • 이번에는, Spring AOP의 포인트 컷 AspectJ 표현식에 대해서 알아보고, Annotation 기반으로 작동하는 AOP를 생성해서 사용해 보겠습니다.

포인트컷 지시자

  • Spring AOP 에서 채택한 AspectJ 라이브러리는, 포인트 컷을 편리하기 사용하기 위해서, 특별한 표현식을 제공합니다.

포인트 컷 지시자 종류

  • execution : 메소드 실행 조인 포인트를 매칭합니다.
  • within : 특정 타입 내의 조인 포인트를 매칭합니다.
  • args : 인자가 주어진 타입의 인스턴스인 조인 포인트를 매칭합니다.
  • this : 스프링 빈 객체를 대상으로 하는 조인 포인트를 매칭 합니다.
  • target : Target 객체를 대상으로 하는 조인 포인트를 매칭 합니다.
  • @target : 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트를 매칭 합니다.
  • @within : 주어진 애노테이션이 있는 타입 내 조인 포인트를 매칭 합니다.
  • @annotation : 매서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭 합니다.
  • @args : 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인포인트를 매칭 합니다.
  • bean : 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정 합니다.
  • 이글만 보고 이해한 당신은 천재입니다.
  • 저는 이해를 할 수 없기 때문에, 대표적인 몇가지만 예제 코드를 작성해서 기록해 두겠습니다.

excution

예제에 사용한 코드 : Github

  • baekgwa/springaop/global/aop/pointcuts/ExecutionPointcut.java 참조
  • baekgwa/springaop/global/aop/aspects/AspectV5.java 참조
  • test코드) baekgwa.springaop.ExecutionTest 참조

하나씩 Case별로 Aspect 등록해서 확인해보려 했지만,,,, 너무 양이 많아 포기하고 테스트 코드로 진행 하겠습니다.

  • execution 지시자는 다음과 같은 조건으로 매칭을 시도합니다.
    • execution("접근제한자 반환타입 패키지경로.메서드이름(파라미터...) throws 예외 class")
    • ex) execution("public String baekgwa.web.cart.CartController.saveNewItem(Integer))");
    • 풀이 : public 접근제한자로, String을 반환하며, ~~경로의 패키지중, CartController 클래스(인터페이스)의 saveNewItem 메서드의 이름을 가지고, 파라미터는 Integer 타입을 받는 조건을 만족하면 생성

주요 규칙

기호설명예시
*모든 타입, 메소드 이름, 반환 타입을 의미execution(* *.*(..))
..0개 이상의 패키지나 파라미터를 의미execution(* com.example..*.*(..))
.단일 패키지 구분자 또는 메소드 이름을 구분execution(* com.example.service.*(..))
(..)0개 이상의 파라미터를 의미execution(* com.example..*.*(String, ..))
Item구체적인 타입 또는 파라미터를 의미execution(* com.example..*.*(Item))

생략 가능한 조건

기호설명예시
[접근제어자]메소드의 접근 제어자는 생략 가능execution(* com.example.service.*.*(..))
[반환타입]메소드의 반환 타입은 생략 가능execution(* com.example.service.*.*(..))
[패키지명]패키지 경로는 생략 가능 (루트 패키지 포함)execution(* *.*(..))
[클래스명]클래스명은 생략 가능 (모든 클래스 포함)execution(* com.example.service.*(..))
[메소드명]메소드 이름은 생략 가능 (모든 메소드 포함)execution(* com.example.service.*(..))
[파라미터 목록]파라미터 목록은 생략 가능 (모든 파라미터 포함)execution(* com.example.service.*.*(..))

예시

  • 접근 제어자 생략: execution(* com.example.service.*.*(..))는 모든 접근 제어자의 메소드를 포함합니다.
  • 반환 타입 생략: execution(public * com.example.service.*.*(..))는 모든 반환 타입의 메소드를 포함합니다.
  • 패키지명 생략: execution(* *.*(..))는 모든 패키지의 메소드를 포함합니다.
  • 클래스명 생략: execution(* com.example.service.*(..))는 모든 클래스를 포함합니다.
  • 메소드명 생략: execution(* com.example.service.*(..))는 모든 메소드를 포함합니다.
  • 파라미터 목록 생략: execution(* com.example.service.*.*(..))는 모든 파라미터 목록을 포함합니다.

테스트 코드

  • 테스트 코드로 여러가지 상황의 테스트를 진행 해보겟습니다.

해당 부분을 포스팅하면, 길이가 너무 길어져 Github 주소로 대체합니다.
Github

  • test코드) baekgwa.springaop.ExecutionTest 참조

within

예제에 사용한 코드 : Github

  • test코드) baekgwa.springaop.WithinTest 참조

주요 규칙

기호설명예시
within(Type)지정된 타입의 객체에 속한 메소드 호출을 매칭within(baekgwa.springaop.web.cart.CartServiceImpl)
CartServiceImpl 클래스의 모든 메소드 호출 매칭
within(Package..*)지정된 패키지와 그 하위 패키지의 모든 타입과 메소드 호출을 매칭within(baekgwa.springaop.web..*)
baekgwa.springaop.web 패키지와 그 하위 패키지의 모든 클래스의 메소드 호출 매칭
within(Type*)지정된 패키지 내에서 이름이 지정된 패턴을 가진 타입의 메소드 호출을 매칭within(baekgwa.springaop.web..*Service*)
baekgwa.springaop.web 패키지 내의 모든 Service가 포함된 클래스의 메소드 호출 매칭
within(Type)은 단일 클래스의 모든 메소드를 매칭하며, within(Package..*)는 패키지 및 하위 패키지의 모든 클래스를 매칭합니다.within(Type)within(Package..*)는 각각 단일 클래스와 패키지의 범위를 매칭합니다.within(com.example..*)com.example 패키지와 그 하위 패키지의 모든 클래스의 메소드 호출을 포함합니다.
within 표현식은 클래스와 패키지의 범위를 설정하며, 메소드 시그니처나 파라미터 타입과는 관련이 없습니다.within 표현식은 메소드 시그니처나 파라미터 타입에 영향을 미치지 않으며, 단순히 클래스 또는 패키지 범위에 따라 포인트컷을 설정합니다.within(com.example.service..*)com.example.service 패키지 및 하위 패키지의 모든 클래스에 대한 포인트컷을 설정합니다.

예시 설명

  • 단일 클래스: within(baekgwa.springaop.web.cart.CartServiceImpl)CartServiceImpl 클래스의 모든 메소드 호출을 매칭합니다.
  • 패키지 및 하위 패키지: within(baekgwa.springaop.web..*)baekgwa.springaop.web 패키지와 그 하위 패키지의 모든 클래스의 메소드 호출을 매칭합니다.
  • 패키지 내 특정 클래스 패턴: within(baekgwa.springaop.web..*Service*)baekgwa.springaop.web 패키지 내에서 Service가 이름에 포함된 클래스의 메소드 호출을 매칭합니다.

테스트 코드

@Test
    @DisplayName("within() test1")
    void withinMatchTest1() {
        pointcut.setExpression("within(baekgwa.springaop.web.cart.CartServiceImpl)");
        assertThat(pointcut.matches(saveNewItemMethod, CartServiceImpl.class)).isTrue();
    }

    @Test
    @DisplayName("within() test2")
    void withinMatchTest2() {
        pointcut.setExpression("within(baekgwa.springaop.web..*Service*)");
        assertThat(pointcut.matches(saveNewItemMethod, CartServiceImpl.class)).isTrue();
    }

결과는 다음과 같이 성공합니다.

args

  • args : 인자가 주어진 타입의 인스턴스인 조인 포인트로 매칭
  • 기본 문법은 executionargs 부분과 같습니다.

주요 규칙

기호설명예시
args(Type)메소드의 인자가 주어진 타입의 인스턴스인 조인 포인트 매칭args(baekgwa.springaop.web.Item)
Item 타입의 인자를 가진 메소드 호출 매칭
args(Type, ..)첫 번째 인자가 주어진 타입이고, 나머지 인자는 상관없는 조인 포인트 매칭args(String, baekgwa.springaop.web.Item)
첫 번째 인자가 String 타입, 두 번째가 Item 타입인 메소드 호출 매칭
args(.., Type)마지막 인자가 주어진 타입인 조인 포인트 매칭args(.., baekgwa.springaop.web.Item)
마지막 인자가 Item 타입인 메소드 호출 매칭
args(.., Type, ..)중간에 주어진 타입의 인자가 있는 조인 포인트 매칭args(.., baekgwa.springaop.web.Item, ..)
중간에 Item 타입의 인자가 있는 메소드 호출 매칭

예시 설명

  • 단일 타입: args(baekgwa.springaop.web.Item)은 메소드의 모든 인자가 Item 타입인 경우를 매칭합니다.
  • 첫 번째 인자 타입: args(String, baekgwa.springaop.web.Item)은 첫 번째 인자가 String이고 두 번째 인자가 Item인 메소드를 매칭합니다.
  • 마지막 인자 타입: args(.., baekgwa.springaop.web.Item)은 마지막 인자가 Item 타입인 메소드를 매칭합니다.
  • 중간 인자 타입: args(.., baekgwa.springaop.web.Item, ..)은 인자 목록의 중간에 Item 타입이 있는 메소드를 매칭합니다.

Execution vs Args

  • execution과 다르게 args는, 파라미터 타입에 부모타입을 허용 합니다.
  • execution은 정확하게 매칭되는 파라미터를 넣어줘야 됩니다.
 	@Test
    void argsVsExecutionTest() {
        //args
        //부모타입을 허용. 실제 넘어온 파라미터 객체 인스턴스를 보고 판단.
        //Item 참조 - 성공
        assertThat(pointcut("args(baekgwa.springaop.web.cart.domain.Item)")
                .matches(saveNewItemMethod, CartServiceImpl.class)).isTrue();

        //Interface 참조 - 성공
        assertThat(pointcut("args(baekgwa.springaop.web.cart.CartService)")
                .matches(saveNewItemMethod, CartServiceImpl.class)).isTrue();

        //최상위 Object - 성공
        assertThat(pointcut("args(Object)")
                .matches(saveNewItemMethod, CartServiceImpl.class)).isTrue();


        //execution
        //정확하게 매칭되는 것만 매칭
        //Item 참조 - 성공
        assertThat(pointcut("execution(* *(baekgwa.springaop.web.cart.domain.Item))")
                .matches(saveNewItemMethod, CartServiceImpl.class)).isTrue();

        //Interface 참조 - 실패
        assertThat(pointcut("execution(* *(baekgwa.springaop.web.cart.CartService))")
                .matches(saveNewItemMethod, CartServiceImpl.class)).isFalse();

        //최상위 Object - 실패
        assertThat(pointcut("execution(* *(Object))")
                .matches(saveNewItemMethod, CartServiceImpl.class)).isFalse();
    }

테스트 코드

해당 부분을 포스팅하면, 길이가 너무 길어져 Github 주소로 대체합니다.
Github

  • test코드) baekgwa.springaop.ArgsTest 참조

@target, @within

  • @target과 @within은 애너테이션이 있는 클래스를 조인 포인트로 지정합니다.
  • 두가지의 차이점은 다음과 같습니다.
  • @target : 인스턴스의 모든 메서드를 조인 포인트로 적용. (즉, 부모로부터 상속 받은 메서드까지 어드바이스 적용)
    • @Around("execution( baekgwa.springaop.web..(..)) && @target(baekgwa.springaop.global.aop.annotation.ClassAnnotation)")
  • @within : 자기 자신의 클래스에 정의된 메서드에만 어드바이스를 적용.
    • @Around("execution( baekgwa.springaop.web..(..)) &&
      @within(baekgwa.springaop.global.aop.annotation.ClassAnnotation)")
package baekgwa.springaop.global.aop.annotation;
~~~

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClassAnnotation {

}

@annotation

  • @annnoation은 애너테이션이 있는 메서드를 조인 포인트로 지정 합니다.
    • @Around("@annotation(baekgwa.springaop.global.aop.annotation.MethodAnnotation)")
package baekgwa.springaop.global.aop.annotation;
~~~

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

}
profile
현재 블로그 이전 중입니다. https://blog.baekgwa.site/

0개의 댓글