728x90
반응형
SMALL
SMALL

참고자료

 

스프링 핵심 원리 - 고급편 강의 | 김영한 - 인프런

김영한 | 스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., 핵심 디자인 패턴, 쓰레드 로컬, 스프링 AOP스프링의 3가지 핵심 고급 개념 이해하기

www.inflearn.com

 

포인트컷 지시자

포인트컷 지시자에 대해서 자세히 알아보자. AspectJ는 포인트컷을 편리하게 표현하기 위한 특별한 표현식을 제공한다. 예를 들면 이렇다.

@Pointcut("execution(* hello.aop.order..*(..))")
  • 포인트컷 표현식은 execution과 같은 포인트컷 지시자(Pointcut Designator)로 시작한다. 줄여서 PCD라고도 한다.

포인트컷 지시자 종류

포인트컷 지시자의 종류는 다음과 같다.

  • execution: 메서드 실행 조인 포인트를 매칭한다. 스프링 AOP에서 가장 많이 사용한다.
  • within: 특정 타입(클래스, 인터페이스) 내의 조인 포인트를 매칭한다.
  • args: 인자가 주어진 타입의 인스턴스인 조인 포인트
  • this: 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
  • target: Target 객체(스프링 AOP 프록시가 가리키는 실제 대상)를 대상으로 하는 조인 포인트
  • @target: 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트
  • @within: 주어진 애노테이션이 있는 타입 내 조인 포인트
  • @annotation: 주어진 애노테이션을 가지고 있는 메서드를 조인 포인트로 매칭
  • @args: 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트
  • bean: 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정한다.

말로만 보면 이해하기가 정말 난해하다. 그래서 코드로 하나씩 뜯어보자. execution이 가장 많이 사용되고 나머지는 거의 사용하지 않는다. 따라서 execution을 중점적으로 이해해보자.

 

예제 만들기

ClassAop

package cwchoiit.springadvanced.aop.member.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

MethodAop

package cwchoiit.springadvanced.aop.member.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

    String value() default "";
}

 

  • 우선, 두 개의 애노테이션을 만들었다. 하나는 클래스 레벨에 적용할 애노테이션이고 하나는 메서드 레벨에 적용할 애노테이션이다.
  • 애노테이션을 만드려면 기본적으로 두 개의 애노테이션이 필요하다. @Target, @Retention.
  • @Target은 이 애노테이션이 어디에 달릴지를 설명하는 애노테이션이다. ElementType.TYPE으로 설정하면 클래스 또는 인터페이스에 레벨에 적용할 애노테이션이고 ElementType.METHOD는 메서드 레벨에 적용할 애노테이션이다.
  • @Retention은 이 애노테이션이 살아있는 레벨을 말한다고 보면 된다. RetentionPolicy.RUNTIME으로 설정하면 런타임에도 해당 애노테이션은 살아 있는 상태로 남아있다. 그래서, 동적으로 애노테이션을 읽을 수 있다. RUNTIME말고 SOURCE도 있는데 이는 컴파일하면 컴파일된 파일에서 애노테이션이 보이지 않고 사라진다. 그래서 동적으로 이 애노테이션을 읽을 수 없다.
  • 그리고 MethodAop 애노테이션은 value() 라는 값을 가질 수 있다. 값의 형태는 문자열이다.

MemberService

package cwchoiit.springadvanced.aop.member;

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

 

MemberServiceImpl

package cwchoiit.springadvanced.aop.member;

import cwchoiit.springadvanced.aop.member.annotation.ClassAop;
import cwchoiit.springadvanced.aop.member.annotation.MethodAop;
import org.springframework.stereotype.Component;

@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";
    }
}

 

  • 이번엔 인터페이스와 그 인터페이스를 구현한 클래스를 만들었다. 간단하게 하나의 메서드를 가지는 인터페이스(MemberService)와 그를 구현한 MemberServiceImpl이 있고, 이 구현 클래스는 @ClassAop 애노테이션을 달았다. 그리고 이 구현 클래스 내부에 hello(String param)@MethodAop 애노테이션이 달려있다. 

ExecutionTest

package cwchoiit.springadvanced.aop.pointcut;

import cwchoiit.springadvanced.aop.member.MemberServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;

import java.lang.reflect.Method;

@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() {
        log.info("helloMethod = {}", helloMethod);
    }
}

 

  • 테스트 코드다. 리플렉션을 이용해서 구현한 MemberServiceImplhello 메서드를 가져온다. 각 테스트의 실행마다 그 바로 직전에 리플렉션을 활용해서 메서드를 가져오기 위해 @BeforeEach를 사용했다. 
  • AspectJExpressionPointcut은 포인트컷 표현식을 처리해주는 클래스다. 여기에 포인트컷 표현식을 지정하면 된다. 이 클래스는 상위에 Pointcut 인터페이스를 가진다. 
  • 간단하게 printMethod()를 실행해보면 다음과 같은 결과를 얻는다.

실행 결과

helloMethod = public java.lang.String cwchoiit.springadvanced.aop.member.MemberServiceImpl.hello(java.lang.String)
  • 이 결과 어디서 많이 본 것 같다. 맞다. 이렇게 메서드 정보를 포인트컷 표현식으로 매칭해서 포인트컷 대상을 찾을 것이다. 

execution - 1

가장 중요한 포인트컷 지시자이다. 이 execution은 다음과 같이 사용한다.

execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)

 

  • execution은 메서드 실행 조인 포인트를 매칭한다. 그래서 결국 모든 메서드들 중 이 표현식에 일치하는 메서드들이 AOP로 적용된다. 위 표현 방식에서 '?'가 있는 것은 생략이 가능하다는 뜻이다.
  • 그럼 하나씩 천천히 알아보자. 가장 정확한(자세한) 포인트 컷으로 표현해보자. 

가장 정확한(자세한) 포인트컷

execution(public String cwchoiit.springadvanced.aop.member.MemberServiceImpl.hello(String))
  • 접근제어자?: public
  • 반환 타입: String
  • 선언 타입?: cwchoiit.springadvanced.aop.member.MemberServiceImpl
  • 메서드이름: hello
  • 파라미터: (String)
  • 예외?: 생략

이렇게 예외를 제외하고 모든 키워드를 작성했다. hello 메서드에 예외는 없기 때문에 제외했다.

이렇게 포인트컷을 지정하고 해당 포인트컷과 hello 메서드가 매치하는지 확인하는 테스트 코드를 작성해보자.

@Test
void exactMatch() {
    pointcut.setExpression("execution(public String cwchoiit.springadvanced.aop.member.MemberServiceImpl.hello(String))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

 

 

가장 많이 생략한 포인트컷

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

'*'은 와일드카드로 모든것을 허용한다는 의미로 받아들이면 될 것 같다. 여기서 생략을 할 수 없는 필수 키워드인 반환 타입, 메서드명, 파라미터만을 작성했다. 반환 타입은 전체(*)이며 메서드명 또한 어떠한 것도 상관 없다는 의미의 '*'이고 파라미터는 어떤 파라미터라도 상관없다는 의미의 (..)를 사용했다. (..)는 파라미터가 없거나 여러개거나 한개거나 어떠한 상태여도 상관이 없다는 의미이다.

 

이런 포인트컷을 사용해서 메서드가 일치하는지 확인해보자.

@Test
    void allMatch() {
        pointcut.setExpression("execution(* *(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
  • 결과는 당연히 테스트 통과한다.

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

메서드 이름과 관련된 포인트 컷을 여러개 확인해보자. 메서드 이름에도 '*'를 사용할 수 있다.

@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(* none(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}
  • 메서드 이름 앞뒤로 *을 사용해서 매칭할 수 있다.

패키지 매칭 관련 포인트컷

패키지 이름과 관련된 포인트 컷도 여러개 확인해보자. 주의할 점은 하위 패키지 전부를 허용하고 싶을 땐 '..'을 사용해야 한다. (점 두개)

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

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

@Test
void packageExactFalse() {
    pointcut.setExpression("execution(* cwchoiit.springadvanced.aop.*.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}

@Test
void packageMatchSubPackage1() {
    pointcut.setExpression("execution(* cwchoiit.springadvanced.aop..*.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
  • packageExactFalse()를 확인해보면 cwchoiit.springadvanced.aop.*.*(..)로 되어 있는데 이는 하위 패키지도 포함하는게 아니다. 즉, 정확히 cwchoiit.springadvanced.aop경로의 모든 타입(인터페이스, 클래스)의 모든 메서드를 지정하는 포인트 컷이다. 하위 패키지도 포함하려면 packageMatchSubPackage1()처럼 cwchoiit.springadvanced.aop..*.*(..)로 작성해야 한다. 

 

execution - 2

타입 매칭 포인트컷 

타입 매칭 - 정확히 매칭

 

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

 

  • 이처럼 정확히 패키지 + 타입(클래스)이 일치하게 포인트컷을 지정할 수 있다. 근데 한가지 조심할 게 있다. 부모 타입은 어떻게 될까?
  • 그러니까 MemberServiceImpl은 상위에 MemberService 인터페이스가 있다. 그럼 포인트컷 표현식에 부모 타입을 작성했을 때 저 hello 메서드는 포인트컷 조건에 만족할까? 결론부터 말하면 만족한다.

타입 매칭 - 부모 타입 매칭

@Test
void typeExactMatchSuperType() {
    pointcut.setExpression("execution(* cwchoiit.springadvanced.aop.member.MemberService.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
  • 자식은 부모에 들어가는 게 가능하기 때문에, 포인트컷 표현식을 부모로 설정하면 자식 클래스들은 포인트컷을 만족한다. 단, 인터페이스에서 선언된 메서드에 한한다. 이 말은 무슨말이냐면 부모일지언정 부모에 선언된 메서드가 아니라 자식 내부적으로만 가지고 있는 메서드는 포인트컷을 만족하지 못한다는 말이다.
  • 위에서 MemberServiceMemberServiceImpl을 보면 부모인 인터페이스에는 hello 메서드만 있고 internal은 없다. 자식인 구현 클래스에는 internal 이라는 내부 메서드가 있다. 이 땐 부모 타입으로 포인트컷을 지정하면 자식 내부적으로만 가지고 있는 메서드에는 포인트 컷 조건이 만족하지 않는다.

타입 매칭 - 부모 타입 매칭이지만, 부모 타입에 있는 메서드만 허용한다.

@Test
void typeExactMatchInternal() throws NoSuchMethodException {
    pointcut.setExpression("execution(* cwchoiit.springadvanced.aop.member.MemberService.*(..))");
    Method method = MemberServiceImpl.class.getMethod("internal", String.class);
    assertThat(pointcut.matches(method, MemberServiceImpl.class)).isFalse();
}
  • isFalse()로 검증하면 테스트에 성공한다.

파라미터 매칭 포인트컷

/**
 * String 타입의 파라미터 허용
 */
@Test
void argsMatch() {
    pointcut.setExpression("execution(* *(String))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

/**
 * 파라미터가 없음
 */
@Test
void noArgsMatch() {
    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 타입으로 시작, 숫자와 무관하게 모든 파라미터, 모든 타입 허용. 없어도 된다.
 */
@Test
void argsMatchComplex() {
    pointcut.setExpression("execution(* *(String, ..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
  • (String) → 정확하게 String 타입 파라미터
  • () → 파라미터가 없어야 한다.
  • (*) → 정확히 하나의 파라미터여야하고, 모든 타입을 허용한다.
  • (*, *) → 정확히 두개의 파라미터여야하고, 모든 타입을 허용한다.
  • (..) → 숫자와 무관하게 모든 파라미터, 모든 타입을 허용한다. 파라미터가 없어도 상관없다.
  • (String, ..)String 타입의 파라미터로 시작하고 그 이후에는 숫자와 무관하게 모든 파라미터, 모든 타입을 허용한다. String 파라미터 이후에 파라미터가 없어도 된다.

 

within

within 지시자는 특정 타입 내의 조인 포인트들로 매칭을 제한한다. 이 말만 보면 무슨말인지 잘 모르겠다. 쉽게 말하면 작성한 타입(클래스, 인터페이스)이 매칭되면 그 안의 메서드들이 자동으로 매치된다. 참고로, 이건 거의 안쓴다. 거의 대부분은 execution으로 전부 해결이 가능하기 때문도 있고 부모 타입으로 매칭을 해야할 때도 있기 때문에 이건 그냥 알아만 두자!

 

WithinTest

package cwchoiit.springadvanced.aop.pointcut;

import cwchoiit.springadvanced.aop.member.MemberServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;

import java.lang.reflect.Method;

import static org.assertj.core.api.Assertions.assertThat;

public class WithinTest {

    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    Method helloMethod;

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

    @Test
    void withinExact() {
        pointcut.setExpression("within(cwchoiit.springadvanced.aop.member.MemberServiceImpl)");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

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

    @Test
    void withinSubPackage() {
        pointcut.setExpression("within(cwchoiit.springadvanced.aop..*)");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    /**
     * within 의 경우, execution 과는 반대로 부모 타입으로는 안된다. 
     */
    @Test
    void withinSuperTypeFalse() {
        pointcut.setExpression("within(cwchoiit.springadvanced.aop.member.MemberService)");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
    }
}

 

주의

주의할 부분이 있다. 마지막 테스트 코드인 withinSuperTypeFalse()를 보면, 표현식에 부모 타입을 지정하면 안된다. 정확하게 타입이 맞아야 한다. 이 점이 execution과 다른 점이다.

 

args

인자가 주어진 타입의 인스턴스인 조인 포인트로 매칭. 말이 또 어려운데 쉽게 말해 파라미터가 매치되는 녀석들이 다 조인 포인트가 된다고 보면 된다. 아래 코드를 보면 바로 이해가 될 것이다. 기본 문법은 executionargs 부분과 같다. 참고로, 이 또한 그렇게 중요한게 아니다. 그냥 참고만 해도 무방하다.

 

executionargs의 차이점

  • execution은 파라미터 타입이 정확하게 매칭되어야 한다. execution은 클래스에 선언된 정보를 기반으로 판단한다.
  • args는 부모 타입을 허용한다. args는 실제 넘어온 파라미터 객체 인스턴스를 보고 판단한다.

 

ArgsTest

package cwchoiit.springadvanced.aop.pointcut;

import cwchoiit.springadvanced.aop.member.MemberServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;

import java.lang.reflect.Method;

import static org.assertj.core.api.Assertions.assertThat;

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;
    }

    @Test
    void args() {
        // hello(String)과 매칭
        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 -> 메서드의 시그니처로 판단 (정적)
     * args -> 런타임에 전달된 인수로 판단 (동적)
     */
    @Test
    void argsVsExecution() {
        // Args (Args 는 상위 타입을 허용한다)
        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 (Execution 은 상위 타입을 허용하지 않고 딱 정확하게 선언해야 한다)
        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();
    }
}
  • pointcut() → AspectJExpressionPointcut에 포인트컷은 한번만 지정할 수 있다. 이번 테스트에서는 포인트컷을 바꿔가면서 테스트 할 목적으로 포인트컷 자체를 생성하는 메서드를 만들었다.
  • 자바가 기본으로 제공하는 StringObject, Serializable의 하위 타입이다. 
  • 정적으로 클래스에 선언된 정보만 보고 판단하는 execution(* *(Object))은 매칭에 실패한다.
  • 동적으로 실제 파라미터로 넘어온 객체 인스턴스로 판단하는 args(Object)는 매칭에 성공한다. 
  • 쉽게 말해, args는 부모 타입도 허용하고, execution은 부모 타입은 허용하지 않는다고 기억하면 된다.
  • 참고로, args 지시자는 단독으로 사용되기 보다는 뒤에서 설명할 파라미터 바인딩에서 주로 사용된다.

 

@target, @within

정의

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

사실 이 지시자도 그렇게 중요하지도 않고 정의만 보고서는 뭔 말인지 감이 잘 안오지만 코드로 보면 간단하다. 우선 둘 모두 타입에 있는 애노테이션으로 AOP 적용 여부를 판단한다. 아, 그리고 앞에 @ 붙은 지시자(@target, @within, @annotation, ...)들은 애노테이션과 관련된 지시자라고 생각하면 된다.

@target(hello.aop.member.annotation.ClassAop)
@within(hello.aop.member.annotation.ClassAop)

 

@ClassAop
class Target {

}

 

여기서 두 개의 차이는 다음과 같다. 

@target은 애노테이션이 달린 클래스의 부모 클래스의 메서드까지 어드바이스를 전부 적용하고, @within은 자기 자신의 클래스에 정의된 메서드만 어드바이스를 적용한다. 

 

 

그래서 한 문장으로 정리를 하자면 @target, @within 둘 모두 애노테이션으로 AOP를 적용하는데 @target의 경우 애노테이션이 달린 클래스와 그 상위 클래스의 메서드 모두에게 어드바이스를 적용하고 @within의 경우 애노테이션이 달린 클래스의 메서드에만 어드바이스를 적용한다.

 

AtTargetAtWithinTest

package cwchoiit.springadvanced.aop.pointcut;

import cwchoiit.springadvanced.aop.member.annotation.ClassAop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;

@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() {
            log.info("[parentMethod] Start");
        }
    }

    @ClassAop
    static class Child extends Parent {
        public void childMethod() {
            log.info("[childMethod] Start");
        }
    }

    @Aspect
    static class AtTargetAtWithinAspect {

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

        // @within: 선택된 클래스 내부에 있는 메서드만 조인 포인트로 선정 = 부모 타입의 메서드는 적용되지 않음
        @Around("execution(* cwchoiit.springadvanced.aop..*(..)) && @within(cwchoiit.springadvanced.aop.member.annotation.ClassAop)")
        public Object atWithin(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[@within] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}
  • Child, Parent 클래스가 있다. Child 클래스는 상위 클래스로 Parent 클래스가 있다. 
  • 두 클래스를 모두 스프링 빈으로 등록한다.
  • 에스팩트가 있고 두 개의 어드바이저가 있다. 하나는 @target, 하나는 @within을 사용하여 만들어진 포인트컷이다.
  • @target@within 모두 같은 애노테이션인 ClassAop 애노테이션이 달린 클래스를 찾아 AOP로 적용한다.
  • @Aspect 역시 스프링 빈으로 등록해야 한다.
  • 스프링 빈으로 등록한 Child 클래스를 테스트 코드에서는 주입받는다.
  • 주입받은 Child 클래스의 childMethod(), parentMethod()를 각각 호출한다. 여기서 parentMethod()는 부모 클래스인 Parent에서 정의된 메서드이다.
  • 결과는 childMethod() 호출 시, @target@within 모두 적용된다. parentMethod() 호출 시 @target만 적용되고 @within은 적용되지 않는다.
  • 아래 실행 결과를 자세히 보자!

실행 결과

child proxy = class cwchoiit.springadvanced.aop.pointcut.AtTargetAtWithinTest$Child$$SpringCGLIB$$0
[@target] void cwchoiit.springadvanced.aop.pointcut.AtTargetAtWithinTest$Child.childMethod()
[@within] void cwchoiit.springadvanced.aop.pointcut.AtTargetAtWithinTest$Child.childMethod()
[childMethod] Start
[@target] void cwchoiit.springadvanced.aop.pointcut.AtTargetAtWithinTest$Parent.parentMethod()
[parentMethod] Start

 

주의
다음 포인트컷 지시자는 단독으로 사용하면 안된다. args, @args, @target
이번 예제를 봐도 execution(* cwchoiit.springadvanced.aop..*(..))를 통해 적용 대상을 줄여준 것을 확인할 수 있다. args, @args, @target은 실제 객체 인스턴스가 생성되고 실행될 때 어드바이스 적용 여부를 확인할 수 있다. 실행 시점에 일어나는 포인트컷 적용 여부도 결국 프록시가 있어야 실행 시점에 판단할 수 있다. 프록시가 없다면 판단 자체가 불가능하다. 그런데 스프링 컨테이너가 프록시를 생성하는 시점은 스프링 컨테이너가 만들어지는 애플리케이션 로딩 시점이다. 실행 시점에 일어나는 포인트컷 적용 여부도 프록시가 있어야 판단이 가능한데 프록시가 없으면 실행 시점에 판단 자체가 불가능하다. 그래서 이 args, @args, @target 과 같은 지시자를 단독으로 사용할 경우, 스프링은 모든 스프링 빈에 AOP를 적용하려고 시도한다. 그런데 문제는 스프링이 내부에서 사용하는 빈 중에는 final로 지정된 빈들도 있기 때문에 프록시를 만들어내질 못하고 오류가 발생한다. 따라서 이러한 표현식은 최대한 프록시 적용 대상을 축소하는 표현식과 함께 사용해야 한다. 

 

 

@annotation, @args

  • @annotation: 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭
  • @args: 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트

@annotation

@annotation은 종종 사용되니 이것을 집중해서 보자.

@annotation(hello.aop.member.annotation.MethodAop)

 

쉽게 말해 메서드에 지정한 애노테이션이 있으면 매칭한다. 다음 코드처럼.

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

 

AtAnnotationTest

package cwchoiit.springadvanced.aop.pointcut;

import cwchoiit.springadvanced.aop.member.MemberService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

@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(cwchoiit.springadvanced.aop.member.annotation.MethodAop)")
        public Object doAtAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[@annotation] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}

 

  • 위 코드에서 @Aspect를 보면 @Around의 포인트컷 지시자로 @annotation을 사용한다.
  • MethodAop라는 애노테이션이 달린 메서드에 이 AOP가 적용된다. 그리고 만든 MemberServiceImplhello()@MethodAop 애노테이션이 있다. 따라서 테스트 success()는 AOP가 적용된 hello()가 호출된다.

실행 결과

memberService Proxy = class cwchoiit.springadvanced.aop.member.MemberServiceImpl$$SpringCGLIB$$0
[@annotation] String cwchoiit.springadvanced.aop.member.MemberServiceImpl.hello(String)

 

bean

  • 스프링 전용 포인트컷 지시자. 빈의 이름으로 지정한다.
  • 스프링 빈의 이름으로 AOP 적용 여부를 지정한다. 이것은 스프링에서만 사용할 수 있는 특별한 지시자이다. 
  • 다음과 같이 작성하면 된다.
bean(orderService) || bean(*Repository)

 

바로 예시 코드로 확인해보자. 그리고 이 지시자 역시 자주 사용되는 지시자는 아니다.

 

BeanTest

package cwchoiit.springadvanced.aop.pointcut;

import cwchoiit.springadvanced.aop.member.MemberService;
import cwchoiit.springadvanced.aop.order.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

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

    @Autowired
    OrderService orderService;

    @Test
    void success() {
        orderService.orderItem("itemA");
    }

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

 

  • BeanAspect를 보면 orderService라는 bean 또는 *Repository라는 bean을 포인트컷의 조건으로 어드바이스를 만든 모습을 확인할 수 있다.
  • 그 후 테스트 success()orderServiceorderItem()을 호출한다. 

실행 결과

[bean] void cwchoiit.springadvanced.aop.order.OrderService.orderItem(String)
[orderService] 실행
[bean] String cwchoiit.springadvanced.aop.order.OrderRepository.save(String)
[orderRepository] 실행

 

 

매개변수 전달 ⭐️

매개변수 전달 - 메서드 파라미터 값 가져오기 (JoinPoint)

어드바이스 쪽에서 메서드의 파라미터를 전달받고 싶을 땐 어떻게 해야 할까? 예를 들어 다음 코드를 보자.

orderService.orderItem("item");

 

  • 이런 코드가 있을 때, 어드바이스가 저 파라미터 "item"을 어떻게 받을 수 있을까? 이를 알아보자.

ParameterTest

package cwchoiit.springadvanced.aop.pointcut;

import cwchoiit.springadvanced.aop.member.MemberService;
import cwchoiit.springadvanced.aop.member.annotation.ClassAop;
import cwchoiit.springadvanced.aop.member.annotation.MethodAop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

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

    @Autowired
    MemberService memberService;

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

    @Aspect
    static class ParameterAspect {

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

        @Around("allMember()")
        public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
            Object arg1 = joinPoint.getArgs()[0];
            log.info("[around] {}, arg = {}", joinPoint.getSignature(), arg1);
            return joinPoint.proceed();
        }
    }
}
  • 첫번째 방법은, JoinPoint로 부터 가져오는 것이다. 다음 코드를 보자.
@Around("allMember()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    Object arg1 = joinPoint.getArgs()[0];
    log.info("[around] {}, arg = {}", joinPoint.getSignature(), arg1);
    return joinPoint.proceed();
}
  • getArgs()를 사용하면 메서드의 파라미터값을 전달 받을 수 있다. 
  • 호출할 때 "helloA"를 파라미터로 넘겼으니 당연히 그 값이 나올 것이다.

실행 결과

[around] String cwchoiit.springadvanced.aop.member.MemberServiceImpl.hello(String), arg = helloA

매개변수 전달 - 메서드 파라미터 값 가져오기 (args)

ParameterTest

package cwchoiit.springadvanced.aop.pointcut;

import cwchoiit.springadvanced.aop.member.MemberService;
import cwchoiit.springadvanced.aop.member.annotation.ClassAop;
import cwchoiit.springadvanced.aop.member.annotation.MethodAop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

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

    @Autowired
    MemberService memberService;

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

    @Aspect
    static class ParameterAspect {

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

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

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

        @Before("allMember() && args(arg, ..)")
        public void before(String arg) {
            log.info("[before] {}", arg);
        }
    }
}
  • 두번째 방법은, 포인트컷 지시자 args를 사용한다. 다음 코드를 보자.
@Around("allMember() && args(arg, ..)")
public Object around(ProceedingJoinPoint joinPoint, Object arg) throws Throwable {
    log.info("[around] {}, arg = {}", joinPoint.getSignature(), arg);
    return joinPoint.proceed();
}

@Before("allMember() && args(arg, ..)")
public void before(String arg) {
    log.info("[before] {}", arg);
}
  • args(arg, ..)은 첫번째 파라미터를 받고 그 이후에 파라미터는 있거나 없거나 신경쓰지 않는다는 뜻이다. 그리고 이 arg를 어드바이스의 파라미터로 이름 그대로(arg) 동일하게 받아야 한다. 
  • @Around를 사용하든, @Before를 사용하든 동일한 방식으로 접근이 가능한데 @AroundProceedingJoinPoint를 반드시 첫번째 파라미터로 받아야 하기 때문에 꼭 필요한 경우가 아니라면 더 깔끔한 @Before를 사용하면 된다. 상황에 따라 달라질 것이다.

실행 결과

[around] String cwchoiit.springadvanced.aop.member.MemberServiceImpl.hello(String), arg = helloA
[before] helloA

 

매개변수 전달 - 실행한 객체 가져오기

이 내용은 뒤에 더 자세히 얘기할 거라서 간단하게 해보겠다. 다음과 같이 this, target 지시자를 사용하면 실행 객체 정보를 가져올 수 있다.

@Before("allMember() && this(obj)")
public void thisArgs(JoinPoint joinPoint, MemberService obj) {
    log.info("[this] {}, obj = {}", joinPoint.getSignature(), obj.getClass());
}

@Before("allMember() && target(obj)")
public void targetArgs(JoinPoint joinPoint, MemberService obj) {
    log.info("[target] {}, obj = {}", joinPoint.getSignature(), obj.getClass());
}

실행 결과

[target] String cwchoiit.springadvanced.aop.member.MemberServiceImpl.hello(String), obj = class cwchoiit.springadvanced.aop.member.MemberServiceImpl
[this] String cwchoiit.springadvanced.aop.member.MemberServiceImpl.hello(String), obj = class cwchoiit.springadvanced.aop.member.MemberServiceImpl$$SpringCGLIB$$0
  • 그런데 주의할 점은, this는 프록시 객체를 전달받고, target은 실제 대상 객체를 전달받는다! 실행 결과를 자세히 보자!

 

매개변수 전달 - 애노테이션 정보와 애노테이션이 가진 값 가져오기

MemberServiceImpl

@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";
    }
}
  • 이렇게 생긴 MemberServiceImpl이 있을 때 저 @ClassAop, @MethodAop 정보가 있는지 없는지 또는 그 애노테이션이 가지고 있는 값인 "test value"를 어떻게 가져오는지 알아보자.
@Before("allMember() && @target(annotation)")
public void atTarget(JoinPoint joinPoint, ClassAop annotation) {
    log.info("[@target] {}, obj = {}", joinPoint.getSignature(), annotation);
}

@Before("allMember() && @within(annotation)")
public void atWithin(JoinPoint joinPoint, ClassAop annotation) {
    log.info("[@within] {}, obj = {}", joinPoint.getSignature(), annotation);
}

@Before("allMember() && @annotation(annotation)")
public void atAnnotation(JoinPoint joinPoint, MethodAop annotation) {
    log.info("[@annotation] {}, obj = {}", joinPoint.getSignature(), annotation.value());
}
  • 지금 보면 세가지 지시자가 있다. @target, @within, @annotation.
  • @target, @within 은 타입 애노테이션에 대해 정보를 가져오는 것이다. 즉, 클래스, 인터페이스 레벨에 붙어있는 애노테이션을 가져오는 지시자이다.
  • @annotation은 메서드 레벨에 붙은 애노테이션을 가져오는 지시자이다. 그리고 난 위에서 그 애노테이션의 value()라는 속성에 "test value"를 넣었다. 이 값을 가져오고 싶을 때 저렇게 할 수 있다. 
  • 여기서, @annotation(annotation)이라고 썼으면 파라미터에서도 annotation이라는 이름으로 받아야 한다. 만약 @annotation(methodAop)로 썼으면 파라미터도 methodAop라는 이름으로 받으면 된다. 물론 @target, @within도 동일하다.

실행 결과

[@annotation] String cwchoiit.springadvanced.aop.member.MemberServiceImpl.hello(String), obj = test value
[@target] String cwchoiit.springadvanced.aop.member.MemberServiceImpl.hello(String), obj = @cwchoiit.springadvanced.aop.member.annotation.ClassAop()
[@within] String cwchoiit.springadvanced.aop.member.MemberServiceImpl.hello(String), obj = @cwchoiit.springadvanced.aop.member.annotation.ClassAop()

 

그리고 한가지 더, 원래는 @annotation 지시자를 사용할 때 패키지명부터 쭉 써줘야 한다. 아래와 같이 말이다. 근데 위에서처럼 저렇게 파라미터로 애노테이션 타입을 명시하면 이름으로 치환할 수 있다.

@annotation(com.example.aop.member.annotation.MethodAop)

 

 

 

this, target

솔직히 이게 중요한지는 모르겠다. 근데 내용이 은근히 어렵다. 그래서 굳이라는 생각이 들지만 한번 정리해보겠다.

  • this: 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
  • target: Target 객체(스프링 AOP 프록시가 가리키는 실제 대상)를 대상으로 하는 조인 포인트

설명

  • this, target은 다음과 같이 적용 타입 하나를 정확하게 지정해야 한다.
  • '*' 같은 패턴을 사용할 수 없다.
  • 부모 타입을 허용한다.
this(hello.aop.member.MemberService)
target(hello.aop.member.MemberService)

 

똑같이 생겨가지고 무슨 차이가 있을까?

스프링에서 AOP를 적용하면 실제 target 객체 대신에 프록시 객체가 스프링 빈으로 등록된다. 여기서,

  • this는 스프링 빈으로 등록되어 있는 프록시 객체를 대상으로 포인트컷을 매칭한다.
  • target은 실제 target 객체를 대상으로 포인트컷을 매칭한다.

그러니까 다음 코드 예시를 보면,

this(hello.aop.member.MemberService)
target(hello.aop.member.MemberService)

 

똑같이 MemberService를 조건으로 입력해도 this는 스프링 빈으로 등록된 프록시를, target은 스프링 빈으로 등록된 프록시가 참조하는 실제 객체를 바라본다는 뜻인데 이게 뭐 큰 의미가 있고 달라지나 싶을 수 있다. 그러나, JDK 동적 프록시와 CGLIB의 프록시 생성 방식이 다르기 때문에 차이점이 발생할 수 있다.

 

JDK 동적 프록시일 때

이 방식은 인터페이스가 필수이고 인터페이스를 구현한 프록시 객체를 생성한다. 다음이 그 그림이다.

 

 

그럼 이 방식으로 프록시를 만들 때 thistarget 지시자가 어떻게 다른지 보자.

 

MemberService 인터페이스 지정

this(hello.aop.member.MemberService)
  • proxy 객체를 보고 판단한다. this는 부모 타입을 허용한다. 프록시는 인터페이스인 MemberService를 참조하므로 AOP가 적용된다.
target(hello.aop.member.MemberService)
  • target 객체를 보고 판단한다. target은 부모 타입을 허용한다. target이 상속받는 MemberService가 있으므로 AOP가 적용된다.

MemberServiceImpl 구체 클래스 지정

this(hello.aop.member.MemberServiceImpl)
  • proxy 객체를 보고 판단한다. 프록시 객체의 부모는 MemberService 인터페이스이다. 인터페이스 위에 있는 것은 없다. MemberServiceImpl에 대한 정보를 아예 알 수 없으므로 AOP 적용 대상이 아니다.
target(hello.aop.member.MemberServiceImpl)
  • target 객체를 보고 판단한다. target은 바로 MemberServiceImpl 구체 클래스이므로 AOP 적용 대상이다.
결론은 JDK 동적 프록시는 this로 구체 클래스를 받으면 AOP 적용 대상이 아니게 된다. 반면, CGLIB는 어떨까?

 

 

CGLIB 프록시일 때

 

MemberService 인터페이스 지정

this(hello.aop.member.MemberService)
  • thisproxy 객체를 바라본다. 프록시 객체는 구체 클래스인 MemberServiceImpl을 상속받는다. 그리고 이 구체 클래스는 부모인 MemberService를 알고 있다. this는 부모 타입을 허용하므로 AOP 적용 대상이다.
target(hello.aop.member.MemberService)
  • target은 실제 target 객체를 바라본다. target 객체인 MemberServiceImpl의 부모인 MemberService가 있다. target은 부모 타입을 허용하므로 AOP 적용 대상이다.

MemberServiceImpl 구체 클래스 지정

this(hello.aop.member.MemberServiceImpl)
  • thisproxy 객체를 바라본다. 프록시 객체는 구체 클래스인 MemberServiceImpl을 상속받는다. this는 부모 타입을 허용하므로 AOP 적용 대상이다.
target(hello.aop.member.MemberServiceImpl)
  • target은 실제 target 객체를 바라본다. target 객체가 MemberServiceImpl이므로 AOP 적용 대상이다.
결론은 CGLIB 프록시는 모든 경우에 AOP 적용 대상이 된다. 그리고 스프링은 기본으로 CGLIB로 프록시를 만들어낸다. 

 

ThisTargetTest

package cwchoiit.springadvanced.aop.pointcut;

import cwchoiit.springadvanced.aop.member.MemberService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

@Slf4j
@Import(ThisTargetTest.ThisTargetAspect.class)
@SpringBootTest
public class ThisTargetTest {

    @Autowired
    MemberService memberService;

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

    @Aspect
    static class ThisTargetAspect {
        @Around("this(cwchoiit.springadvanced.aop.member.MemberService)")
        public Object doThisInterface(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[this-interface] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        @Around("target(cwchoiit.springadvanced.aop.member.MemberService)")
        public Object doTargetInterface(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[target-interface] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        @Around("this(cwchoiit.springadvanced.aop.member.MemberServiceImpl)")
        public Object doThis(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[this-concrete] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        @Around("target(cwchoiit.springadvanced.aop.member.MemberServiceImpl)")
        public Object doTarget(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[target-concrete] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}

 

  • 에스팩트에 4개의 어드바이저가 있다. 위 설명 대로 this에 인터페이스, 구체 클래스를 target에 인터페이스, 구체 클래스를 적용했을 때 AOP가 적용되는지에 대한 내용이다. 스프링은 기본으로 CGLIB 프록시로 프록시를 만들어내는데 그 설정 값은 다음과 같다.
spring:
  aop:
    proxy-target-class: true # true = CGLIB 를 기본으로 / false = JDK 동적 프록시를 기본으로

 

이 상태로 success() 테스트를 실행하면 모든 어드바이저가 적용된다.

 

이제 JDK 동적 프록시를 스프링 기본 프록시로 설정해보자. 

spring:
  aop:
    proxy-target-class: false

this-impl 로그가 찍히지 않았음을 확인할 수 있다.

 

스프링 AOP - 실전 예제

지금까지 학습한 내용을 활용해서 유용한 스프링 AOP를 만들어보자. 

  • @Trace → 애노테이션으로 로그 출력하기
  • @Retry → 애노테이션으로 예외 발생 시 재시도 하기

먼저 AOP를 적용할 예제를 만들자.

ExamRepository

package cwchoiit.springadvanced.aop.example;

import org.springframework.stereotype.Repository;

@Repository
public class ExamRepository {

    private static int seq = 0;

    public String save(String itemId) {
        seq++;
        if (seq % 5 == 0) {
            throw new IllegalStateException("예외 발생");
        }
        return "ok";
    }
}
  • 5번에 1번 정도 실패하는 저장소이다. 이렇게 간헐적으로 실패할 경우, 재시도하는 AOP가 있으면 편리할 것 같다.

ExamService

package cwchoiit.springadvanced.aop.example;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class ExamService {

    private final ExamRepository examRepository;

    public void request(String itemId) {
        examRepository.save(itemId);
    }
}
  • 단순히 ExamRepository 위임 서비스라고 생각하면 된다.

 

로그 출력 AOP

먼저 로그 출력용 AOP를 만들어보자. @Trace가 메서드에 붙어 있으면 호출 정보가 출력되는 편리한 기능이다.

@Trace

package cwchoiit.springadvanced.aop.example.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

 

TraceAspect

package cwchoiit.springadvanced.aop.example.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Slf4j
@Aspect
public class TraceAspect {

    @Before("@annotation(cwchoiit.springadvanced.aop.example.annotation.Trace)")
    public void beforeTrace(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        log.info("[trace] before {} args = {}", joinPoint.getSignature(), args);
    }
}
  • @annotation(cwchoiit.springadvanced.aop.example.annotation.Trace) 포인트컷을 사용해서 @Trace가 붙은 메서드에 어드바이스를 적용한다.

ExamService - @Trace 추가

package cwchoiit.springadvanced.aop.example;

import cwchoiit.springadvanced.aop.example.annotation.Trace;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class ExamService {

    private final ExamRepository examRepository;

    @Trace
    public void request(String itemId) {
        examRepository.save(itemId);
    }
}

 

ExamRepository - @Trace 추가

package cwchoiit.springadvanced.aop.example;

import cwchoiit.springadvanced.aop.example.annotation.Trace;
import org.springframework.stereotype.Repository;

@Repository
public class ExamRepository {

    private static int seq = 0;

    @Trace
    public String save(String itemId) {
        seq++;
        if (seq % 5 == 0) {
            throw new IllegalStateException("예외 발생");
        }
        return "ok";
    }
}

 

ExamServiceTest

package cwchoiit.springadvanced.aop.example;

import cwchoiit.springadvanced.aop.example.aop.TraceAspect;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

@Slf4j
@Import(TraceAspect.class)
@SpringBootTest
class ExamServiceTest {

    @Autowired
    ExamService examService;

    @Test
    void test() {
        for (int i = 0; i < 5; i++) {
            log.info("client request i = {}", i);
            examService.request("data" + i);
        }
    }
}
  • @Aspect를 스프링 빈으로 등록해야 하니까, @Import를 사용했다. 간단한 테스트니까.

실행 결과

client request i = 0
[trace] before void cwchoiit.springadvanced.aop.example.ExamService.request(String) args = [data0]
[trace] before String cwchoiit.springadvanced.aop.example.ExamRepository.save(String) args = [data0]
client request i = 1
[trace] before void cwchoiit.springadvanced.aop.example.ExamService.request(String) args = [data1]
[trace] before String cwchoiit.springadvanced.aop.example.ExamRepository.save(String) args = [data1]
client request i = 2
[trace] before void cwchoiit.springadvanced.aop.example.ExamService.request(String) args = [data2]
[trace] before String cwchoiit.springadvanced.aop.example.ExamRepository.save(String) args = [data2]
client request i = 3
[trace] before void cwchoiit.springadvanced.aop.example.ExamService.request(String) args = [data3]
[trace] before String cwchoiit.springadvanced.aop.example.ExamRepository.save(String) args = [data3]
client request i = 4
[trace] before void cwchoiit.springadvanced.aop.example.ExamService.request(String) args = [data4]
[trace] before String cwchoiit.springadvanced.aop.example.ExamRepository.save(String) args = [data4]
  • 실행 결과를 보면, @Trace가 붙은 request(), save() 호출 시 로그가 잘 남는 것을 확인할 수 있다.

 

재시도 AOP

이번에는 좀 더 의미있는 재시도 AOP를 만들어보자. @Retry 애노테이션이 있으면 예외가 발생했을 때, 다시 시도해서 문제를 복구한다.

@Retry

package cwchoiit.springadvanced.aop.example.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

    int value() default 3;
}
  • 이 애노테이션에는 재시도 횟수로 사용할 값이 있다. 그리고 이런 재시도와 같은 작업은 정말 정말 최대 재시도 수 같은 게 중요하다. 왜냐하면 그게 없으면 해결이 안되는 것을 무한 반복해서 셀프 디도스를 일으킬 수도 있으니 말이다.

RetryAspect

package cwchoiit.springadvanced.aop.example.aop;

import cwchoiit.springadvanced.aop.example.annotation.Retry;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

import java.util.Objects;

@Slf4j
@Aspect
public class RetryAspect {

    @Around("@annotation(retry)")
    public Object retry(ProceedingJoinPoint joinPoint, Retry retry) throws Throwable {
        int maxRetry = retry.value();

        log.info("[retry] {}, Max retry = {}", joinPoint.getSignature(), maxRetry);

        Exception exceptionHolder = null;
        for (int i = 1; i < maxRetry; i++) {
            try {
                log.info("[retry] try count = {}/{}", i, maxRetry);
                return joinPoint.proceed();
            } catch (Exception e) {
                exceptionHolder = e;
            }
        }
        throw Objects.requireNonNull(exceptionHolder);
    }
}
  • 재시도하는 @Aspect이다.
  • @annotation(retry), Retry retry를 사용해서 어드바이스에 애노테이션을 파라미터로 전달한다.
  • retry.value()를 통해서 애노테이션에 지정한 값을 가져올 수 있다.
  • 예외가 발생해서 결과가 정상 반환되지 않으면 retry.value()만큼 재시도한다.

ExamRepository - @Retry 추가

package cwchoiit.springadvanced.aop.example;

import cwchoiit.springadvanced.aop.example.annotation.Retry;
import cwchoiit.springadvanced.aop.example.annotation.Trace;
import org.springframework.stereotype.Repository;

@Repository
public class ExamRepository {

    private static int seq = 0;

    @Trace
    @Retry(4)
    public String save(String itemId) {
        seq++;
        if (seq % 5 == 0) {
            throw new IllegalStateException("예외 발생");
        }
        return "ok";
    }
}
  • @Retry(4)를 적용했다. 이 메서드에서 문제가 발생하면 최대 4번까지 재시도한다.

ExamServiceTest

package cwchoiit.springadvanced.aop.example;

import cwchoiit.springadvanced.aop.example.aop.RetryAspect;
import cwchoiit.springadvanced.aop.example.aop.TraceAspect;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

@Slf4j
@Import({TraceAspect.class, RetryAspect.class})
@SpringBootTest
class ExamServiceTest {

    @Autowired
    ExamService examService;

    @Test
    void test() {
        for (int i = 0; i < 5; i++) {
            log.info("client request i = {}", i);
            examService.request("data" + i);
        }
    }
}
  • @Aspect를 스프링 빈으로 등록하고 테스트해보자.

실행 결과

client request i = 0
[trace] before void cwchoiit.springadvanced.aop.example.ExamService.request(String) args = [data0]
[trace] before String cwchoiit.springadvanced.aop.example.ExamRepository.save(String) args = [data0]
[retry] String cwchoiit.springadvanced.aop.example.ExamRepository.save(String), Max retry = 4
[retry] try count = 1/4
client request i = 1
[trace] before void cwchoiit.springadvanced.aop.example.ExamService.request(String) args = [data1]
[trace] before String cwchoiit.springadvanced.aop.example.ExamRepository.save(String) args = [data1]
[retry] String cwchoiit.springadvanced.aop.example.ExamRepository.save(String), Max retry = 4
[retry] try count = 1/4
client request i = 2
[trace] before void cwchoiit.springadvanced.aop.example.ExamService.request(String) args = [data2]
[trace] before String cwchoiit.springadvanced.aop.example.ExamRepository.save(String) args = [data2]
[retry] String cwchoiit.springadvanced.aop.example.ExamRepository.save(String), Max retry = 4
[retry] try count = 1/4
client request i = 3
[trace] before void cwchoiit.springadvanced.aop.example.ExamService.request(String) args = [data3]
[trace] before String cwchoiit.springadvanced.aop.example.ExamRepository.save(String) args = [data3]
[retry] String cwchoiit.springadvanced.aop.example.ExamRepository.save(String), Max retry = 4
[retry] try count = 1/4
client request i = 4
[trace] before void cwchoiit.springadvanced.aop.example.ExamService.request(String) args = [data4]
[trace] before String cwchoiit.springadvanced.aop.example.ExamRepository.save(String) args = [data4]
[retry] String cwchoiit.springadvanced.aop.example.ExamRepository.save(String), Max retry = 4
[retry] try count = 1/4
[retry] try count = 2/4
  • 가장 마지막만 보면, ExamRepository.save()를 5번째 실행할때 예외가 발생하는데 그때 재시도하는 모습을 볼 수 있다. 재시도한 후 성공적으로 문제가 복구되고 정상 응답된 모습이다.

 

 

728x90
반응형
LIST

'Spring Advanced' 카테고리의 다른 글

Redis를 사용해서 캐싱하기  (0) 2024.10.02
Mockito를 사용한 스프링 프로젝트 단위 테스트  (4) 2024.09.29
스프링 AOP Part.2  (0) 2024.01.02
스프링 AOP Part. 1  (0) 2023.12.29
AOP와 @Aspect  (0) 2023.12.29
728x90
반응형
SMALL

참고자료

 

스프링 핵심 원리 - 고급편 강의 | 김영한 - 인프런

김영한 | 스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., 핵심 디자인 패턴, 쓰레드 로컬, 스프링 AOP스프링의 3가지 핵심 고급 개념 이해하기

www.inflearn.com

 

 

SMALL

Advisor, Advice, Pointcut - 소개

  • Advice: 프록시가 제공하는 추가 기능에 대한 로직을 가지고 있는 곳을 말한다. (조언)
  • Pointcut: 프록시가 제공하는 추가 기능을 어디에 적용할것인가?을 가지고 있는 곳을 말한다. (어디에?)
  • Advisor: AdvicePointcut을 한 개씩 가지고 있는 곳을 말한다. (조언자)

그리고 ProxyFactoryAdvisor가 필수이다. 근데 저번 포스팅에서는 Advisor를 안 사용했고 addAdvice()만 호출해서 Advice만 넘겼는데 이렇게 하면 기본 Advisor에 모든 곳에 적용하는 Pointcut으로 할당된다. 단순 편의 메서드인 것 뿐이다.

 

역할과 책임

이렇게 구분한 것은 역할과 책임을 명확하게 분리한 것이다.

  • 포인트컷은 대상 여부를 확인하는 필터 역할만 담당한다.
  • 어드바이스는 깔끔하게 부가 기능 로직만 담당한다.
  • 둘을 합치면 어드바이저가 된다. 스프링의 어드바이저는 하나의 포인트컷 + 하나의 어드바이스로 구성된다.

  • 위 그림은 이해를 돕기 위해 만들어진 그림이다. 실제 구현은 약간 다를 수 있지만, 흐름은 동일하다.
  • 클라이언트가 프록시를 호출하면, 이 프록시는 먼저 Advice 적용 여부를 확인한다. 만약 적용되지 않았다면 부가 기능을 적용하지 않은 채로 실제 객체만 호출하고, 적용 대상이라면 부가 기능을 적용한다. 

한번 Advisor, Advice, Pointcut을 적용해 보는 코드를 작성해보자.

예제 코드1 - 어드바이저

AdvisorTest

package cwchoiit.springadvanced.proxy.advisor;

import cwchoiit.springadvanced.proxy.common.advice.TimeAdvice;
import cwchoiit.springadvanced.proxy.common.service.ServiceImpl;
import cwchoiit.springadvanced.proxy.common.service.ServiceInterface;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;

public class AdvisorTest {

    @Test
    void advisorTest1() {
        ServiceInterface target = new ServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);

        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice());
        proxyFactory.addAdvisor(advisor);
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

        proxy.save();
        proxy.find();
    }
}
  • 이전 포스팅에서 사용했던 ServiceInterfaceTimeAdvice를 그대로 사용해보자. 그리고 이제 어드바이저를 만들어보자. 그 코드는 다음과 같다.
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice());
  • Advisor 인터페이스의 가장 일반적인 구현체이다. 생성자를 통해 하나의 포인트컷과 하나의 어드바이스를 넣어주면 된다. 어드바이저는 하나의 포인트컷과 하나의 어드바이스로 구성된다. 
  • Pointcut.TRUE는 항상 true를 반환하는 포인트컷이다. 즉, 모든곳에 프록시의 부가 기능이 적용이 된다는 의미이다.
proxyFactory.addAdvisor(advisor);
  • 프록시팩토리에 적용할 어드바이저를 지정한다. 어드바이저는 내부에 포인트컷과 어드바이스를 모두 가지고 있다. 따라서 어디에 부가 기능을 적용해야 할지 어드바이저 하나로 알 수 있다. 프록시 팩토리를 사용할 때 어드바이저는 필수이다. 

실행 결과

  • 실행 결과를 보면, save(), find() 각각 모두 어드바이스가 적용된 것을 확인할 수 있다.

 

예제 코드2 - 직접 만든 포인트컷

이번에는, save() 메서드에는 어드바이스 로직을 적용하지만, find() 메서드에는 어드바이스 로직을 적용하지 않도록 해보자. 물론 과거에 했던 코드와 유사하게 어드바이스에 로직을 추가해서 메서드 이름을 보고 코드를 실행할지 말지 분기를 타도 된다. 하지만 이런 기능에 특화되어서 제공되는 것이 바로 포인트컷이다. 그리고 그렇게하면 어드바이스의 역할과 책임이 너무 많아진다(=유지보수가 안 좋아진다)

 

Pointcut 관련 인터페이스 - 스프링 제공

public interface Pointcut {
     ClassFilter getClassFilter();
     MethodMatcher getMethodMatcher();
}

public interface ClassFilter {
     boolean matches(Class<?> clazz);
}

public interface MethodMatcher {
     boolean matches(Method method, Class<?> targetClass);
     //..
}
  • 포인트컷은 크게 ClassFilter, MethodMatcher 둘로 이루어진다. 이름 그대로 하나는 클래스가 맞는지, 하나는 메서드가 맞는지 확인할 때 사용한다. 둘 다 true로 반환해야 어드바이스를 적용할 수 있다.
  • 일반적으로 스프링이 이미 만들어둔 구현체를 사용하지만, 학습 차원에서 한번 간단히 직접 구현해보자.
@Test
@DisplayName("직접 만든 포인트컷")
void advisorTest2() {
    ServiceInterface target = new ServiceImpl();
    ProxyFactory proxyFactory = new ProxyFactory(target);

    DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(new MyPointcut(), new TimeAdvice());
    proxyFactory.addAdvisor(advisor);
    ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

    proxy.save();
    proxy.find();
}

/**
 * 직접 만들일은 없음, 스프링이 만들어주는 Pointcut을 사용하면 되지만 한번 만들어보자.
 * 클래스와 메서드 둘 다 'true' 를 리턴해야만 Pointcut에 적합한 요청이라고 판단하여 Advice를 적용한다.
 * */
static class MyPointcut implements Pointcut {
    /**
     * 클래스를 기준으로 필터링
     * ClassFilter.TRUE 를 반환하면 모든 클래스에 대해 Advice 적용을 허용
     * */
    @Override
    public ClassFilter getClassFilter() {
        return ClassFilter.TRUE;
    }

    /**
     * 메서드를 기준으로 필터링
     * MethodMatcher를 구현해야 한다.
     * */
    @Override
    public MethodMatcher getMethodMatcher() {
        return new MyMethodMatcher();
    }
}

static class MyMethodMatcher implements MethodMatcher {

    private String matchName = "save";

    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        boolean result = method.getName().equals(matchName);

        log.info("포인트컷 호출 method = {} targetClass= {}", method.getName(), targetClass);
        log.info("포인트컷 결과 result = {}", result);

        return result;
    }

    @Override
    public boolean isRuntime() {
        return false;
    }

    @Override
    public boolean matches(Method method, Class<?> targetClass, Object... args) {
        return false;
    }
}

 

MyPointcut

  • 직접 구현한 포인트컷이다. Pointcut 인터페이스를 구현한다.
  • 현재 메서드 기준으로 로직을 적용하면 된다. 클래스 필터는 항상 true를 반환하도록 했고, 메서드 비교 기능은 MyMethodMatcher를 사용한다.

MyMethodMatcher

  • 직접 구현한 MethodMatcher이다. MethodMatcher 인터페이스를 구현한다.
  • matches() → 이 메서드에 method, targetClass 정보가 넘어온다. 이 정보로 어드바이스를 적용할지 적용하지 않을지 판단할 수 있다. 여기서는 메서드 이름이 'save'인 경우에 true를 반환하도록 판단 로직을 적용한다.
  • isRuntime(), matches(..., args) → isRuntime() 이 값이 true이면 matches(..., args) 메서드가 matches() 대신 호출된다. 동적으로 넘어오는 매개변수를 판단 로직으로 사용할 수 있다. isRuntime()false인 경우, 클래스의 정적 정보만 사용하기 때문에 스프링이 내부에서 캐싱을 통해 성능 향상이 가능하지만, isRuntime()true인 경우 매개변수가 동적으로 변경된다고 가정하기 때문에 캐싱을 하지 않는다. 크게 중요한 부분은 아니니 참고만 하자. 어차피 포인트컷을 직접 만들일은 없다.

 

위에서 모든 대상에 대해 Advice를 적용했던 예시 코드에 비교해서 바뀌는 부분은 딱 Pointcut이 달라지는것 말고 없다. 우리가 만든 MyPointcut을 전달한다. 대신 실행 결과가 달라질 것이다. 'save()'가 아닌 'find()'에는 Advice는 적용되지 않는다.

 

실행 결과

 

위 실행 결과의 흐름을 그림으로 비교해보자.

  • 클라이언트가 프록시의 save()를 호출한다.
  • 포인트컷에게 Service 클래스의 save() 메서드에 어드바이스를 적용해도 될지 물어본다.
  • 포인트컷이 true를 반환한다. 따라서 어드바이스를 호출해서 부가 기능을 적용한다.
  • 이후 실제 인스턴스의 save()를 호출한다.

  • 클라이언트가 프록시의 find()를 호출한다.
  • 포인트컷에게 Service 클래스의 find() 메서드에 어드바이스를 적용해도 될지 물어본다.
  • 포인트컷이 false를 반환한다. 따라서 어드바이스를 호출하지 않고, 부가 기능도 적용되지 않는다.
  • 실제 인스턴스를 호출한다.

 

예제 코드3 - 스프링이 제공하는 포인트컷

사실, 포인트컷을 저렇게 직접 구현해서 사용할 일은 앞으로 없다. 이미 스프링은 여러 포인트컷을 제공하고 있고 그 중 하나를 골라 사용하거나 결국엔 끝판왕인 AspectJExpressionPointcut을 사용하게 될 것이다. 한번 스프링이 제공하는 여러 포인트컷 중 하나를 골라서 사용해보자.

@Test
@DisplayName("스프링이 제공하는 포인트컷")
void advisorTest3() {
    // 실제 객체 서비스
    ServiceInterface target = new ServiceImpl();

    // ProxyFactory 객체 생성 후 실제 객체를 전달
    ProxyFactory proxyFactory = new ProxyFactory(target);

    // 스프링이 제공하는 포인트 컷 중 하나인 NameMatchMethodPointcut
    NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
    // Method 명이 save인 애들에게 Advice를 적용해주는 Pointcut을 만든다.
    pointcut.setMappedName("save");

    DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice());

    // ProxyFactory에 advisor 추가
    proxyFactory.addAdvisor(advisor);

    // ProxyFactory 로부터 proxy 를 꺼내온다.
    ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

    // Pointcut에 의하여 Advice 적용된다.
    proxy.save();
    // Pointcut에 의하여 Advice 적용되지 않는다.
    proxy.find();
}
  • 스프링이 제공하는 NameMatchMethodPointcut을 사용해보자. 
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedName("save");

 

  • 말 그대로 메서드의 이름으로 Pointcut을 설정하는 Pointcut이다. 위 코드처럼 'save'라는 값을 setMappedName()에 넘겨주면 이 Pointcut'save'라는 메서드명을 가진 요청에 한하여 Advice를 적용한다. 나머지는 동일하다.

실행 결과

  • save()가 호출됐을 땐 TimeProxy가 동작하고 그렇지 않은 find()가 호출됐을 땐 Advice가 적용되지 않았다. 이렇게 스프링이 제공해주는 Pointcut으로 편리하게 Pointcut을 만들 수 있다.

 

예제 코드4 - 여러 어드바이저 함께 적용

어드바이저는 하나의 포인트컷과 하나의 어드바이스를 가지고 있다. 만약, 여러 어드바이저를 하나의 target에 적용하려면 어떻게 해야할까? 쉽게 이야기해서 하나의 target에 여러 어드바이스를 적용하려면 어떻게 해야할까? 지금 떠오르는 방법은 프록시를 여러개 만들면 될 것 같다.

 

프록시 여러개로 구현

package cwchoiit.springadvanced.proxy.advisor;

import cwchoiit.springadvanced.proxy.common.advice.TimeAdvice;
import cwchoiit.springadvanced.proxy.common.service.ServiceImpl;
import cwchoiit.springadvanced.proxy.common.service.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;

@Slf4j
public class MultiAdvisorTest {

    @Test
    @DisplayName("여러 프록시")
    void multiAdvisorTest1() {
        ServiceInterface target = new ServiceImpl();
        ProxyFactory proxyFactory1 = new ProxyFactory(target);
        DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
        proxyFactory1.addAdvisor(advisor1);
        ServiceInterface proxy1 = (ServiceInterface) proxyFactory1.getProxy();

        ProxyFactory proxyFactory2 = new ProxyFactory(proxy1);
        DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
        proxyFactory2.addAdvisor(advisor2);
        ServiceInterface proxy2 = (ServiceInterface) proxyFactory2.getProxy();

        proxy2.find();
    }

    static class Advice1 implements MethodInterceptor {

        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            log.info("Advice1 invoked");
            return invocation.proceed();
        }
    }

    static class Advice2 implements MethodInterceptor {

        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            log.info("Advice2 invoked");
            return invocation.proceed();
        }
    }
}
  • 코드상에 새로운 개념은 없다. 프록시를 여러개 입혔다.

실행 결과

11:35:11.287 [Test worker] INFO cwchoiit.springadvanced.proxy.advisor.MultiAdvisorTest -- Advice2 invoked
11:35:11.289 [Test worker] INFO cwchoiit.springadvanced.proxy.advisor.MultiAdvisorTest -- Advice1 invoked
11:35:11.289 [Test worker] INFO cwchoiit.springadvanced.proxy.common.service.ServiceImpl -- find 호출
  • 차례대로 Advice2, Advice1이 실행된다. 그리고 실제 객체인 target의 로직까지 실행됐다. 
  • 그러나, 이 방법이 잘못된 것은 아니지만 프록시를 2번 생성해야 한다는 문제가 있다. 만약 적용해야 하는 어드바이저가 10개라면 10개의 프록시를 생성해야 한다. 

 

하나의 프록시, 여러 어드바이저로 구현

스프링은 이 문제를 해결하기 위해 하나의 프록시에 여러 어드바이저를 적용할 수 있게 만들어 두었다.

@Test
@DisplayName("하나의 프록시, 여러 어드바이저")
void multiAdvisorTest2() {
    DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
    DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());

    ServiceInterface target = new ServiceImpl();
    ProxyFactory proxyFactory = new ProxyFactory(target);
    proxyFactory.addAdvisors(advisor2, advisor1);
    ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

    proxy.find();
}
  • 프록시 팩토리에 원하는 어드바이저를 addAdvisors(...)로 등록하면 끝이다. 먼저 넣은 어드바이저가 먼저 실행된다.

 

 

정리

결과적으로, 여러 프록시를 사용할 때와 비교해서 결과는 같으나 성능은 더 좋다. 스프링 AOP도 이와 같다. AOP 적용 수만큼 프록시가 생성되는 것이 아니다! 스프링은 AOP를 적용할 때, 최적화를 진행해서 지금처럼 프록시는 하나만 만들고, 하나의 프록시에 여러 어드바이저를 적용한다. 정리하면 하나의 target에 여러 AOP가 적용되어도, 스프링의 AOP는 target마다 하나의 프록시만 생성한다. 꼭 기억하자!

 

프록시팩토리 - 적용1

지금까지 학습한 프록시 팩토리를 사용해서 애플리케이션에 프록시를 만들어보자. 먼저 인터페이스가 있는 V1 애플리케이션에 LogTrace 기능을 프록시 팩토리를 통해서 프록시를 만들어 적용해보자.

 

LogTraceAdvice

package cwchoiit.springadvanced.proxy.config.v3_proxyfactory.advice;

import cwchoiit.springadvanced.trace.TraceStatus;
import cwchoiit.springadvanced.trace.logtrace.LogTrace;
import lombok.RequiredArgsConstructor;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

import java.lang.reflect.Method;

@RequiredArgsConstructor
public class LogTraceAdvice implements MethodInterceptor {

    private final LogTrace trace;

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        TraceStatus status = null;
        Method method = invocation.getMethod();
        try {
            String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()";
            status = trace.begin(message);
            Object result = invocation.proceed();
            trace.end(status);
            return result;
        } catch (Exception e) {
            trace.exception(status, e);
            throw e;
        }
    }
}

 

  • Advice를 만들기 위해 MethodInterceptor를 구현하는 LogTraceAdvice 클래스를 만들었다. MethodInterceptor가 구현해야 하는 invoke()를 기존에 계속 사용했던 LogTrace 기능으로 채워넣었다. Advice는 실제 객체를 주입받지 않아도 되기 때문에 편리함을 준다. 
  • 이제 Advice를 만들었으니까 ProxyFactory를 통해서 프록시를 만들고 스프링 빈으로 등록해보자.

ProxyFactoryConfigV1

package cwchoiit.springadvanced.proxy.config.v3_proxyfactory;

import cwchoiit.springadvanced.proxy.app.v1.*;
import cwchoiit.springadvanced.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;
import cwchoiit.springadvanced.trace.logtrace.LogTrace;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class ProxyFactoryConfigV1 {

    @Bean
    public OrderRepositoryV1 orderRepositoryV1(LogTrace trace) {
        OrderRepositoryV1 target = new OrderRepositoryV1Impl();
        ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.addAdvisor(getAdvisor(trace));
        OrderRepositoryV1 proxy = (OrderRepositoryV1) proxyFactory.getProxy();
        log.info("OrderRepositoryV1 proxy: {}, targetClass: {}", proxy.getClass(), target.getClass());
        return proxy;
    }

    @Bean
    public OrderServiceV1 orderServiceV1(LogTrace trace) {
        OrderServiceV1 target = new OrderServiceV1Impl(orderRepositoryV1(trace));
        ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.addAdvisor(getAdvisor(trace));
        OrderServiceV1 proxy = (OrderServiceV1) proxyFactory.getProxy();
        log.info("OrderServiceV1 proxy: {}, targetClass: {}", proxy.getClass(), target.getClass());
        return proxy;
    }

    @Bean
    public OrderControllerV1 orderControllerV1(LogTrace trace) {
        OrderControllerV1 target = new OrderControllerV1Impl(orderServiceV1(trace));
        ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.addAdvisor(getAdvisor(trace));
        OrderControllerV1 proxy = (OrderControllerV1) proxyFactory.getProxy();
        log.info("OrderControllerV1 proxy: {}, targetClass: {}", proxy.getClass(), target.getClass());
        return proxy;
    }

    private Advisor getAdvisor(LogTrace trace) {
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");
        LogTraceAdvice logTraceAdvice = new LogTraceAdvice(trace);

        return new DefaultPointcutAdvisor(pointcut, logTraceAdvice);
    }
}
  • 포인트컷은 NameMatchMethodPointcut을 사용한다. 여기에는 심플 매칭 기능이 있어서 *을 매칭할 수 있다. 
    • request*, order*, save* → request, order, save로 시작하는 메서드에 포인트컷은 true를 반환한다. 
    • 이렇게 설정한 이유는 noLog() 메서드에는 어드바이스를 적용하지 않기 위해서다.
  • 어드바이저는 포인트컷(NameMatchMethodPointcut), 어드바이스(LogTraceAdvice)를 가지고 있다.
  • 프록시 팩토리에 각각의 targetadvisor를 등록해서 프록시를 생성한다. 그리고 생성된 프록시를 스프링 빈으로 등록한다.

 

테스트 해보자. '/v1/request'로 요청하면 LogTrace 정보가 남아야한다. '/v1/no-log'로 요청하면 LogTrace 정보가 남지 않아야 한다.

 

 

프록시팩토리 - 적용2

구체 클래스로 프록시를 만든다고 달라지는 건 없다. 반환 타입만 달라질 뿐이다. 왜냐하면 ProxyFactory를 사용하기 때문이다. ProxyFactory는 알아서 구체 클래스면 CGLIB로 프록시를, 인터페이스면 JDK 동적 프록시를 사용해서 프록시를 만들어 준다.

 

ProxyFactoryConfigV2

package cwchoiit.springadvanced.proxy.config.v3_proxyfactory;

import cwchoiit.springadvanced.proxy.app.v1.*;
import cwchoiit.springadvanced.proxy.app.v2.OrderControllerV2;
import cwchoiit.springadvanced.proxy.app.v2.OrderRepositoryV2;
import cwchoiit.springadvanced.proxy.app.v2.OrderServiceV2;
import cwchoiit.springadvanced.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;
import cwchoiit.springadvanced.trace.logtrace.LogTrace;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class ProxyFactoryConfigV2 {

    @Bean
    public OrderRepositoryV2 orderRepositoryV2(LogTrace trace) {
        OrderRepositoryV2 target = new OrderRepositoryV2();
        ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.addAdvisor(getAdvisor(trace));
        OrderRepositoryV2 proxy = (OrderRepositoryV2) proxyFactory.getProxy();
        log.info("OrderRepositoryV2 proxy: {}, targetClass: {}", proxy.getClass(), target.getClass());
        return proxy;
    }

    @Bean
    public OrderServiceV2 orderServiceV2(LogTrace trace) {
        OrderServiceV2 target = new OrderServiceV2(orderRepositoryV2(trace));
        ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.addAdvisor(getAdvisor(trace));
        OrderServiceV2 proxy = (OrderServiceV2) proxyFactory.getProxy();
        log.info("OrderServiceV2 proxy: {}, targetClass: {}", proxy.getClass(), target.getClass());
        return proxy;
    }

    @Bean
    public OrderControllerV2 orderControllerV2(LogTrace trace) {
        OrderControllerV2 target = new OrderControllerV2(orderServiceV2(trace));
        ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.addAdvisor(getAdvisor(trace));
        OrderControllerV2 proxy = (OrderControllerV2) proxyFactory.getProxy();
        log.info("OrderControllerV2 proxy: {}, targetClass: {}", proxy.getClass(), target.getClass());
        return proxy;
    }

    private Advisor getAdvisor(LogTrace trace) {
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");
        LogTraceAdvice logTraceAdvice = new LogTraceAdvice(trace);

        return new DefaultPointcutAdvisor(pointcut, logTraceAdvice);
    }
}

 

  • 끝이다. AdvicePointcut도 이미 만들어 놓은거니까 가져다가 사용만 하면 된다.
  • '/v2/request' 로 요청하면 마찬가지로 LogTrace의 정보를 출력한다.

 

 

정리

확실히 프록시로 사용될 코드도 Advice 하나만 만들면 되고, 동적 프록시를 만들기 때문에 프록시를 일일이 만들어 줄 필요도 없으며 구체클래스냐 인터페이스냐에 따라 나뉘어지는 동적 프록시 생성 방법을 스프링의 도움을 받아 프록시 팩토리를 사용하므로써 고민하지 않게됐다.

 

훨씬 개선되었지만 여전히 불편함은 남아있다

  • 너무 많은 설정 - ProxyFactoryConfigV1, ProxyFactoryConfigV2와 같이 설정 파일이 지나치게 많다. 예를 들어, 애플리케이션에 스프링 빈이 100개가 있다면 여기에 프록시를 통해 부가 기능을 적용하려면 100개의 동적 프록시 생성 코드를 만들어야 한다. 무수히 많은 설정 파일 때문에 설정 지옥을 경험하게 될 것이다. 최근에는 스프링 빈을 수동으로 등록하는 케이스보다 컴포넌트 스캔을 사용하는게 일반적인데 이렇게 직접 등록하는 것도 모자라서 프록시를 적용하는 코드까지 빈 생성 코드에 넣어야 한다.
  • 컴포넌트 스캔 - 애플리케이션 V3처럼 컴포넌트 스캔을 사용하는 경우, 지금까지 학습한 방법으로는 프록시 적용이 불가능하다. 왜냐하면 실제 객체를 컴포넌트 스캔으로 스프링 컨테이너에 스프링 빈으로 등록을 다 해버린 상태이기 때문이다. 지금까지 학습한 방법으로 프록시를 적용하려면, 실제 객체를 스프링 컨테이너에 빈으로 등록하는 것이 아니라, ProxyFactoryConfigV2에서 한 것처럼, 부가 기능이 있는 프록시를 실제 객체 대신 스프링 컨테이너에 빈으로 등록해야 한다.

 

이 두 가지 문제를 모두 해결하는 방법이 빈 후처리기이다.

728x90
반응형
LIST

'Spring Advanced' 카테고리의 다른 글

AOP와 @Aspect  (0) 2023.12.29
빈 후처리기(BeanPostProcessor)  (2) 2023.12.27
CGLIB,스프링이 지원하는 ProxyFactory  (0) 2023.12.14
Proxy/Decorator Pattern 2 (JDK 동적 프록시)  (0) 2023.12.14
Proxy/Decorator Pattern  (0) 2023.12.13

+ Recent posts