관리 메뉴

나구리의 개발공부기록

스프링 AOP - 포인트컷, 포인트컷지시자, 예제 만들기, execution, within, args, @target/@within, @annotation/@args, bean, 매개변수 전달, this/target 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 핵심원리 - 고급편

스프링 AOP - 포인트컷, 포인트컷지시자, 예제 만들기, execution, within, args, @target/@within, @annotation/@args, bean, 매개변수 전달, this/target

소소한나구리 2024. 11. 14. 22:34

출처 : 인프런 - 스프링 핵심 원리 - 고급편 (유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


1. 포인트컷 지시자

1) 포인트컷 지시자

(1) 포인트컷 표현식

  • 애스펙트J는 포인트컷을 편리하게 표현하기 위한 특별한 표현식을 제공하는데 이를 AspectJ pointcut expression 이라고 함
  • 애스펙트J가 제공하는 포인트컷 표현식이라는 뜻으로 줄여서 포인트컷 표현식이라고함

2) 종류 및 요약 설명

(1) execution

  • 메소드 실행 조인 포인트를 매칭
  • 스프링 AOP에서 가장 많이 사용하고 기능도 복잡함

(2) within

  • 특정 타입 내의 조인 포인트를 매칭

(3) args

  • 인자가 주어진 타입의 인스턴스 조인 포인트

(4) target

  • 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트

(5) @target

  • 주어진 애노테이션이 있는 타입 내 조인 포인트

(6) @within

  • 주어진 애노테이션이 있는 타입 내 조인 포인트

(7) @annotation

  • 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭

(8) @args

  • 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트

(9) bean

  • 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정함

** 참고

  • 포인트컷 지시자가 무엇을 뜻하는지는 글로만 읽어보면 이해하기가 쉽지 않으므로 실습이 필요함
  • execution을 가장 많이 사용하고 나머지는 자주 사용하지는 않으므로 execution을 중점적으로 이해하는 것을 권장함

2. 예제 만들기

1) Main

(1) ClassAop - 애노테이션

  • member.annotation 패키지를 생성하여 코드를 작성
  • @Target(ElementType.TYPE): 클래스, 인터페이스, 열거형(enum)에 애노테이션을 적용할 수 있도록 타겟을 설정
  • @Retention(RetentionPolicy.RUNTIME): 런타임 시점까지 애노테이션이 유지됨(애노테이션 정보가 유지)
package hello.aop.member.annotation;

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

 

(2) MethodAop - 애노테이션

  • 해당 애노테이션은 메서드에만 적용되도록 @Target을 설정
  • 애노테이션에 변수를 선언하면 애노테이션을 적용할 때 이 속성에 값을 지정할 수 있음
package hello.aop.member.annotation;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodAop {
    String value();
}

 

(3) MemberService

package hello.aop.member;

public interface MemberService {
    String hello(String param);
}

 

(4) MemberServiceImpl

  • 직접만든 @ClassAop 애노테이션을 적용하고 MemberService 인터페이스를 구현한 클래스
  • 적용한 AOP를 자동으로 스프링빈으로 등록되도록 @Component를 적용
  • MemberService의 메서드를 구현한 hello()메서드에는 @MethodAop 애노테이션을 "test value"라는 값을 입력하여 적용
  • MemberServiceImpl이 가지고있는 internal()메서드는 아무것도 적용하지 않음
package hello.aop.member;

@ClassAop
@Component
public class MemberServiceImpl implements MemberService {

    @Override
    @MethodAop("test value")
    public String hello(String param) {
        return "ok";
    }
    public String internal (String param) {
        return "ok";
    }
}

2) Test

(1) ExecutionTest 생성

  • 테스트 하위에 pointcut패키지를 생성하여 작성
  • AspectJExpressionPointcut: 상위에 Pointcut 인터페이스를 구현하고 있는 포인트컷 표현식을 처리해주는 클래스로 여기에 포인트컷 표현식을 지정하면 됨
  • @BeforeEach로 MemberServiceImpl의 메서드 정보를 가져오고 테스트로 출력
  • execution으로 시작하는 포인트컷 표현식은 메서드 정보를 매칭해서 포인트컷을 대상으로 찾아냄
package hello.aop.pointcut;

@Slf4j
public class ExecutionTest {

    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    Method helloMethod;

    @BeforeEach
    public void init() throws NoSuchMethodException {
        helloMethod = MemberServiceImpl.class.getMethod("hello", String.class);
    }

    @Test
    void printMethod() {
        // execution 매칭이 아래의 로그와 매칭됨
        // helloMethod=public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)
        log.info("helloMethod={}", helloMethod);
    }
}
/* 출력결과
helloMethod=public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)
*/

3.execution

1) 기본사용법

(1) 공식 문서 문법

  • 메소드 실행 조인 포인트를 매칭
  • ?는 생략할 수 있음
  • *같은 패턴을 지정할 수 있음
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
          throws-pattern?)
          
execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)

 

(2-1) 가장 정확한 포인트컷 - exactmatch 추가

  • MemberServiceImpl.hello(String)메서드와 가장 정확하게 모든 내용이 매칭되는 표현식
  • pointcut.setExpression을 통해서 포인트컷 표현식을 적용하고, pointcut.matches(메서드, 대상 클래스)를 입력하여 메서드와 적용한 포인트컷의 매칭 여부를 true, false로 반환함
@Test
void exactMatch() {
    pointcut.setExpression("execution(public String hello.aop.member.MemberServiceImpl.hello(String))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

 

(2-2) 매칭 조건

  • MemberServiceImpl.hello(String)의 메서드와 포인트컷 표현식의 모든 내용이 100%일치
  • 접근 제어자?: public
  • 반환타입: String
  • 선언타입?: hello.aop.member.MemberServiceImpl
  • 메서드이름: hello
  • 파라미터:(String)
  • 예외?: 생략

(3-1) 가장 많이 생략한 포인트컷

  • 생략할 수 있는 매칭조건을 전부 생략한 포인트컷을 적용
  • 모든 조건을 다 만족하기 때문에 당연히 테스트가 성공함
  • *은 아무값이 들어와도 된다는 뜻이며 파라미터의 ..은 파라미터의 타입과 파라미터의 수가 전부 상관없다는 뜻임
@Test
void allMatch() {
    pointcut.setExpression("execution(* *(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

 

(3-2) 매칭 조건

  • 접근 제어자?: 생략
  • 반환타입: *
  • 선언타입?: 생략
  • 메서드이름: *
  • 파라미터:(..)
  • 예외?: 생략

(4) 메서드 이름 매칭 관련 포인트컷

  • 메서드 이름의 앞이나 뒤에 *을 사용하여 특정 단어가 메서드 이름의 시작, 끝, 또는 중간에 포함될 때도 매칭되도록 설정할 수 있음
@Test
void nameMatch() {
    pointcut.setExpression("execution(* hello(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void nameMatchStar1() {
    pointcut.setExpression("execution(* hel*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void nameMatchStar2() {
    pointcut.setExpression("execution(* *el*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test   // 메서드가 없으므로 실패함
void nameMatchFalse() {
    pointcut.setExpression("execution(* nono(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}

 

(5) 패키지 매칭 관련 포인트컷

  • 패키지를 매칭하여 포인트컷을 지정할 수 있음
  • hello.aop.member.*.* : 첫번째 *은 타입, 두번째 *은 메서드 이름을 *로 지정
  • 패키지로 매칭할 때 점이 하나(.)이면 정확하게 해당 위치의 패키지만 대상으로하고 점이 두개(..)이면 대상 패키지와 하위 패키지도 포함함
// 패키지 이름 매칭
@Test
void packageExactMatch1() {
    pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.hello(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void packageExactMatch2() {
    pointcut.setExpression("execution(* hello.aop.member.*.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test   // 주의, .이 하나면 타입이 위치한 패키지를 정확하게 지정해야함
void packageExactMatchFalse() {
    pointcut.setExpression("execution(* hello.aop.*.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}
@Test // ..(점 2개)면 해당 위치의 패키지와 그 하위 패키지를 포함함
void packageMatchSubPackage1() {
    pointcut.setExpression("execution(* hello.aop.member..*.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void packageMatchSubPackage2() {
    pointcut.setExpression("execution(* hello.aop..*.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

2) 타입 매칭

(1) 부모 타입 허용

  • typeExactMatch()는 포인트컷과 타입 정보가 정확하게 일치하기 때문에 당연히 매칭됨
  • typeMatchSuperType()에 적용한 포인트컷을 보면 MemberService으로 부모타입인 인터페이스를 선언하였음에도 다형성이 적용되어 자식타입도 매칭됨(인터페이스도 되니 상속도 당연히 됨)
// 타입 매칭 - 부모 타입에 있는 메서드만 허용
@Test
void typeExactMatch() {
    pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test   // 부모타입을 포인트컷으로 지정해도 자식타입 메서드와 매칭이 됨
void typeMatchSuperType() {
    pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

 

(2) 부모 타입에 있는 메서드만 허용함

  • typeMatchInternal()은 internal(String)메서드가 MemberServiceImpl에있기 때문에 당연히 매칭에 성공함
  • typeMatchNoSuperTypeMethodFalse()는 pointcut.matches의 결과가 false로 반환되는데, 부모 타입인 MemberService에는 internal(String) 메서드가 없고 MemberServiceImpl에만 있기 때문임
  • 즉, 부모 타입을 포인트컷 표현식에 선언한 경우 부모 타입에서 선언한 메서드가 자식 타입에 있어야만 매칭에 성공함
// 상속받지 않은 internal 메서드를 비교
@Test
void typeMatchInternal() throws NoSuchMethodException {
    pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.*(..))");
    Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);
    assertThat(pointcut.matches(internalMethod, MemberServiceImpl.class)).isTrue();
}


@Test   // 부모 타입에서 선언한 메서드만 매칭에 성공함
void typeMatchNoSuperTypeMethodFalse() throws NoSuchMethodException {
    pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
    Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);
    assertThat(pointcut.matches(internalMethod, MemberServiceImpl.class)).isFalse();
}

 

(3-1) 파라미터 매칭

  • 다양한 케이스의 파라미터 매칭 방법
// 파라미터 매칭
// String 타입의 파라미터 허용
@Test
void argsMatch() {
    pointcut.setExpression("execution(* *(String))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

// 파라미터가 없어야 함
@Test
void argsMatchNoArgs() {
    pointcut.setExpression("execution(* *())");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}

// 정확히 하나의 파라미터를 허용(모든 타입 허용)
@Test
void argsMatchStar() {
    pointcut.setExpression("execution(* *(*))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

// 모든 타입의 파라미터가 허용되며 파라미터의 개수도 상관없음(파라미터가 없어도됨)
@Test
void argsMatchAll() {
    pointcut.setExpression("execution(* *(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

// String 타입으로 시작, 파라미터 개수는 무관하고 모든 타입을 허용
// (String), (String, ...)
@Test
void argsMatchComplex() {
    pointcut.setExpression("execution(* *(String, ..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

 

(3-2) 파라미터 매칭 규칙

  • (String) : 정확하게 String 타입 파라미터를 매칭
  • () : 파라미터가 없어야함
  • (*) : 정확히 하나의 파라미터를 매칭, 타입은 모두 허용
  • (*, *) : 정확히 두개의 파라미터를 매칭, 타입은 모두 허용
  • (..) : 파라미터의 개수도 무관하고 타입도 모두 허용, 파라미터가 없어도됨, (0..*)과 동일
  • (String, ..) : String타입으로 시작해야함, 두번째가 .. String 타입으로 시작만 하면 그이후의 파라미터의 개수와 타입은 무관함
  • 만약 (String, *)이면 파라미터의 개수는 정확히 2개여야하지만 첫번째는 String타입 두번째는 타입은 상관없다는 뜻이며, *대신에 특정 타입을 입력할 수도 있음

4. within

1) 설명 및 예제

(1) 설명

  • 특정 타입 내의 조인 포인트들로 매칭을 제한, 즉 해당 타입이 매칭되면 그안의 메서드(조인 포인트)들이 자동으로 매칭됨
  • execution에서 타입 부분만 사용한다고 보면 됨
  • 필요하면 사용할 수 있으나 execution으로 대부분의 기능이 되며 표현식일 뿐이기에 거의 사용하지는 않음

(2) WithinTest 생성

  • 다른 테스트는 크게 어려운 점은 없으나 주의할 점은 execution과 다르게 표현식에 부모타입을 지정하면 안되고 정확하게 타입이 맞아야함
  • withinSupertypeFalse()메서드를 보면 within으로 표현식을 작성하면 isFalse()로 테스트가 통과하는 것을 알 수 있으며 execution으로 작성한 것은 테스트가 통과함
package hello.aop.pointcut;

public class WithinTest {
   
    // 초기화 코드는 동일

    @Test
    void withinExact() {
        pointcut.setExpression("within(hello.aop.member.MemberServiceImpl)");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
    @Test
    void withinStar() {
        pointcut.setExpression("within(hello.aop.member.*Service*)");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
    @Test
    void withinPackage() {
        pointcut.setExpression("within(hello.aop..*)");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    @Test
    @DisplayName("타겟의 타입에만 직접 적용, 인터페이스를 선정하면 안됨")
    void withinSuperTypeFalse() {
        pointcut.setExpression("within(hello.aop.member.MemberService)");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
    }
    
    @Test
    @DisplayName("execution은 타입 기반이므로 인터페이스를 선정 가능")
    void executionSuperTypeFalse() {
        pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
}

5. args

1) 설명 및 예제

(1) 설명

  • 인자가 주어진 타입의 인스턴스인 조인 포인트로 매칭
  • 기본 문법은 execution의 args부분과 같음
  • execution은 클래스에 선언된 정보를 기반으로 판단하며 파라미터 타입이 정확하게 매칭되어야 하지만 args는 실제 넘어온 파라미터의 객체 인스턴스를 보고 판단하며 부모 타입을 허용함

(2) ArgsTest

  • pointcut(): 테스트를 편리하게 진행하기 위해 포인트컷 자체를 생성하는 메서드를 생성
  • String은 Object, java.io.Serializable의 하위타입인데, 정적으로 클래스에 선언된 정보만 보고 판단하는 execution은 상위 타입을 포인트컷으로 지정하면 매칭에 실패하지만 동적으로 실제 파라미터로 넘어온 객체로 판단하는 args는 매칭에 성공함

** 참고

  • args 지시자는 단독으로 사용되기 보다는 파라미터 바인딩에서 주로 사용됨
package hello.aop.pointcut;

public class ArgsTest {

    Method helloMethod;

    @BeforeEach
    public void init() throws NoSuchMethodException {
        helloMethod = MemberServiceImpl.class.getMethod("hello", String.class);
    }

    // 테스트를 편리하기 위해 포인트컷을 생성하는 메서드를 작성
    private AspectJExpressionPointcut pointcut(String expression) {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression(expression);
        return pointcut;
    }
    
 
    // args()만 false, 나머지는 true
    @Test
    void args() {
        assertThat(pointcut("args(String)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
        assertThat(pointcut("args(Object)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
        assertThat(pointcut("args()").matches(helloMethod, MemberServiceImpl.class)).isFalse();
        assertThat(pointcut("args(..)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
        assertThat(pointcut("args(*)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
        assertThat(pointcut("args(String, ..)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
    
    /**
     * execution(* *(java.io.Serializable)): 메서드의 시그니처로 판단 (정적)
     * * args(java.io.Serializable): 런타임에 전달된 인수로 판단 (동적)
     */
    @Test
    void argsVsExecution() {
        assertThat(pointcut("args(String)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
        assertThat(pointcut("args(java.io.Serializable)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
        assertThat(pointcut("args(Object)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
        
        // execution, 타입이 정확히 일치해야함, (String)만 true 나머지는 false
        assertThat(pointcut("execution(* *(String))").matches(helloMethod, MemberServiceImpl.class)).isTrue();
        assertThat(pointcut("execution(* *(java.io.Serializable))").matches(helloMethod, MemberServiceImpl.class)).isFalse();
        assertThat(pointcut("execution(* *(Object))").matches(helloMethod, MemberServiceImpl.class)).isFalse();
    }
}

6. @target, @within

1) 설명 및 예제

(1) @target

  • 실행 객체의 클래스가 주어진 타입의 애노테이션이 있는 조인 포인트
  • @target(hello.aop.member.annotation.ClassAop)

(2) @within

  • 주어진 애노테이션이 있는 타입 내 조인 포인트
  • @within(hello.aop.member.annotation.ClassAop)

(3) @target vs @within

  • @target은 인스턴스의 모든 메서드를 조인 포인트로 적용
  • @within인 해당 타입 내에 있는 메서드만 조인 포인트로 적용
  • 즉 @target은 부모 클래스의 메서드까지 어드바이스를 전부 적용하고 @within은 자기 자신의 클래스에 정의된 메서드에만 어드바이스를 적용함

(4) AttargetAtwithinTest

  • 자식클래스인 Child에 @ClassAop 애노테이션을 적용하고 Child와 Parent 모두 스프링빈으로 등록
  • @Around에 포인트컷 표현식으로 @target과 @within을 각각 적용한 애스펙트를 @Aspect 애노테이션으로 생성 후 스프링 빈으로 등록하여 AOP가 적용되는 로그를 확인하는 테스트를 진행
package hello.aop.pointcut;

@Slf4j
@Import(AtTargetAtWithinTest.Config.class)
@SpringBootTest
public class AtTargetAtWithinTest {

    @Autowired
    Child child;

    @Test
    void success() {
        log.info("child Proxy={}", child.getClass());
        child.childMethod();
        child.parentMethod();
    }

    static class Config {
        @Bean
        public Parent parent() {
            return new Parent();
        }
        @Bean
        public Child child() {
            return new Child();
        }
        @Bean
        public AtTargetAtWithinAspect attargetAtWithinAspect() {
            return new AtTargetAtWithinAspect();
        }
    }

    static class Parent {
        public void parentMethod() {}       // 부모 클래스에만 있는 메서드
    }

    @ClassAop
    static class Child extends Parent {
        public void childMethod() {}
    }

    @Slf4j
    @Aspect
    static class AtTargetAtWithinAspect {
        // @target: 인스턴스 기준으로 모든 메서드의 조인 포인트를 선정, 부모 타입의 메서드도 적용
        @Around("execution(* hello.aop..*(..)) && @target(hello.aop.member.annotation.ClassAop)")
        public Object atTarget(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[@target] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        // @within: 선택된 클래스 내부에 있는 메서드만 조인 포인트로 선정, 부모 타입의 메서드는 적용 되지 않음
        @Around("execution(* hello.aop..*(..)) && @within(hello.aop.member.annotation.ClassAop)")
        public Object atWithin(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[@within] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}

 

(5) 실행 결과

  • 실행 결과를 보면 parentMethod()는 Parent 클래스에만 정의되어있고 Child 클래스에는 정의되어있지 않기 때문에 @within에서 AOP 적용 대상이 되지 않은것을 로그로 확인할 수 있음
  • @target은 부모클래스의 메서드까지도 AOP가 적용되어 로그가 전부 출력되는 것을 확인할 수 있음
child Proxy=class hello.aop.pointcut.AtTargetAtWithinTest$Child$$SpringCGLIB$$0
[@target] void hello.aop.pointcut.AtTargetAtWithinTest$Child.childMethod()
[@within] void hello.aop.pointcut.AtTargetAtWithinTest$Child.childMethod()
[@target] void hello.aop.pointcut.AtTargetAtWithinTest$Parent.parentMethod()

 

** 참고

  • @target, @within 지시자도 args와 마찬가지로 파라미터 바인딩에서 함께 사용됨
  • @target, args, @args는 단독으로 사용하면 안됨
  • @Around에서 적용한 포인트컷을 확인해보면 execution(* hello.aop.*(..))를 통해서 AOP가 적용되는 대상을 줄여주었음
  • args, @args, @target은 실제 객체 인스턴스가 생성되고 실행될 때 어드바이스 적용 여부를 확인할 수 있음
  • 실행 시점에 일어나는 포인트컷 적용 여부도 결국 프록시가 있어야 실행 시점에 판단할 수 있으므로 프록시가 없다면 판단 자체가 불가능함
  • 스프링 컨테이너가 프록시를 생성하는 시점은 스프링 컨테이너가 만들어지는 애플리케이션 로딩 시점에 적용할 수 있으므로 args, @args, @target 같은 포인트컷 지시자가 있으면 스프링은 모든 스프링빈에 AOP를 적용하려고 시도함
  • 이렇게 모든 스프링 빈에 AOP 프록시를 적용하려고 하면 스프링이 내부에서 사용한 빈 중에는 final로 지정된 빈들도 있기 때문에 이런 빈들은 프록시 생성이 되지 않으므로 오류가 발생할 수 있음
  • 따라서 이러한 표현식은 최대한 프록시 적용 대상을 축소하는 표현식과 함께 사용해야함

@Around에 @target만 적용하고 실행했을 때의 에러 발생한 모습, Cannot subclass final class... 로 에러메세지가 출력됨

 


7. @annotation, @args

1) 설명 및 예제

(1) @annotation

  • 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭
  • @annotation(hello.aop.member.annotation.MethodAop)

(2) AtAnnotationTest

  • MemberService의 구현체인 MemberServiceImpl의 hello 메서드에 적용한 @MethodAop를 조인포인트로 적용하는 테스트
  • @annotation으로 특정 애노테이션을 조인포인트로 지정하여 편리하게 포인트컷을 적용할 수 있어 간혹 사용함
package hello.aop.pointcut;

@Slf4j
@Import(AtAnnotationTest.AtAnnotationAspect.class)
@SpringBootTest
public class AtAnnotationTest {

    @Autowired
    MemberService memberService;

    @Test
    void success() {
        log.info("memberService Proxy={}", memberService.getClass());
        memberService.hello("helloA");
    }

    @Slf4j
    @Aspect
    static class AtAnnotationAspect {

        @Around("@annotation(hello.aop.member.annotation.MethodAop)")
        public Object doAtAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[@annotation] {} ", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}

/* 실행결과
memberService Proxy=class hello.aop.member.MemberServiceImpl$$SpringCGLIB$$0
[@annotation] String hello.aop.member.MemberServiceImpl.hello(String) 
*/

 

(3) @args

  • 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트
  • 전달된 인수의 런타임 타입에 @Check 애노테이션이 있는 경우에 매칭이 됨
  • @args(test.Check)
  • 거의 사용하지 않음

8. bean

1) 설명 및 예제

(1) 설명

  • 스프링 전용 포인트컷 지시자, 빈의 이름으로 지정함
  • 스프링 빈의 이름으로 AOP 적용 여부를 지정할 수 있으며 스프링에서만 사용할 수 있으므로 AspectJ에서는 사용할 수 없음
  • bean(orderService) || bean(*Repository)처럼 적용할 수 있으며 *과 같은 패턴을 사용할 수 있음

(2) BeanTest

  • @Around에 bean(빈이름)으로 포인트컷을 지정하여 적용하여 실행해보면 OrderService와 OrderRepository에 AOP가 정상적으로 적용되어 로그가 출력된 것을 확인할 수 있음
  • 빈이 변하지 않는 상황에서는 사용할 수 있겠으나 실무에서 주로 사용하지는 않음
package hello.aop.pointcut;

@Slf4j
@Import(BeanTest.BeanAspect.class)
@SpringBootTest
public class BeanTest {

    @Autowired
    OrderService orderService;

    @Test
    void success() {
        log.info("bean {}", orderService.getClass());
        orderService.orderItem("itemA");
    }

    @Aspect
    static class BeanAspect {
        @Around("bean(orderService) || bean(*Repository)")
        public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[bean] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}

/* 실행 로그
bean class hello.aop.order.OrderService$$SpringCGLIB$$0
[bean] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[bean] String hello.aop.order.OrderRepository.save(String)
[orderRepository] 실행
*/

9. 매개변수 전달

1) 어드바이스에 매개변수를 전달

(1) 매개변수를 전달 가능한 포인트컷 표현식

  • this, target, args, @target, @within, @annotation, @args

(2) 사용방법

  • 포인트컷의 이름과 매개변수의 이름을 맞추어야 함
  • 타입이 메서드에 지정한 타입으로 제한됨
  • 아래의 예제에서는 arg로 이름을 맞추었고 메서드의 타입이 String으로 되어있기 때문에 args(String, ..) 처럼 정의된다고 이해하면 됨
@Before("allMember() && args(arg,..)")
public void logArgs3(String arg) {
   log.info("[logArgs3] arg={}", arg);
}

 

(3) ParameterTest

  • logArgs1: joinPoint.getArgs()[0]으로 매개변수를 전달 받을 수 있음
  • logArgs2: args(arg, ..)를 사용하여 logArgs1보다 코드를 더 간결하게 전달 받음
  • logArgs3: @Before를 사용하고, 메서드의 매개변수 타입을 String으로 제한, logArgs메서드들 중에서 가장 깔끔함
  • thisArgs: this를 사용하면 프록시 객체를 전달 받음
  • targetArgs: target을 사용하면 실제 객체를 전달받음
  • atTarget, atWithin: @target과 @within은 타입의 애노테이션을 전달받을 수 있음(적용 범위의 차이는 위에서 설명함)
  • atAnnotation: @annotation으로 메서드의 애노테이션을 전달받을 수 있으며 MethodAop에는 값이 선언되어있어 해당 값이 로그로 출력됨(@target, @within도 애노테이션의 속성값을 전달할 수 있음)
package hello.aop.pointcut;

@Slf4j
@Import(ParameterTest.ParameterAspect.class)
@SpringBootTest
public class ParameterTest {

    @Autowired
    MemberService memberService;

    @Test
    void success() {
        log.info("memberService Proxy={}", memberService.getClass());
        memberService.hello("helloA");
    }

    @Slf4j
    @Aspect
    static class ParameterAspect {

        @Pointcut("execution(* hello.aop.member..*.*(..))")
        private void allMember() {
        }

        @Around("allMember()")
        public Object logArgs1(ProceedingJoinPoint joinPoint) throws Throwable {
            Object arg1 = joinPoint.getArgs()[0];
            log.info("[logArgs1]{}, arg={}", joinPoint.getSignature(), arg1);
            return joinPoint.proceed();
        }

        // args(arg, ..) 적용
        @Around("allMember() && args(arg,..)")
        public Object logArgs2(ProceedingJoinPoint joinPoint, Object arg) throws Throwable {
            log.info("[logArgs2]{}, arg={}", joinPoint.getSignature(), arg);
            return joinPoint.proceed();
        }

        // 메서드 타입이 String, String 으로 타입이 제한됨
        @Before("allMember() && args(arg,..)")
        public void logArgs3(String arg) {
            log.info("[logArgs3] arg={}", arg);
        }

        // 이후에 배울 this 적용, 프록시 객체가 전달됨
        @Before("allMember() && this(obj)")
        public void thisArgs(JoinPoint joinPoint, MemberService obj) {
            log.info("[this]{}, obj={}", joinPoint.getSignature(), obj.getClass());
        }

        // 이후에 배울 target 적용, 실제 객체가 전달됨
        @Before("allMember() && target(obj)")
        public void targetArgs(JoinPoint joinPoint, MemberService obj) {
            log.info("[target]{}, obj={}", joinPoint.getSignature(), obj.getClass());
        }

        // annotation 정보를 가져옴, @target 적용
        @Before("allMember() && @target(annotation)")
        public void atTarget(JoinPoint joinPoint, ClassAop annotation) {
            log.info("[@target]{}, obj={}", joinPoint.getSignature(), annotation);
        }

        // annotation 정보를 가져옴, @within 적용
        @Before("allMember() && @within(annotation)")
        public void atWithin(JoinPoint joinPoint, ClassAop annotation) {
            log.info("[@within]{}, obj={}", joinPoint.getSignature(), annotation);
        }

        // 메서드에 적용된 annotation 정보를 가져옴, @annotation 적용
        @Before("allMember() && @annotation(annotation)")
        public void atAnnotation(JoinPoint joinPoint, MethodAop annotation) {
            log.info("[@annotation]{}, annotationValue={}", joinPoint.getSignature(), annotation);
        }
    }
}

 

(4) 실행 결과

memberService Proxy=class hello.aop.member.MemberServiceImpl$$SpringCGLIB$$0
[logArgs1]String hello.aop.member.MemberServiceImpl.hello(String), arg=helloA
[logArgs2]String hello.aop.member.MemberServiceImpl.hello(String), arg=helloA
[@annotation]String hello.aop.member.MemberServiceImpl.hello(String), annotationValue=@hello.aop.member.annotation.MethodAop("test value")
[@target]String hello.aop.member.MemberServiceImpl.hello(String), obj=@hello.aop.member.annotation.ClassAop()
[@within]String hello.aop.member.MemberServiceImpl.hello(String), obj=@hello.aop.member.annotation.ClassAop()
[logArgs3] arg=helloA
[target]String hello.aop.member.MemberServiceImpl.hello(String), obj=class hello.aop.member.MemberServiceImpl
[this]String hello.aop.member.MemberServiceImpl.hello(String), obj=class hello.aop.member.MemberServiceImpl$$SpringCGLIB$$0

 


10. this, target

1) 설명

(1) this

  • 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
  • this(hello.aop.member.MemberService)

(2) target

  • target객체(스프링 AOP 프록시가 가리키는 실제 대상)를 대상으로 하는 조인 포인트
  • target(hello.aop.member.MemberService)

(3) 공통 적용

  • *같은 패턴을 사용할 수 없음
  • 부모 타입을 허용함

(4) this vs target

  • 스프링에서 AOP를 적용하면 실제 target 객체 대신에 프록시 객체가 스프링 빈으로 등록됨
  • this는 스프링 빈으로 등록되어 있는 프록시 객체를 대상으로 포인트컷을 매칭함
  • target은 실제 target객체를 대상으로 포인트컷으로 매칭함

(5) 프록시 생성 방식에 따른 차이

  • 스프링은 프록시를 생성할 때 JDK 동적 프록시와 CGLIB를 선택할 수 있는데, 둘은 프록시를 생성하는 방식이 다르기 때문에 차이가 발생함
  • JDK 동적 프록시는 인터페이스가 필수이고 인터페이스를 구현한 프록시 객체를 생성함
  • CGLIB는 인터페이스가 있어도 구체 클래스를 상속받아서 프록시 객체를 생성함

JDK 동적 프록시와 CGLIB 프록시 차이

 

(6) JDK 동적 프록시 적용시 this, target 동작

  • MemberService 인터페이스에 this, target을 적용하면 둘다 부모 타입을 허용하기 때문에 AOP가 적용
  • MemberServiceImpl 구체 클래스에 지정할 경우에 this는 AOP가 적용되지 않음
  • this는 프록시 객체를 대상으로 판단하는데 JDK 동적 프록시로 만들어진 proxy 객체는 MemberService 인터페이스를 기반으로 구현된 새로운 클래스이므로 MemberServiceImpl을 알지 못하기 때문임
  • 반면 target은 실제 target객체를 보고 판단하기 때문에 MemberServiceImpl이 실제 target이기 때문에 AOP 적용 대상이 됨

(7) CGLIB 프록시 적용시 this, target 동작

  • MemberService 인터페이스에 적용하든, MemberServiceImpl 구체클래스에 적용 하든 this, target 모두 AOP가 적용
  • 인터페이스일경우에는 this, target 둘다 부모 타입을 허용하기 때문에 AOP가 적용됨
  • 구체클래스인경우에는 CGLIB로 만들어진 프록시 객체가 구체클래스인 MemberServiceImpl을 상속받아서 만들어졌기 때문에 this로도 적용이 되며, target은 동작 메커니즘상 당연히 적용이됨

(8) 정리

  • 프록시를 대상으로하는 this의 경우 구체 클래스를 지정하면 프록시 생성 전략에 따라서 다른 결과가 나올 수 있다는 점이 포인트임

2) 예제

(1) ThisTargetTest

  • 위에서 설명한 상황을 모두 예제로 구현하여 this와 target으로 인터페이스와 구체클래스를 각각 포인트컷으로 적용한 테스트를 시행
  • @SpringBoot(properties = "spring.aop.proxy-target-class=false") : application.properties에 적용하는 대신에 해당 테스트에서만 설정을 임시로 적용할 수 있으며, 이렇게 하면 각 테스트마다 다른 설정을 손쉽게 할 수 있음
  • spring.aop.proxy-target-class=false: 스프링이 AOP 프록시를 생성할 때 JDK 동적 프록시를 우선 생성하며 인터페이스가 없으면 CGLIB를 사용함 즉, 둘다 사용함
  • spring.aop.proxy-target-class=true: 스프링이 AOP 프록시를 생성할 때 CGLIB 프록시를 생성하며 스프링 부트로 프로젝트를 생성했다면 이설정은 기본값으로 적용되어 기본으로 CGLIB를 사용함
  • 이부분은 강의 제일 마지막에서 스프링 부트가 왜 이런 전략을 선택했는지에 대해서 자세히 설명함
package hello.aop.pointcut;

/**
 * 스프링 부트로 프로젝트를 생성하면 기본으로 프록시를 CGLIB 를 사용함
 * application.properties
 * spring.aop.proxy-target-class=true CGLIB 사용(기본값)
 * spring.aop.proxy-target-class=false JDK 동적 프록시, CGLIB 사용
 */
@Slf4j
@Import(ThisTargetTest.ThisTargetAspect.class)
// 테스트에서도 바로 설정할 수 있음
@SpringBootTest(properties = "spring.aop.proxy-target-class=false")     // JDK 동적 프록시, CGLIB 동작
//@SpringBootTest(properties = "spring.aop.proxy-target-class=true")    // CGLIB 로만 동작
public class ThisTargetTest {

    @Autowired
    MemberService memberService;

    @Test
    void success() {
        log.info("memberService Proxy={}", memberService.getClass());
        memberService.hello("helloA");
    }

    @Slf4j
    @Aspect
    static class ThisTargetAspect {

        // 인터페이스, this - 부모 타입 허용
        @Around("this(hello.aop.member.MemberService)")
        public Object doThisInterface(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[this-interface] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        // 인터페이스, target - 부모 타입 허용
        @Around("target(hello.aop.member.MemberService)")
        public Object doTargetInterface(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[target-interface] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        // 구체클래스, this - AOP 적용 안됨
        @Around("this(hello.aop.member.MemberServiceImpl)")
        public Object doThis(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[this-impl] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        // 구체클래스, target - 부모 타입 허용
        @Around("target(hello.aop.member.MemberServiceImpl)")
        public Object doTarget(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[target-impl] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}

 

(2) 실행결과

  • JDK 동적 프록시로 동작하도록 설정 후 테스트를 실행해보면 this-impl 로그는 출력되지 않는것을 확인할 수 있음
memberService Proxy=class jdk.proxy3.$Proxy53
[target-impl] String hello.aop.member.MemberService.hello(String)
[target-interface] String hello.aop.member.MemberService.hello(String)
[this-interface] String hello.aop.member.MemberService.hello(String)

 

(3) CGLIB로 동작 설정 후 실행

  • CGLIB 프록시로 동작하면 this로 구체클래스를 포인트컷으로 적용하여도 프록시가 구체클래스를 상속받아서 만들어졌기 때문에 this-impl 로그도 AOP가 적용되어 로그가 출력된 모습을 확인할 수 있음
memberService Proxy=class hello.aop.member.MemberServiceImpl$$SpringCGLIB$$0
[target-impl] String hello.aop.member.MemberServiceImpl.hello(String)
[target-interface] String hello.aop.member.MemberServiceImpl.hello(String)
[this-impl] String hello.aop.member.MemberServiceImpl.hello(String)
[this-interface] String hello.aop.member.MemberServiceImpl.hello(String)

 

** 참고

  • this, target 지시자는 단독으로 사용되기 보다는 파라미터 바인딩에서 주로 사용됨
  • 해당 내용이 이해가 잘 되지 않으면 AOP 실무 주의사항에서 프록시 기술과 한계를 듣고 다시한번 복습해보면 좋음