본문 바로가기
Coding/Java Spring

Spring 빈 후처리기 정리

by Hide­ 2023. 7. 19.
반응형

의존성 추가

implementation("org.springframework.boot:spring-boot-starter-aop")

먼저 위처럼 aop에 관한 의존성을 추가해준다. 이렇게 하면 aspectjweaver라는 aspectJ 관련 라이브러리를 등록하고 스프링 부트가 AOP관련 클래스를 자동으로 빈으로 등록한다. 

AutoProxyCreator

스프링 부트는 AnnotationAwareAspectJAutoProxyCreator라는 빈 후처리기를 자동으로 빈으로 등록한다. 해당 빈은 자동으로 프록시를 생성해주는 클래스로써 스프링 빈으로 등록된 Advisor들을 자동으로 찾은 후 프록시가 필요하다면 자동으로 프록시를 적용해준다. 일반적으로 AOP를 구현할 때 클래스 위에 @Aspect 어노테이션을 붙여주는데, AnnotationAwareAspectJAutoProxyCreator가 자동으로 @Aspect 어노테이션을 인식하여 프록시를 만든 후 AOP를 적용해준다고 생각하면 된다.

간단하게 과정을 살펴보자면 다음과 같다.

  1. 스프링이 스프링 빈 대상이 되는 객체(@Bean, 컴포넌트 스캔)를 생성한다.
  2. 생성된 객체를 스프링 컨테이너에 등록하기 전에 빈 후처리기에 전달한다.
  3. AnnotationAwareAspectJAutoProxyCreator는 스프링 컨테이너에서 모든 Advisor를 조회한다.
  4. 3번의 결과를 대상으로 포인트컷을 사용하여 해당 객체가 프록시 적용 대상인지를 판단한다.
  5. 적용 대상이라면 프록시를 생성하고 반환한다. 대상이 아니라면 원본 객체를 반환한다.
  6. 반환받은 객체를 스프링 빈으로 등록한다.

프록시는 내부에 Advisor와 실제 호출해야할 대상 객체 정보를 알고있다.

@Configuration
public class AutoProxyConfig {
    @Bean
    public Advisor advisor(LogTrace logTrace) {
        // Pointcut
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("/api/a*", "/api/b*");

        // Advice
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);
        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}

예를 들어 위와 같이 Advisor타입의 빈을 등록하기만 하면 스프링이 이미 빈 후처리기(AnnotationAwareAspectJAutoProxyCreator)를 빈으로 등록해둔 상태이기 때문에 자동으로 Advisor를 탐색하기 때문이다. 

Pointcut

포인트컷은 다음과 같이 2가지에 사용된다.

  • 생성 단계 - 프록시 적용 여부 판단
    • AnnotationAwareAspectJAutoProxyCreator는 해당 빈이 프록시를 생성해야 하는지 검사한다.
    • 클래스/메소드 조건을 모두 비교하여 조건이 맞는다면 프록시를 생성한다.
    • 조건에 맞는게 없다면 프록시를 생성하지 않는다.
  • 사용 단계 - 어드바이스 적용 여부 판단
    • 프록시가 호출되었을 때 Advice를 적용할 지 말지 Pointcut을 통해 판단한다.
    • 적용 대상이라면 프록시는 Advice를 먼저 호출하고 타겟 대상 객체를 호출한다.
    • 적용 대상이 아니라면 Advice를 호출하지않고 바로 타겟 대상 객체를 호출한다.

AspectJExpressionPointcut

실무에서는 위 포인트컷만 사용하는데 이를 사용하면 AspectJ AOP에 특화된 정밀한 표현식을 만들 수 있다.

AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* com.teamhide.api..*(..))");

예를 들어 위와 같이 작성할 수 있는데, com.teamhide.api 하위 모든 패키지를 대상으로 하고 파라미터의 종류나 개수/리턴 타입을 상관하지 않는다는 뜻이다.

  • *: 모든 반환 타입
  • com.teamhide.api..: 해당 패키지와 그 하위 패키지
  • *(..): * 모든 메소드 이름, (..) 파라미터 상관하지 않음
  • !: 제외

참고로 프록시 - Advisor가 1:1 매칭이 아니라 하나의 프록시에 매칭되는 여러개의 Advisor가 생성되는 구조이다.

@Aspect

스프링에 프록시를 적용하려면 Pointcut과 Advice로 구성되어있는 Advisor를 만들어서 스프링 빈으로 등록하면 된다. 그러면 AnnotationAwareAspectJAutoProxyCreator가 자동으로 처리해준다. AnnotationAwareAspectJAutoProxyCreator는 자동 프록시 생성기로써 스프링 빈으로 등록된 Advisor들을 모두 찾고 자동으로 프록시를 적용해준다.

스프링은 @Aspect 라는 어노테이션을 통해 매우 편리하게 Pointcut과 Advice로 구성되어있는 Advisor 자동 생성 기능을 지원한다. 

@Aspect
public class LogTraceAspect {
    @Around("execution(* com.teamhide.batch..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        // Pre logic
        joinPoint.proceed();
        // Post logic
    }
}

위처럼 클래스 상단에 @Aspect 어노테이션을 붙이고 내부에 @Around 어노테이션을 통해 Pointcut을 지정하고 ProceedingJoinPoint를 인자로 받는 메소드를 하나 생성하면, 내부에 전/후처리 로직을 추가할 수 있다.

쉽게 설명하자면, @Aspect를 통해 어노테이션 기반 프록시 적용 대상으로 적용하고 @Around를 통해 Pointcut을 지정한 후 @Around가 선언된 메소드를 통해 Advice를 구현하는 것이라고 볼 수 있다. 참고로 joinPoint를 통해서는 아래와 같은 정보 또한 얻어올 수 있다.

  • joinPoint.getTarget(): 실제 호출 대상
  • joinPoint.getArgs(): 전달 인자
  • joinPoint.getSignature(): joinPoint 시그니처

@Aspect 프록시

위에서 AnnotationAwareAspectJAutoProxyCreator는 자동으로 빈으로 등록된 Advisor를 탐색한 후 프록시를 생성/적용해준다고 했다. 여기서 한가지 일을 더 하는데 바로 @Aspect 어노테이션이 붙은 클래스 또한 탐색하여 Advisor로 만들어준다는 점이다. 그렇기 때문에 클래스 이름이 AnnotationAware(어노테이션을 인식하는)로 시작한다.

한마디로 @Aspect 어노테이션을 보고 Advisor로 변환하여 저장하고 해당 Advisor를 기반으로 프록시를 생성해준다.

자세하게 단계를 나눠 설명하자면 다음과 같다.

  1. 스프링 어플리케이션 로딩 시점에 AnnotationAwareAspectJAutoProxyCreator를 호출한다.
  2. AnnotationAwareAspectJAutoProxyCreator는 스프링 컨테이너에서 @Aspect 어노테이션이 붙은 모든 빈을 조회한다.
  3. BeanFactoryAspectJAdvisorsBuilder를 통해 @Aspect 어노테이션 정보를 기반으로 Advisor를 생성한다.
  4. 생성한 Advisor를 BeanFactoryAspectJAdvisorsBuilder 내부에 저장한다.

BeanFactoryAspectJAdvisorsBuilder는 @Aspect의 정보를 기반으로 Pointcut, Advice, Advisor를 생성하고 보관하는 역할을 수행한다. @Aspect의 정보를 기반으로 Advisor를 만들고 내부 저장소에 캐시한다. 만약 캐시에 이미 Advisor가 존재한다면 해당 정보를 반환한다.

private final Map<String, List<Advisor>> advisorsCache = new ConcurrentHashMap<>();

실제 BeanFactoryAspectJAdvisorsBuilder 클래스 내부로 들어가보면 위처럼 캐시를 위한 Map이 선언되어있음을 확인할 수 있다.

 

출처: 인프런 스프링 핵심원리 - 고급편