오봉이와 함께하는 개발 블로그

스프링 핵심 원리 고급편 - 스프링이 제공하는 빈 후처리기 1, 2 본문

BE/Spring

스프링 핵심 원리 고급편 - 스프링이 제공하는 빈 후처리기 1, 2

오봉봉이 2024. 10. 8. 00:06
728x90

스프링 핵심 원리 - 스프링이 제공하는 빈 후처리기 1

build.gradle - 추가

implementation 'org.springframework.boot:spring-boot-starter-aop'

이 라이브러리를 추가하면 aspectjweaver라는 aspectJ관련 라이브러리를 등록하고, 스프링 부트가 AOP 관련 클래스를 자동으로 스프링 빈에 등록한다.
스프링 부트가 없던 시절에는 @EnableAspectJAutoProxy를 직접 사용해야 했는데, 이 부분을 스프링 부트가 자동으로 처리해준다.
aspectJ 는 뒤에서 설명하고, 스프링 부트가 활성화하는 빈은 AopAutoConfiguration 를 참고하자.

자동 프록시 생성기 - AutoProxyCreator

  • 앞서 이야기한 스프링 부트 자동 설정으로 AnnotationAwareAspectJAutoProxyCreator라는 빈 후처리기가 스프링 빈에 자동으로 등록된다.
  • 이름 그대로 자동으로 프록시를 생성해주는 빈 후처리기이다.
  • 이 빈 후처리기는 스프링 빈으로 등록된 Advisor 들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시 를 적용해준다.
  • Advisor 안에는 PointcutAdvice 가 이미 모두 포함되어 있다.
  • 따라서 Advisor 만 알고 있으면 그 안에 있는 Pointcut 으로 어떤 스프링 빈에 프록시를 적용해야 할지 알 수 있고 Advice 로 부가 기능을 적용하면 된다.

참고로 AnnotationAwareAspectJAutoProxyCreator@AspectJ와 관련된 AOP 기능도 자동으로 찾아서
처리해준다.
Advisor 는 물론이고, @Aspect 도 자동으로 인식해서 프록시를 만들고 AOP를 적용해준다.

image
  • 1. 생성: 스프링이 스프링 빈 대상이 되는 객체를 생성한다. (@Bean, 컴포넌트 스캔 모두 포함)
  • 2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
  • 3. 모든 Advisor 빈 조회: 자동 프록시 생성기 - 빈 후처리기는 스프링 컨테이너에서 모든 Advisor 를 조회한다. 때문에 Advisor는 스프링 빈으로 등록되어야 한다.
  • 4. 프록시 적용 대상 체크: 앞서 조회한 Advisor 에 포함되어 있는 포인트컷을 사용해서 해당 객체가 프록시를 적용할 대상인지 아닌지 판단한다. 이때 객체의 클래스 정보는 물론이고, 해당 객체의 모든 메서드를 포인트컷에 하나하나 모두 매칭해본다. 그래서 조건이 하나라도 만족하면 프록시 적용 대상이 된다. 예를 들어서 10개의 메서드 중에 하나만 포인트컷 조건에 만족해도 프록시 적용 대상이 된다.
  • 5. 프록시 생성: 프록시 적용 대상이면 프록시를 생성하고 반환해서 프록시를 스프링 빈으로 등록한다. 만약 프록시 적용 대상이 아니라면 원본 객체를 반환해서 원본 객체를 스프링 빈으로 등록한다.
  • 6. 빈 등록: 반환된 객체는 스프링 빈으로 등록된다.
image

프록시는 내부에 어드바이저와 실제 호출해야할 대상 객체(target)을 알고 있다.
코드를 통해 바로 적용해보자.

@Slf4j
@Configuration
@Import({AppConfigV1.class, AppConfigV2.class})
public class AutoProxyConfig {

  @Bean
  public Advisor advisor1(LogTrace logTrace) {
    //pointcut
    NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
    pointcut.setMappedNames("request*", "order*", "save*");
    //advice
    LogTraceAdvice advice = new LogTraceAdvice(logTrace);
    //advisor = pointcut + advice
    return new DefaultPointcutAdvisor(pointcut, advice);
  }
}
  • AutoProxyConfig코드를 보면 advisor1이라는 어드바이저 하나만 등록했다.
  • 빈 후처리기는 이제 등록하지 않아도 된다. 스프링은 자동 프록시 생성기라는(AnnotationAwareAspectJAutoProxyCreator) 빈 후처리기를 자동으로 등록해준다.
  • 이전 코드와의 차이를 보면 advisor가 스프링 빈으로 등록이 되었다
@Bean
public Advisor advisor1(...) { ... }
@Import(AutoProxyConfig.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app") //주의
public class ProxyApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProxyApplication.class, args);
    }

    @Bean
    public LogTrace logTrace() {
        return new ThreadLocalLogTrace();
    }

}

실행

중요: 포인트컷은 두 가지에 사용된다.

  1. 프록시 적용 여부 판단
    • 자동 프록시 생성기는 포인트컷을 사용해서 해당 빈이 프록시를 생성할 필요가 있는지 없는지 체크한다.
    • 클래스 + 메서드 조건을 모두 비교한다.
    • 이때 모든 메서드를 체크하는데, 포인트컷 조건 하나하나 매칭해본다.
    • 만약 조건에 맞는 것이 하나라도 있다면 프록시를 생성한다.
    • 예를 들어 특정 ControllerrequestOrder()라는 메서드와 responseOrder()라는 메서드가 있을 때 위 코드에서 포인트컷으로 request*를 등록했기 때문에 request가 있는 Controller는 프록시를 생성, 등록한다.
    • 조건에 맞는 것이 하나도 없을 경우 프록시가 필요 없기 때문에 생성하지 않는다.
  2. 어드바이서 적용 여부 판단 - 사용 단계
    • 프록시가 호출되었을 때 부가 기능인 어드바이스를 적용할지, 말지 포인트컷을 보고 판단한다.
    • 1번 예시에서 설명한 Controller는 이미 프록시가 걸려있다.
    • ControllerrequestOrder는 현재 포인트컷 조건에 만족하므로 프록시는 어드바이스를 먼저 호출하고 비즈니스 로직을 호출한다.
    • ControllerresponseOrder는 현재 포인트컷 조건에 만족하지 않기 때문에 어드바이스를 호출하지 않고 비즈니스 로직을 호출한다.

참고: 프록시를 모든 곳에 생성하는 것은 비용 낭비이다. 꼭 필요한 곳에 최소한의 프록시를 적용해야 한다. 그래서 자동 프록시 생성기는 모든 스프링 빈에 프록시를 적용하는 것이 아니라 포인트컷으로 한번 필터링해서 어드바이스가 사용될 가능성이 있는 곳에만 프록시를 생성한다.

스프링 핵심 원리 - 스프링이 제공하는 빈 후처리기 2

먼저 애플리케이션 로딩 로그를 보자

[38b9c73e] EnableWebMvcConfiguration.requestMappingHandlerAdapter()
[38b9c73e] EnableWebMvcConfiguration.requestMappingHandlerAdapter() time=23ms
[f2ed13eb] EnableWebMvcConfiguration.requestMappingHandlerMapping()
[f2ed13eb] EnableWebMvcConfiguration.requestMappingHandlerMapping() time=3ms

애플리케이션 서버를 실행해보면, 스프링이 초기화 되면서 기대하지 않은 이러한 로그들이 올라온다.
그 이유는 지금 사용한 포인트컷이 단순히 메서드 이름에 "request*", "order*", "save*"만 포함되어 있으면 매칭 된다고 판단하기 때문이다.
결국 스프링이 내부에서 사용하는 빈에도 메서드 이름에 request라는 단어만 들어가 있으면 프록시가 만들어지고, 어드바이스도 적용되는 것이다.

결론적으로 패키지에 메서드 이름까지 함께 지정할 수 있는 매우 정밀한 포인트컷이 필요하다.

AspectJExpressionPointcut

AspectJ라는 AOP에 특화된 포인트컷 표현식을 적용할 수 있다. AspectJ 포인트컷 표현식과 AOP는 나중에 자세하게 보고 지금은 그냥 이런 것이 있구나 정도만 생각하고 넘어가자.

  @Bean
  public Advisor advisor2(LogTrace logTrace) {
    //pointcut
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    pointcut.setExpression("execution(* hello.proxy.app..*(..))");
    //advice
    LogTraceAdvice advice = new LogTraceAdvice(logTrace);
    //advisor = pointcut + advice
    return new DefaultPointcutAdvisor(pointcut, advice);
  }

(advisor1의 @Bean은 주석 처리하자.)

  • AspectJExpressionPointcut
    • AspectJ 포인트컷 표현식을 적용할 수 있다.
  • execution(* hello.proxy.app..*(..))
    • AspectJ가 제공하는 포인트컷 표현식이다.
    • * : 모든 반환 타입
    • hello.proxy.app.. : 해당 패키지와 그 하위 패키지
    • *(..) : * 모든 메서드 이름, (..) 파라미터는 상관 없음

쉽게 이야기해서 hello.proxy.app패키지와 그 하위 패키지의 모든 메서드는 포인트컷의 매칭 대상이 된다.

실행

실행하면 동일한 결과가 나오는데, http://localhost:8080/v1/no-log를 들어가도 로그가 찍힌다.
다시 AspectJExpression을 작성해보자.

  @Bean
  public Advisor advisor3(LogTrace logTrace) {
    //pointcut
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    pointcut.setExpression("execution(* hello.proxy.app..*(..)) && !execution(* hello.proxy.app..noLog(..))");
    //advice
    LogTraceAdvice advice = new LogTraceAdvice(logTrace);
    //advisor = pointcut + advice
    return new DefaultPointcutAdvisor(pointcut, advice);
  }

(advisor1, 2의 @Bean은 주석 처리하자.)
표현식을 다음과 같이 수정했다.

execution(* hello.proxy.app..*(..)) && !execution(* hello.proxy.app..noLog(..))

  • &&: 두 조건을 모두 만족해야 함
  • !: 반대
    hello.proxy.app패키지와 하위 패키지의 모든 메서드는 포인트컷에 매칭하되, noLog()메서드는 제외하라는 뜻이다.

다시 http://localhost:8080/v1/no-log를 들어가보면 로그가 뜨지 않는다.

출처: 김영한 지식공유자의 스프링 핵심 원리 고급편

728x90
Comments