728x90
반응형
SMALL
SMALL

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

@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.java

package com.example.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) // Class, Interface 에다가 붙이는 애노테이션인 경우 ElementType.TYPE 을 사용한다.
// RetentionPolicy.RUNTIME 은 실제 RUNTIME 일 때에도 이 애노테이션이 살아있는 경우를 말한다.
// RUNTIME 말고 SOURCE 란 것도 있는데 이건 컴파일하면 컴파일된 파일은 이 애노테이션이 사라져버린다. 그래서 동적으로 이 애노테이션을 읽을 수 없다. 우리가 원하는게 아니다.
@Retention(RetentionPolicy.RUNTIME)
public @interface ClassAop {

}

 

MethodAop.java

package com.example.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();
}

 

우선, 두 개의 애노테이션을 만들었다. 하나는 클래스 레벨에 달 애노테이션이고 하나는 메서드 레벨에 달 애노테이션이다.

애노테이션을 만드려면 기본적으로 두 개의 애노테이션이 필요하다. @Target, @Retention.

 

@Target은 이 애노테이션이 어디에 달릴지를 설명하는 애노테이션이다. ElementType.TYPE으로 설정하면 클래스 또는 인터페이스에 레벨에 적용할 애노테이션이고 ElementType.METHOD는 메서드 레벨에 적용할 애노테이션이다. 

 

@Retention은 이 애노테이션이 살아있는 레벨을 말한다고 보면 된다. RetentionPolicy.RUNTIME으로 설정하면 런타임에도 해당 애노테이션은 살아 있는 상태로 남아있다. 그래서, 동적으로 애노테이션을 읽을 수 있다. RUNTIME말고 SOURCE도 있는데 이는 컴파일하면 컴파일된 파일에서 애노테이션이 보이지 않고 사라진다. 그래서 동적으로 이 애노테이션을 읽을 수 없다. 

 

그리고 MethodAop 애노테이션은 value() 라는 값을 가질 수 있다. 값의 형태는 문자열이다.

 

MemberService.java

package com.example.aop.member;

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

 

MemberServiceImpl.java

package com.example.aop.member;

import com.example.aop.member.annotation.ClassAop;
import com.example.aop.member.annotation.MethodAop;
import org.springframework.stereotype.Component;

@ClassAop
@Component
public class MemberServiceImpl implements MemberService {

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

    public String internal(String param) {
        return "ok";
    }

    public String twoParams(String param1, String param2) {
        return "ok";
    }
}

 

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

 

ExecutionTest.java

package com.example.aop.pointcut;

import com.example.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;

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

@Slf4j
public class ExecutionTest {

    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    Method helloMethod;

    @BeforeEach
    public void init() throws NoSuchMethodException {
        helloMethod = MemberServiceImpl.class.getMethod("hello", String.class); // method 이름이 hello, 파라미터의 타입이 String
    }
}

 

테스트 코드다. 리플렉션을 이용해서 구현한 MemberServiceImpl의 hello 메서드를 가져온다. 각 테스트의 실행마다 그 바로 직전에 리플렉션을 활용해서 메서드를 가져오기 위해 @BeforeEach를 사용했다. 

 

AspectJExpressionPointcut은 포인트컷 표현식을 처리해주는 클래스다. 여기에 포인트컷 표현식을 지정하면 된다. 이 클래스는 상위에 Pointcut 인터페이스를 가진다. 

 

 

execution

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

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

 

execution은 메서드 실행 조인 포인트를 매칭한다. 그래서 결국 모든 메서드들 중 이 표현식에 일치하는 메서드들이 AOP로 적용된다. 위 표현 방식에서 '?'가 있는 것은 생략이 가능하다는 뜻이다.

 

그럼 하나씩 천천히 알아보자. 가장 정확한(자세한) 포인트 컷으로 표현해보자. 

 

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

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

 

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

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

@Test
void exactMatch() {
    pointcut.setExpression("execution(public String com.example.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 nameWildcardMatch() {
    pointcut.setExpression("execution(* hel*(..))");

    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void nameWildcardMatch2() {
    pointcut.setExpression("execution(* *el*(..))");

    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void notNameMatch() {
    pointcut.setExpression("execution(* notMatched(..))");

    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}

 

 

패키지 매칭 관련 포인트 컷

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

@Test
void packageExactMatch() {
    pointcut.setExpression("execution(* com.example.aop.member.MemberServiceImpl.hello(..))");

    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void packageExactMatch2() {
    pointcut.setExpression("execution(* com.example.aop.member.*.*(..))");

    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void packageNotMatch() {
    // 패키지에서 (.)은 정확히 그 위치. 즉, 아래같은 경우 com.example.aop 딱 그 위치를 말한다.
    pointcut.setExpression("execution(* com.example.aop.*.*(..))");

    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}

@Test
void subPackageMatch() {
    // 패키지에서 하위 패키지까지 몽땅 포함하려면 (..)이어야 한다.
    pointcut.setExpression("execution(* com.example.aop..*.*(..))");

    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void subPackageMatch2() {
    pointcut.setExpression("execution(* com.example.aop.member..*.*(..))");

    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

 

packageNotMatch()를 확인해보면 com.example.aop.*.*(..)로 되어 있는데 이는 하위 패키지도 포함하는게 아니다. 즉, 정확히 com.example.aop의 모든 타입(인터페이스, 클래스)의 모든 메서드를 지정하는 포인트 컷이다. 하위 패키지도 포함하려면 subPackageMatch()처럼 com.example.aop..*.*(..)로 작성해야 한다. 

 

타입 매칭 포인트 컷 

타입 정보에 대한 매치 조건이다.

@Test
void typeExactMatch() {
    pointcut.setExpression("execution(* com.example.aop.member.MemberServiceImpl.*(..))");

    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

 

이처럼 정확히 패키지 + 타입(클래스)가 일치하게 포인트컷을 지정할 수 있다. 근데 한가지 조심할 게 있다. 부모 타입은 어떻게 될까?

그러니까 MemberServiceImpl은 상위에 MemberService 인터페이스가 있다. 그럼 포인트컷 표현식에 부모 타입을 작성했을 때 저 hello 메서드는 포인트컷 조건에 만족할까? 결론부터 말하면 만족한다.

@Test
void typeMatchSuperType() {
    // 상위 타입으로 expression 을 설정
    pointcut.setExpression("execution(* com.example.aop.member.MemberService.*(..))");

    // pointcut 은 상위 타입이고 상위 타입이 가지고 있는 메서드면 자식 메서드도 역시 가능하다. 이유는 자식은 부모에 들어갈 수 있으니까.
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

자식은 부모에 들어가는 게 가능하기 때문에, 포인트컷 표현식을 부모로 설정하면 자식 클래스들은 포인트컷을 만족한다. 단, 인터페이스에서 선언된 메서드에 한하여. 이 말은 무슨말이냐면 부모일지언정 부모에 선언된 메서드가 아니라 자식 내부적으로만 가지고 있는 메서드는 포인트컷을 만족하지 못한다는 말이다.

 

위에서 MemberService와 MemberServiceImpl을 보면 부모인 인터페이스에는 hello 메서드만 있고 internal은 없다. 자식인 구체 클래스에는 internal 이라는 내부 메서드가 있다. 이 땐 부모 타입으로 포인트컷을 지정하면 자식 내부적으로만 가지고 있는 메서드에는 포인트 컷 조건이 만족하지 않는다.

@Test
void typeMatchInternal() throws NoSuchMethodException {
    // 상위 타입으로 expression 을 설정
    pointcut.setExpression("execution(* com.example.aop.member.MemberService.*(..))");

    Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);

    // 상위 타입으로 pointcut 의 expression 을 설정한 경우, 상위 타입이 가지고 있는 메서드만 가능하다.
    assertThat(pointcut.matches(internalMethod, MemberServiceImpl.class)).isFalse();
}

 

 

파라미터 매칭 포인트 컷

이 파라미터 매칭 조건은 다음 예시를 보면 하나하나 다 이해가 가능할 것이다.

/**
 * 모든 메서드 중 파라미터가 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 argsMatchWildCard() {
    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 타입인 것
 * */
@Test
void argsMatchComplexExactly() throws NoSuchMethodException {
    pointcut.setExpression("execution(* *(String, String))");

    Method twoParamsMethod = MemberServiceImpl.class.getMethod("twoParams", String.class, String.class);

    assertThat(pointcut.matches(twoParamsMethod, MemberServiceImpl.class)).isTrue();
}

/**
 * 모든 메서드 중 파라미터가 딱 두개이면서 첫번째는 String, 두번째는 모든 타입
 * */
@Test
void argsMatchComplexExactly2() throws NoSuchMethodException {
    pointcut.setExpression("execution(* *(String, *))");

    Method twoParamsMethod = MemberServiceImpl.class.getMethod("twoParams", String.class, String.class);

    assertThat(pointcut.matches(twoParamsMethod, MemberServiceImpl.class)).isTrue();
}

 

 

within

within 지시자는 특정 타입 내의 조인 포인트들로 매칭을 제한한다. 이 말만 보면 무슨말인지 잘 모르겠다. 쉽게 말하면 작성한 타입이 매칭되면 그 안의 메서드들이 자동으로 매치된다.

 

WithinTest.java

package com.example.aop.pointcut;

import com.example.aop.member.MemberServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;

import java.lang.reflect.Method;

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

/**
 * Within은 타입(클래스, 인터페이스)을 지정하면 그 안에 메서드는 모두 매치가 되게 하는 방법
 * */
public class WithinTest {

    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    Method helloMethod;

    @BeforeEach
    public void init() throws NoSuchMethodException {
        helloMethod = MemberServiceImpl.class.getMethod("hello", String.class); // method 이름이 hello, 파라미터의 타입이 String
    }

    @Test
    void withinExactly() throws NoSuchMethodException {
        pointcut.setExpression("within(com.example.aop.member.MemberServiceImpl)");

        Method internal = MemberServiceImpl.class.getMethod("internal", String.class);

        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
        assertThat(pointcut.matches(internal, MemberServiceImpl.class)).isTrue();
    }

    @Test
    void withinWildCard() {
        pointcut.setExpression("within(com.example.aop.member.*Service*)");

        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    @Test
    void withinSubPackage() {
        pointcut.setExpression("within(com.example.aop..*)");

        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    @Test
    @DisplayName("타겟의 정확하게 타입에만 직접 적용해야 한다")
    void withinSuperTypeFalse() {
        // 상위 타입으로 설정하면 within 은 안된다. 정확히 그 타입으로 지정해야 한다. execution 은 이게 가능했는데 within 은 아니다.
        pointcut.setExpression("within(com.example.aop.member.MemberService)");

        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
    }
}

 

withinExactly()를 보면 within(com.example.aop.member.MemberServiceImpl)이라고 되어 있다. 이렇게 하면 MemberServiceImpl 클래스 내 메서드들이 이 포인트컷에 매칭된다.

 

주의

그러나, 주의할 부분이 있다. 표현식에 부모 타입을 지정하면 안된다. 정확하게 타입이 맞아야 한다. 이 점이 execution과 다른 점이다.

 

 

args

인자가 주어진 타입의 인스턴스인 조인 포인트로 매칭. 말이 또 어려운데 쉽게 말해 파라미터가 매치되는 녀석들이 다 조인 포인트가 된다고 보면 된다. 아래 코드를 보면 바로 이해가 될 것이다.

 

ArgsTest.java

package com.example.aop.pointcut;

import com.example.aop.member.MemberServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
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); // method 이름이 hello, 파라미터의 타입이 String
    }

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

    @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();
    }
}

 

args()를 보면, pointcut으로 args(String), args(Object),... 이렇게 되어 있다. 즉, 이 파라미터와 일치하는 메서드를 매치시키는 방법. 근데 이 args는 execution과 다르게 부모 타입도 허용한다. 즉, 파라미터의 타입이 String인 메서드라면 args(Object)로 해도 매치가 된다는 뜻이다. 

 

참고로 args 지시자는 단독으로 사용되기 보다는 파라미터 바인딩에서 주로 사용된다. 

 

 

@target, @within

정의

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

@within: 주어진 애노테이션이 있는 타입 내 조안 포인트

 

사실 그렇게 중요하지도 않고 정의만 보고서는 뭔 말인지 감이 잘 안오지만 코드로 보면 간단하다. 우선 둘 모두 타입에 있는 애노테이션으로 AOP 적용 여부를 판단한다.

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

 

@ClassAop
class Target {

}

 

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

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

 

 

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

 

AtTargetAtWithinTest.java

package com.example.aop.pointcut.annotation;

import com.example.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;


/**
 * 클래스에 붙이는 애노테이션을 기반으로 포인트컷을 만들 땐 @target, @within 을 사용할 수 있다.
 * */
@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(* com.example.aop..*(..)) && @target(com.example.aop.member.annotation.ClassAop)")
        public Object atTarget(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[@target] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

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

        // 참고로 @target, @args, args 이런 포인트컷 지시자는 단독으로 사용하면 안된다. 위 에제에서도 execution 과 같이 사용했는데
        // 그 이유는 스프링이 이런 포인트컷 지시자가 있으면 모든 스프링 빈에 AOP 를 적용하려고 시도하는데 스프링이 내부에서 사용하는 빈 중에는 final 로
        // 지정된 빈들도 있기 때문에 오류가 발생할 수 있다.
    }
}

 

 

우선 체크포인트는 다음과 같다.

 

  • Child, Parent 클래스가 있다. Child 클래스는 상위 클래스로 Parent 클래스가 있다. 
  • 두 클래스를 모두 스프링 빈으로 등록한다.
  • 에스팩트가 있고 두 개의 어드바이저가 있다. 하나는 @target 하나는 @within으로 만들어진 포인트컷이다.
  • @target@within 모두 같은 애노테이션인 ClassAop 애노테이션이 달린 클래스를 찾아 AOP로 적용한다.
  • 이 에스팩트 역시 스프링 빈으로 등록한다.
  • 스프링 빈으로 등록한 Child 클래스를 테스트 코드에서는 주입받는다.
  • 주입받은 Child 클래스의 childMethod(), parentMethod()를 각각 호출한다.
  • 결과는 childMethod() 호출 시, @target과 @within 모두 적용된다. parentMethod() 호출 시 @target만 적용되고 @within은 적용되지 않는다.

 

주의

args, @args, @target 이 포인트컷 지시자는 단독으로 사용할 수 없다. 그 이유는 이런 포인트컷이 있으면 스프링은 모든 스프링 빈에 AOP를 적용하려고 시도한다. 문제는 모든 스프링 빈에 AOP를 적용하려고 하면 스프링이 내부에서 사용하는 빈 중에는 final로 지정된 빈들도 있기 때문에 오류가 발생한다. 따라서 이런 포인트컷 지시자는 단독으로 사용하면 안되고 최대한 적용 대상을 축소하는 표현식과 함께 사용해야 한다.

 

 

@annotation, @args

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

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

 

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

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

 

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

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

 

AtAnnotationTest.java

package com.example.aop.pointcut.annotation;

import com.example.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;


/**
 * 메서드에 붙이는 애노테이션을 기반으로 포인트컷을 만들 때 사용되는 @annotation
 *
 * ⭐이거는 좀 자주 사용된다.
 * */
@Slf4j
@Import(AtAnnotationTest.AtAnnotationAspect.class)
@SpringBootTest
public class AtAnnotationTest {

    @Autowired
    MemberService memberService;

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

    @Aspect
    static class AtAnnotationAspect {

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

 

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

 

 

bean

스프링 전용 포인트컷 지시자. 빈의 이름으로 지정한다.

bean(orderService) || bean(*Repository)

 

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

 

BeanTest.java

package com.example.aop.pointcut;

import com.example.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("item");
    }

    @Aspect
    static class BeanAspect {

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

 

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

 

 

매개변수 전달 (중요⭐️)

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

orderService.orderItem("item");

 

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

 

우선 이 경우 joinPoint를 사용하거나 다음 포인트컷 지시자를 활용한다.

  • args

가장 원시적인 방법을 먼저 확인해보자.

 

포인트컷

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

 

JoinPoint를 활용하기

 

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

 

joinPoint를 활용하면 getArgs() 메서드를 사용할 수 있다. 허나, 이 방법은 배열에서 꺼내는 방식인데 그렇게 좋은 방식은 아닌것 같다.

 

args

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

 

포인트컷 지시자 'args'를 사용한다. args(arg, ..)은 첫번째 파라미터를 받고 그 이후에 파라미터는 있거나 없거나 신경쓰지 않는다는 뜻이다. 그리고 이 arg를 어드바이스의 파라미터로 이름 그대로(arg) 동일하게 받아야 한다. 

 

실제 파라미터의 타입은 String인데 그 상위 타입인 Object로 받아도 무방하다. 

 

위에서 @Around를 사용했는데 @Around는 ProceedingJoinPoint를 반드시 첫번째 파라미터로 받아야 하는 불편함이 있다. 굳이 코드 내에서 실제 객체를 호출하는 코드를 직접 호출해야 하는 경우가 아니라면 다음처럼 더 간략하게 사용할 수 있다.

@Before("allMember() && args(arg, ..)")
public void logArgs3(String arg) {
    log.info("[logArgs3] arg = {}", arg);
}

 

이번에는 상위 타입이 아닌 정확히 String으로 받아주었다. 물론 상위 타입도 상관없다.

 

 

@annotation으로 애노테이션이 가지고 있는 값들 꺼내오기

애노테이션 중에는 특정 값을 가지는 애노테이션이 있다. 다음이 그 예시다.

package com.example.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();
}

 

 

value() 라는 값을 가지는 애노테이션이다.

 

package com.example.aop.member;

import com.example.aop.member.annotation.ClassAop;
import com.example.aop.member.annotation.MethodAop;
import org.springframework.stereotype.Component;

@ClassAop
@Component
public class MemberServiceImpl implements MemberService {

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

    public String internal(String param) {
        return "ok";
    }

    public String twoParams(String param1, String param2) {
        return "ok";
    }
}

 

그 애노테이션의 value 값으로 'test value'라는 값을 가지는 hello()가 있을 때 이 값은 어떻게 가져올까?

 

다음처럼 @annotation을 활용해서 애노테이션을 파라미터로 받으면 된다.

@Before("allMember() && @annotation(annotation)")
public void atAnnotationAcceptedArgs(JoinPoint joinPoint, MethodAop annotation) {
    log.info("[@annotation Accepted]{}, annotationValue = {}", joinPoint.getSignature(), annotation.value());
}

여기서, @annotation(annotation)이라고 썼으면 파라미터에서도 'annotation'이라는 이름으로 받아야 한다. 만약 @annotation(methodAop)로 썼으면 파라미터도 'methodAop'라는 이름으로 받으면 된다. 

 

그리고 한가지 더, 원래는 @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 동적 프록시

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

 

 

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

 

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

MemberServiceImpl 구체 클래스 지정

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

 

결론은 CGLIB 프록시는 모든 경우에 AOP 적용 대상이 된다. 그리고 스프링은 기본으로 CGLIB로 프록시를 만들어낸다. 

 

 

실제로 AOP 적용을 위 설명처럼 하는지 확인해보자.

 

ThisTargetTest.java

package com.example.aop.pointcut;

import com.example.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(com.example.aop.member.MemberService)")
        public Object doThisInterface(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[this-interface] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

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

        @Around("this(com.example.aop.member.MemberServiceImpl)")
        public Object doThisConcrete(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[this-impl] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        @Around("target(com.example.aop.member.MemberServiceImpl)")
        public Object doTargetConcrete(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[target-impl] {}", 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 로그가 찍히지 않았음을 확인할 수 있다.

 

 

728x90
반응형
LIST

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

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

스프링에서 프록시를 만들고 AOP 관련 개념을 접하면 한번은 듣는 단어인 Advisor, Advice, Pointcut에 대해 정리하고자 한다.

 

우선 그 전에 다음 포스팅을 참고하면 좋을 것 같다. https://cwchoiit.tistory.com/80

 

스프링이 지원하는 프록시

https://cwchoiit.tistory.com/79 Proxy/Decorator Pattern 2 (동적 프록시) https://cwchoiit.tistory.com/78 Proxy/Decorator Pattern 이제 스프링에서 굉장히 자주 사용되는 프록시와 데코레이터 패턴을 정리해 보자. 이 프록시

cwchoiit.tistory.com

위 포스팅에서 스프링이 ProxyFactory를 제공해주고 이 프록시 팩토리로 동적 프록시를 만드는 내용을 얘기하면서 Advice라는 개념을 살짝 맛봤다. ProxyFactory에 Advice를 추가해서 프록시가 제공하는 추가 기능 로직을 담당하는 녀석이 Advice라고. 

 

SMALL

Advisor, Advice, Pointcut

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

쉽게 풀어 얘기하면 프록시는 Pointcut(어디에?) Advice(조언 = 추가기능)을 할 것인가? 그리고 그 프록시가 어디에 어떤 조언을 할 지 알려줄 Advisor(조언자)를 가지고 있다.

 

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

 

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

 

모든 대상에 대해 허용하는 Pointcut으로 Advisor를 만들기

어떤 요청이어도 Advice(프록시가 제공하는 추가 기능)를 모두 적용하는 Pointcut으로 Advisor를 만든다.

ServiceInterface.java

package com.example.advanced.common.service;

public interface ServiceInterface {
    void save();

    void find();
}

ServiceImpl.java

package com.example.advanced.common.service;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ServiceImpl implements ServiceInterface {
    @Override
    public void save() {
        log.info("save calling");
    }

    @Override
    public void find() {
        log.info("find calling");
    }
}

TimeAdvice.java

package com.example.advanced.common.advice;

import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

@Slf4j
public class TimeAdvice implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        log.info("TimeProxy Start");

        long startTime = System.currentTimeMillis();

        Object result = invocation.proceed();

        long endTime = System.currentTimeMillis();

        log.info("TimeProxy End. ResultTime = {}ms", endTime - startTime);
        return result;
    }
}

 

Advice를 만들었다. Advice를 만들기 위해 MethodInterceptor를 구현한다. 왜 Advice를 만든다고 해놓고 MethodInterceptor일까? MethodInterceptor가 상속받는 Interceptor가 있고 그 Interceptor가 상속받는 인터페이스가 'Advice'이기 때문이다. 이 MethodInterceptor를 구현하려면 'invoke()'를 구현해야한다. 이 메서드는 프록시가 추가로 제공해주는 기능에 대한 내용을 넣는 곳이다. 그리고 프록시는 항상 실제 객체가 있어야 하는데 기존에는 실제 객체를 주입받아서 프록시를 만들었는데 이 Advice에는 없다. 왜 그러냐면 ProxyFactory에 Advice가 적용될텐데 ProcyFactory가 실제 객체를 들고 있기 때문에 Advice에서는 필요가 없다. 그저 MethodInvocation 타입의 invocation.proceed()를 호출하면 ProxyFactory가 가지고 있는 실제 객체의 호출한 메서드가 호출된다.

 

AdvisorTest.java

package com.example.advanced.advisor;

import com.example.advanced.common.advice.TimeAdvice;
import com.example.advanced.common.service.ServiceImpl;
import com.example.advanced.common.service.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.MethodMatcher;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;

import java.lang.reflect.Method;

@Slf4j
public class AdvisorTest {

    @Test
    void advisorTest1() {
        // 실제 객체가 될 서비스 객체
        ServiceInterface target = new ServiceImpl();

        // ProxyFactory 객체를 생성
        ProxyFactory proxyFactory = new ProxyFactory(target);

        // DefaultPointcutAdvisor 객체를 생성.
        // 여기서 Pointcut.TRUE 는 모든 요청에 대해 내가 지금 전달한 TimeAdvice 로직을 적용하겠다는 의미다.
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice());

        // ProxyFactory에 Advisor를 추가한다.
        proxyFactory.addAdvisor(advisor);

        // ProxyFactory로부터 프록시를 꺼낸다.
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

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

 

이제 Advisor, Advice, Pointcut을 만들어보자. 우선 첫번째로 해볼 것은 아무런 필터링도 하지 않는 'Pointcut'을 만드는 것이다.

그 부분이 아래 코드이다. Advisor를 만들기 위해 DefaultPointcutAdvisor 객체를 생성하는데 이 때 생성자에 두가지가 넘어간다. Pointcut과 Advice. 모든 대상에 대해 Advice를 적용하는 Pointcut.TRUE를 넘기면 위에서 말한것처럼 아무런 필터링도 하지 않는다.

DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice());

 

 

두번째 파라미터는 'TimeAdvice()'를 넘겨주면 된다. 이렇게 만든 advisor를 ProxyFactory에 추가해준다.

proxyFactory.addAdvisor(advisor);

 

이제 프록시 팩토리에서 프록시를 꺼내서 프록시가 가진(실제 객체가 가진) 메서드를 호출할 수 있다. 이렇게 프록시를 만들고 프록시의 메서드를 호출해보자. 실제 객체가 가진 모든 메서드에 대해서 프록시가 주는 추가 기능(Advice)이 적용되어야 한다.

결과

결과 로그를 보면 save(), find() 모두 프록시가 제공하는 TimeProxy Start, End, ResultTime = Xms 로그가 찍히는 것을 볼 수 있다.

그러나, 이러면 사실상 Pointcut이 의미가 없기 때문에 Pointcut으로 필터링도 해보자. 딱 한번만 Pointcut을 직접 만들어 보자. 그 이후에는 스프링이 제공해주는 여러가지 Pointcut으로 편하게 사용하면 된다. 

 

 

일부 대상에 대해 허용하는 Pointcut으로 Advisor를 만들기

Pointcut을 만들어보자. Pointcut을 구현하면 되는데 두 가지 메서드가 있다. getClassFilter(), getMethodMatcher().

메서드 명만 봐도 어떤것을 하는 메서드인지 알 수 있을것 같다. 하나는 클래스로 필터링을 하는것이고 하나는 메서드로 필터링을 하는것.

    /**
     * 직접 만들일은 없음, 스프링이 만들어주는 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;
        }
    }

 

getMethodMatcher()는 MethodMatcher 타입을 반환하는데 이는 우리가 직접 구현하면 된다. 위 MyMethodMatcher 클래스가 그 예시이다. 이 MethodMatcher를 구현하면 세가지 메서드를 구현해야한다. 

 

우선, 간단하게 예시를 작성할거니까 'save()' 메서드만 Advice를 적용시켜보자. 이름으로 비교를 하기 위해 변수로 저장한다.

private String matchName = "save";

 

그 다음, 지금 전달받은 Method의 이름이 'save'와 일치하는지 판단한다.

boolean result = method.getName().equals(matchName);

 

맞다면, true를 반환해서 Advice가 적용되게끔 작성했다. 이제 우리가 직접만든 Pointcut으로 Advisor를 만들어보자.

@Test
@DisplayName("직접 만든 포인트컷")
void advisorTest2() {
    // 실제 객체 서비스
    ServiceInterface target = new ServiceImpl();

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

    // ProxyFactory가 만들 Advisor
    // Pointcut을 직접 만들어서 넣었고, Advice도 만들어서 넣었음.
    DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(new MyPointcut(), new TimeAdvice());

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

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

    // Pointcut에 의하여 Advice 적용된다.
    proxy.save();
    // Pointcut에 의하여 Advice 적용되지 않는다.
    proxy.find();
}

 

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

결과 로그를 보면, save calling이 찍히기 전 후에 TimeProxy Start, TimeProxy End, ResultTime = Xms가 찍히지만, find calling은 없다. Pointcut이 잘 동작하는 것을 확인할 수 있다. 이제 두번 다시 Pointcut을 직접 만들지는 않을거다. 스프링의 도움을 받자.

 

 

스프링이 제공하는 Pointcut으로 Advisor를 만들기

이제 Spring이 제공해주는 여러가지 Pointcut이 있는데 그 중 하나로 Advisor를 만들어 보자. 

@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();
}

 

스프링이 제공하는 Pointcut인 NameMatchMethodPointcut을 사용해보자. 

NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedName("save");

 

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

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

 

더 나아가서, 여러개의 Advisor를 사용할 수도 있다. 프록시가 여러개로 사용될 수 있는 것처럼 Advisor도 마찬가지로 여러개가 사용될 수 있다. 프록시 팩토리로 프록시를 만들 때 Advisor가 반드시 있어야 한다는 것은 프록시가 여러개면 Advisor도 여러개란 뜻이다. 그러나 프록시를 하나만 가지고 있고 Advisor가 여러개일수도 있다. 그리고 이게 더 일반적으로 많이 사용되는 방법이다.

 

Multi Advisor

 

이제 여러개의 Advisor를 사용해서 클라이언트 요청을 처리해보자. 우선 두 가지 경우를 다룰 것이다. 

 

1. Proxy가 2개 = Advisor가 2개

2. Proxy 1개가 가지고 있는 2개의 Advisor

 

먼저 공통으로 사용될 Advice1, 2를 먼저 살펴보자.

static class Advice1 implements MethodInterceptor {

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

 

static class Advice2 implements MethodInterceptor {

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

 

Proxy가 2개 = Advisor가 2개

@Test
@DisplayName("여러 프록시")
void multiAdvisorTest1() {
    // Client -> Proxy2(Advisor2) -> Proxy1(Advisor1) -> target

    // Proxy1 생성
    ServiceInterface target = new ServiceImpl();

    ProxyFactory proxyFactory1 = new ProxyFactory(target);
    DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
    proxyFactory1.addAdvisor(advisor);
    ServiceInterface proxy1 = (ServiceInterface) proxyFactory1.getProxy();

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

    proxy2.save();
}

 

뭐 새로운 개념은 없다 ProxyFactory를 두개 생성해서 하나는 실제 객체를 target으로 받고 하나는 target을 실제 객체로 받은 프록시를 받으면 된다. 그래서 프록시를 target으로 받은 proxy를 실행하면 끝이다. 결과를 보자.

 

차례대로 Advice2, Advice1이 실행된다. 그리고 실제 객체인 target의 로직까지 실행됐다. 그러나 이건 프록시를 계속 만들어내야 하니까 프록시 하나에 여러개의 Advisor를 생성해보자. 

 

Proxy 1개가 가지고 있는 2개의 Advisor

@Test
@DisplayName("하나의 프록시, 여러 어드바이저")
void multiAdvisorTest2() {
    // Client -> Proxy -> Advisor2 -> Advisor1 -> target

    DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
    DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());

    // Proxy 생성
    ServiceInterface target = new ServiceImpl();

    ProxyFactory proxyFactory = new ProxyFactory(target);
    proxyFactory.addAdvisor(advisor2);
    proxyFactory.addAdvisor(advisor);

    ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

    proxy.save();
}

 

Advisor2개를 먼저 생성한다. 그리고 ProxyFactory에서 addAdvisor()를 두번하면 끝난다. 그럼 위 코드상 먼저 등록된 advisor2가 먼저 실행되고 그다음 advisor1이 실행될 것이다. 결과를 보자.

 

 

결론

이제 동적 프록시를 만들 때 스프링의 도움을 받아 ProxyFactory로 간단하게 만들 수 있었다. 또한 인터페이스를 제공하느냐 구체 클래스를 제공하느냐에 따라 동적 프록시를 만들어내는 방법이 달라지는 문제 또한 스프링의 ProxyFactory가 해결해 주었다. 이렇게 스프링은 유사한 여러 구현 기술이 있을 때 추상화를 통해 편리하게 사용할 수 있게 해준다. 그리고 ProxyFactory를 통해 프록시를 만들 때 항상 Advisor가 생성되어야 한다는 사실도 알았다. Advisor는 하나의 프록시에 여러개가 들어갈 수 있고 Pointcut으로 어떤 요청에 대해서는 Advice를 적용하고 적용하지 않을지도 필터링이 가능하다는 사실도 알았다. 

 

참고로, 이후에 AOP를 배우겠지만 하나의 target에 여러 AOP를 적용을 한다고 해도 프록시는 하나만 만들어진다. 위 내용이 그 근거이다.

 

Advisor, Advice, Pointcut을 실제 프로젝트 코드에 도입하기

이제 Advisor, Advice, Pointcut이 뭔지도 배웠고 ProxyFactory를 통해서 동적 프록시를 쉽게 만들 수 있게 됐으니 실제 프로젝트 코드에 도입해보자. 두 가지 케이스를 해볼것이다.

 

  • 인터페이스로 프록시를 만드는 경우
  • 구체 클래스로 프록시를 만드는 경우

 

우선, 인터페이스건 구체 클래스건을 떠나 프록시의 추가 기능을 담당하는 Advice를 만들어야 한다. 

 

Advice 만들기

LogTraceAdvice.java

package com.example.advanced.app.proxy.config.v3_proxyfactory.advice;

import com.example.advanced.trace.TraceStatus;
import com.example.advanced.trace.logtrace.LogTrace;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

import java.lang.reflect.Method;

public class LogTraceAdvice implements MethodInterceptor {

    private final LogTrace logTrace;

    public LogTraceAdvice(LogTrace logTrace) {
        this.logTrace = logTrace;
    }

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        TraceStatus status = null;

        try {
            Method method = invocation.getMethod();

            // Ex) "OrderController.request()"
            String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()";

            status = logTrace.begin(message);

            Object result = invocation.proceed();

            logTrace.end(status);
            return result;
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }
}

 

Advice를 만들기 위해 MethodInterceptor를 구현하는 LogTraceAdvice 클래스를 만들었다. MethodInterceptor가 구현해야 하는 Invoke()를 기존에 계속 사용했던 LogTrace 기능으로 채워넣었다. Advice는 실제 객체를 주입받지 않아도 되기 때문에 편리함을 준다. 

 

이제 Advice를 만들었으니까 ProxyFactory를 통해서 만든 프록시를 스프링 빈으로 등록해보자.

 

인터페이스로 프록시를 만드는 경우

ProxyFactoryConfigV1.java

package com.example.advanced.app.proxy.config.v3_proxyfactory;

import com.example.advanced.app.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;
import com.example.advanced.app.proxy.v1.OrderRepositoryV1;
import com.example.advanced.app.proxy.v1.OrderRepositoryV1Impl;
import com.example.advanced.app.proxy.v1.OrderServiceV1;
import com.example.advanced.app.proxy.v1.OrderServiceV1Impl;
import com.example.advanced.app.proxy_v1_controller.OrderControllerV1;
import com.example.advanced.app.proxy_v1_controller.OrderControllerV1Impl;
import com.example.advanced.trace.logtrace.LogTrace;
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;

@Configuration
public class ProxyFactoryConfigV1 {

    @Bean
    public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
        OrderControllerV1Impl orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));

        ProxyFactory proxyFactory = new ProxyFactory(orderController);
        proxyFactory.addAdvisor(getAdvisor(logTrace));

        return (OrderControllerV1) proxyFactory.getProxy();
    }

    @Bean
    public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
        OrderServiceV1Impl orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));

        ProxyFactory proxyFactory = new ProxyFactory(orderService);
        proxyFactory.addAdvisor(getAdvisor(logTrace));

        return (OrderServiceV1) proxyFactory.getProxy();
    }

    @Bean
    public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
        OrderRepositoryV1Impl orderRepository = new OrderRepositoryV1Impl();

        ProxyFactory proxyFactory = new ProxyFactory(orderRepository);
        proxyFactory.addAdvisor(getAdvisor(logTrace));

        return (OrderRepositoryV1) proxyFactory.getProxy();
    }

    private Advisor getAdvisor(LogTrace logTrace) {

        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");

        LogTraceAdvice advice = new LogTraceAdvice(logTrace);

        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}

 

컨트롤러, 서비스, 레포지토리를 프록시로 스프링 컨테이너에 등록해야 한다. 그렇기 때문에 Configuration 파일이 필요하고 여기서 각각을 프록시로 등록하기 위해 ProxyFactory로 프록시를 만든다. ProxyFactory로 프록시를 만들려면 Advisor가 필요하기 때문에 Advisor를 만들어야 한다. Advisor는 Advice와 Pointcut이 필요하기 때문에 우리가 이 위에서 만든 Advice를 전달해야 하고 그 Advice가 적용될 Pointcut을 스프링이 제공하는 NameMatchMethodPointcut을 사용해서 만든다. 메서드 명이 'request'로 시작하는 것과 'order'로 시작하는 것과 'save'로 시작하는 것들은 이 Advice를 적용한다. 이제 Pointcut과 Advice를 Advisor에게 전달한다. 

 

 

이렇게 Config 파일 하나를 만들면 끝난다. 이게 인터페이스를 이용해서 ProxyFactory로 동적 프록시를 만드는 방법이다. 

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

 

 

구체 클래스로 프록시를 만드는 경우

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

 

ProxyFactoryConfigV2.java

package com.example.advanced.app.proxy.config.v3_proxyfactory;

import com.example.advanced.app.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;
import com.example.advanced.app.proxy.v2.OrderControllerV2;
import com.example.advanced.app.proxy.v2.OrderRepositoryV2;
import com.example.advanced.app.proxy.v2.OrderServiceV2;
import com.example.advanced.trace.logtrace.LogTrace;
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;

@Configuration
public class ProxyFactoryConfigV2 {

    @Bean
    public OrderControllerV2 orderControllerV2(LogTrace logTrace) {
        OrderControllerV2 orderController = new OrderControllerV2(orderServiceV2(logTrace));

        ProxyFactory proxyFactory = new ProxyFactory(orderController);
        proxyFactory.addAdvisor(getAdvisor(logTrace));

        return (OrderControllerV2) proxyFactory.getProxy();
    }

    @Bean
    public OrderServiceV2 orderServiceV2(LogTrace logTrace) {
        OrderServiceV2 orderService = new OrderServiceV2(orderRepositoryV2(logTrace));

        ProxyFactory proxyFactory = new ProxyFactory(orderService);
        proxyFactory.addAdvisor(getAdvisor(logTrace));

        return (OrderServiceV2) proxyFactory.getProxy();
    }

    @Bean
    public OrderRepositoryV2 orderRepositoryV2(LogTrace logTrace) {
        OrderRepositoryV2 orderRepository = new OrderRepositoryV2();

        ProxyFactory proxyFactory = new ProxyFactory(orderRepository);
        proxyFactory.addAdvisor(getAdvisor(logTrace));

        return (OrderRepositoryV2) proxyFactory.getProxy();
    }

    private Advisor getAdvisor(LogTrace logTrace) {

        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");

        LogTraceAdvice advice = new LogTraceAdvice(logTrace);

        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}

 

끝이다. Advice도 Pointcut도 이미 만들어 놓은거니까 가져다가 사용만 하면 된다. 아니면 이 코드처럼 복붙으로 처리해 버리기. 

'/v2/request' 로 요청하면 마찬가지로 LogTrace의 정보를 출력한다.

 

 

결론

확실히 프록시로 사용될 코드도 Advice 하나만 만들면 되고, 동적 프록시를 만들기 때문에 프록시를 일일이 만들어 줄 필요도 없으며 구체클래스냐 인터페이스냐에 따라 나뉘어지는 동적 프록시 생성 방법을 스프링의 도움을 받아 고민하지 않게됐다. 훨씬 개선되었지만 여전히 불편함은 남아있다. 어떤 게 불편하냐면 일단 프록시로 만들기 원하는 것들은 전부 이렇게 빈으로 등록해야 한다는 것이다. 만약 100개면 100개의 빈을 이렇게 일일이 등록해야 한다. 또 한가지는 컴포넌트 스캔 대상은 프록시로 만들수가 없다는 사실이다. 왜냐하면 스프링이 이미 스프링 컨테이너에 컴포넌트 스캔 대상 클래스를 등록해버렸기 때문에. 이도 역시 더 좋은 코드로 개선될 수 있지 않을까? '빈 후처리기'이다.

728x90
반응형
LIST

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

AOP와 @Aspect, @Around  (0) 2023.12.29
빈 후처리기(BeanPostProcessor)  (2) 2023.12.27
Proxy/Decorator Pattern 2 (JDK 동적 프록시)  (0) 2023.12.14
Proxy/Decorator Pattern  (0) 2023.12.13
Template Callback Pattern  (0) 2023.12.12

+ Recent posts