Advance I/Spring

22.10.17

kinggora 2022. 10. 17. 21:05

스프링이 제공하는 빈 후처리기1

build.gradle - 추가

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

 

이 라이브러리를 추가하면 aspectjweaver 라는 aspectJ 관련 라이브러리를 등록하고, 스프링 부트가 AOP 관련 클래스를 자동으로 스프링 빈에 등록한다.

스프링 부트가 없던 시절에는 @EnableAspectJAutoProxy 를 직접 사용해야 했는데, 이 부분을 스프링 부트가 자동으로 처리해준다. (aspectJ 는 뒤에서 설명)

스프링 부트가 활성화하는 빈은 AopAutoConfiguration 를 참고하자.

자동 프록시 생성기 ( AutoProxyCreator )

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

참고: AnnotationAwareAspectJAutoProxyCreator 는 @AspectJ와 관련된 AOP 기능도 자동으로 찾아서 처리해준다.

Advisor 는 물론이고, @Aspect 도 자동으로 인식해서 프록시를 만들고 AOP를 적용해준다.

 

자동 프록시 생성기의 작동 과정

1. 생성: 스프링이 스프링 빈 대상이 되는 객체를 생성한다. ( @Bean , 컴포넌트 스캔 모두 포함)

2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.

3. 모든 Advisor 빈 조회: 자동 프록시 생성기 - 빈 후처리기는 스프링 컨테이너에서 모든 Advisor 를 조회한다.

4. 프록시 적용 대상 체크: 앞서 조회한 Advisor 에 포함되어 있는 포인트컷을 사용해서 해당 객체가 프록시를 적용할 대상인지 아닌지 판단한다. 이때 객체의 클래스 정보는 물론이고, 해당 객체의 모든 메서드를 포인트컷에 하나하나 모두 매칭해본다. 그래서 조건이 하나라도 만족하면 프록시 적용 대상이 된다. 예를 들어서 10개의 메서드 중에 하나만 포인트컷 조건에 만족해도 프록시 적용 대상이 된다.

5. 프록시 생성: 프록시 적용 대상이면 프록시를 생성하고 반환해서 프록시를 스프링 빈으로 등록한다. 만약 프록시 적용 대상이 아니라면 원본 객체를 반환해서 원본 객체를 스프링 빈으로 등록한다.

6. 빈 등록: 반환된 객체는 스프링 빈으로 등록된다.

 

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

 

AutoProxyConfig

@Slf4j
@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AutoProxyConfig {

    @Bean
    public Advisor adviser1(LogTrace logTrace) {
        //Pointcut
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");
        //Advice
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);
        //Advisor
        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}
  • 어드바이저만 직접 스프링 빈으로 등록했다.
  • 빈 후처리기는 이제 등록하지 않아도 된다. 스프링은 자동 프록시 생성기라는 ( AnnotationAwareAspectJAutoProxyCreator ) 빈 후처리기를 자동으로 등록해준다.

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

1. 프록시 적용 여부 판단 - 생성 단계

  • 자동 프록시 생성기는 포인트컷을 사용해서 해당 빈이 프록시를 생성할 필요가 있는지 없는지 체크한다.
  • 클래스 + 메서드 조건을 모두 비교한다. 이때 모든 메서드를 체크하는데, 포인트컷 조건에 하나하나 매칭해본다. 만약 조건에 맞는 것이 하나라도 있으면 프록시를 생성한다.

예) orderControllerV1 은 request() , noLog() 가 있다. 여기에서 request() 가 조건에 만족하므로 프록시를 생성한다.

만약 조건에 맞는 것이 하나도 없으면 프록시를 생성할 필요가 없으므로 프록시를 생성하지 않는다.

 

2. 어드바이스 적용 여부 판단 - 사용 단계

  • 프록시가 호출되었을 때 부가 기능인 어드바이스를 적용할지 말지 포인트컷을 보고 판단한다.

예) orderControllerV1 은 이미 프록시가 걸려있다.

orderControllerV1 의 request() -> 포인트컷 조건 만족 -> 프록시는 어드바이스를 먼저 호출하고, target 을 호출

orderControllerV1 의 noLog() -> 포인트컷 조건 만족X -> 어드바이스를 호출하지 않고 바로 target 만 호출

 

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

스프링이 제공하는 빈 후처리기2

애플리케이션 서버를 실행해보면, 스프링이 초기화 되는 과정에 실제 클라이언트의 호출 없이 기대하지 않은 로그들이 올라온다.

그 이유는 지금 사용한 포인트컷이 단순히 메서드 이름에 "request*", "order*", "save*" 만 포함되어 있으면 매칭 된다고 판단하기 때문이다. 결국 스프링이 내부에서 사용하는 빈에도 메서드 이름에 request 라는 단어만 들어가 있으면 프록시가 만들어지고 되고, 어드바이스도 적용되는 것이다.

 

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

AspectJExpressionPointcut

AspectJ라는 AOP에 특화된 포인트컷 표현식을 적용할 수 있다.

 

AutoProxyConfig - 추가

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

AspectJExpressionPointcut : AspectJ 포인트컷 표현식을 적용할 수 있다.

execution(* hello.proxy.app..*(..)) : AspectJ가 제공하는 포인트컷 표현식이다.

  • * : 모든 반환 타입
  • hello.proxy.app.. : 해당 패키지와 그 하위 패키지
  • *(..) : * 모든 메서드 이름, (..) 파라미터는 상관 없음

=> hello.proxy.app 패키지와 그 하위 패키지의 모든 메서드는 포인트컷의 매칭 대상이 된다. 즉, noLog() 도 로그가 남는다.

 

표현식 수정

execution(* hello.proxy.app..*(..)) && !execution(* hello.proxy.app..noLog(..))
  • && : 두 조건을 모두 만족해야 함
  • ! : 반대

=> hello.proxy.app 패키지와 하위 패키지의 모든 메서드는 포인트컷의 매칭하되, noLog() 메서드는 제외하라는 뜻이다.

하나의 프록시, 여러 Advisor 적용

어떤 스프링 빈이 advisor1 , advisor2 가 제공하는 포인트컷의 조건을 모두 만족하면 프록시 자동 생성기는 프록시를 몇 개 생성할까?

프록시 자동 생성기는 프록시를 하나만 생성한다. 프록시 팩토리가 생성하는 프록시는 내부에 여러 advisor 들을 포함할 수 있기 때문에, 프록시를 여러 개 생성해서 비용을 낭비할 이유가 없다.

프록시 자동 생성기 상황별 정리

  • advisor1 의 포인트컷만 만족 -> 프록시1개 생성, 프록시에 advisor1 만 포함
  • advisor1 , advisor2 의 포인트컷을 모두 만족 -> 프록시1개 생성, 프록시에 advisor1 , advisor2 모두 포함
  • advisor1 , advisor2 의 포인트컷을 모두 만족하지 않음 -> 프록시가 생성되지 않음

 

이후에 설명할 스프링 AOP도 동일한 방식으로 동작한다.

정리

자동 프록시 생성기인 AnnotationAwareAspectJAutoProxyCreator 덕분에 Advisor 만 스프링 빈으로 등록하면 프록시를 적용할 수 있다.

Advisor = Pointcut + Advice

 

이후부터는 @Aspect 애노테이션을 사용해서 더 편리하게 포인트컷과 어드바이스를 만들고 프록시를 적용해보자.

8. @Aspect AOP

@Aspect 프록시 - 적용

스프링 애플리케이션에 프록시를 적용하려면 포인트컷과 어드바이스로 구성되어 있는 어드바이저 ( Advisor )를 만들어서 스프링 빈으로 등록하면 된다.

그러면 나머지는 앞서 배운 자동 프록시 생성기가 모두 자동으로 처리해준다. 자동 프록시 생성기는 스프링 빈으로 등록된 어드바이저들을 찾고, 스프링 빈들에 자동으로 프록시를 적용해준다. (물론 포인트컷이 매칭되는 경우)

 

스프링은 @Aspect 애노테이션으로포인트컷과 어드바이스로 구성되어 있는 어드바이저 생성 기능을 지원한다.

 

참고: @Aspect 는 관점 지향 프로그래밍(AOP)을 가능하게 하는 AspectJ 프로젝트에서 제공하는 애노테이션이다. 스프링은 이것을 차용해서 프록시를 통한 AOP를 가능하게 한다. 지금은 프록시에 자체에 초점을 맞춰서 스프링이 편리하게 프록시를 만들어준다고 생각하자.

LogTraceAspect 클래스

@Slf4j
@Aspect
public class LogTraceAspect {

    private final LogTrace logTrace;

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

    @Around("execution(* hello.proxy.app..*(..)) && !execution(* hello.proxy.app..noLog(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable{
        TraceStatus status = null;

//        log.info("target={}", joinPoint.getTarget());
//        log.info("getArgs={}", joinPoint.getArgs());
//        log.info("getSignature={}", joinPoint.getSignature());
        
        try{
            String message = joinPoint.getSignature().toShortString();
            status = logTrace.begin(message);

            //target 호출
            Object result = joinPoint.proceed();
            logTrace.end(status);
            return result;
        } catch (Exception e){
            logTrace.exception(status, e);
            throw e;
        }
    }
}

 

  • @Aspect : 애노테이션 기반 프록시를 적용할 때 필요하다.
  • @Around("execution(* hello.proxy.app..*(..)) && !execution(* hello.proxy.app..noLog(..))")
    • @Around 의 값에 포인트컷 표현식을 넣는다. 표현식은 AspectJ 표현식을 사용한다.
    • @Around 의 메서드는 어드바이스( Advice )가 된다.
  • ProceedingJoinPoint joinPoint : 어드바이스에서 살펴본 MethodInvocation invocation 과 유사한 기능이다. 내부에 실제 호출 대상(target), 전달 인자(args), 그리고 어떤 객체와 어떤 메서드가 호출되었는지(signature) 정보가 포함되어 있다.
  • joinPoint.proceed() : 실제 호출 대상( target )을 호출한다.

 

AopConfig ( 수동 빈 등록 )

@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AopConfig {

    @Bean
    public LogTraceAspect logTraceAspect(LogTrace logTrace){
        return new LogTraceAspect(logTrace);
    }
}
  • @Import({AppV1Config.class, AppV2Config.class}) : V1, V2 애플리케이션은 수동으로 스프링 빈으로 등록해야 동작한다.
  • @Bean logTraceAspect() : @Aspect 가 있어도 스프링 빈으로 등록을 해줘야 한다. 물론 LogTraceAspect 에 @Component 애노테이션을 붙여서 컴포넌트 스캔을 사용해서 스프링 빈으로 등록해도 된다.

실행 로그

[6facd576] OrderControllerV1Impl.request(..)
[6facd576] |-->OrderServiceV1Impl.orderItem(..)
[6facd576] |   |-->OrderRepositoryV1Impl.save(..)
[6facd576] |   |<--OrderRepositoryV1Impl.save(..) time=1002ms
[6facd576] |<--OrderServiceV1Impl.orderItem(..) time=1003ms
[6facd576] OrderControllerV1Impl.request(..) time=1005ms

joinPoint.getSignature().toShortString() 로 받은 메세지가 지금까지와는 약간 다르다.

@Aspect 프록시 - 설명

자동 프록시 생성기의 기능

 AnnotationAwareAspectJAutoProxyCreator

  1. @Aspect 를 보고 어드바이저( Advisor )로 변환해서 저장한다.
  2. 어드바이저를 기반으로 프록시를 생성한다.

 

@Aspect 를 어드바이저로 변환해서 저장하는 과정

1. 실행: 스프링 애플리케이션 로딩 시점에 자동 프록시 생성기를 호출한다.

2. 모든 @Aspect 빈 조회: 자동 프록시 생성기는 스프링 컨테이너에서 @Aspect 애노테이션이 붙은 스프링 빈을 모두 조회한다.

3. 어드바이저 생성: @Aspect 어드바이저 빌더를 통해 @Aspect 애노테이션 정보를 기반으로 어드바이저를 생성한다.

  @Around 값 => PointCut, @Around 메서드 => Advice 

4. @Aspect 기반 어드바이저 저장: 생성한 어드바이저를 @Aspect 어드바이저 빌더 내부에 저장한다.

 

참고: @Aspect 어드바이저 빌더

BeanFactoryAspectJAdvisorsBuilder 클래스이다. @Aspect 의 정보를 기반으로 포인트컷, 어드바이스, 어드바이저를 생성하고 보관하는 것을 담당한다. @Aspect 의 정보를 기반으로 어드바이저를 만들고, @Aspect 어드바이저 빌더 내부 저장소에 캐시한다. 캐시에 어드바이저가 이미 만들어져 있는 경우 캐시에 저장된 어드바이저를 반환한다.

 

어드바이저를 기반으로 프록시 생성하는 과정

기존에 스프링 컨테이너에서 어드바이저를 조회하던 그림에서 @Aspect 어드바이저 빌더가 추가됨 (-> 3-2)

 

1. 생성: 스프링 빈 대상이 되는 객체를 생성한다. ( @Bean , 컴포넌트 스캔 모두 포함. Advisor 객체도 스프링 빈) 

2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.

3-1. Advisor 빈 조회: 스프링 컨테이너에서 Advisor 빈을 모두 조회한다.

3-2. @Aspect Advisor 조회: @Aspect 어드바이저 빌더 내부에 저장된 Advisor 를 모두 조회한다.

4. 프록시 적용 대상 체크: 앞서 3-1, 3-2 에서 조회한 Advisor 에 포함되어 있는 포인트컷을 사용해서 해당 객체가 프록시를 적용할 대상인지 아닌지 판단한다. 이때 객체의 클래스 정보는 물론이고, 해당 객체의 모든 메서드를 포인트컷에 하나하나 모두 매칭해본다. 그래서 조건이 하나라도 만족하면 프록시 적용 대상이 된다.

5. 프록시 생성: 프록시 적용 대상이면 프록시를 생성하고 프록시를 반환한다. 그래서 프록시를 스프링 빈으로 등록한다. 만약 프록시 적용 대상이 아니라면 원본 객체를 반환해서 원본 객체를 스프링 빈으로 등록한다.

6. 빈 등록: 반환된 객체는 스프링 빈으로 등록된다.

정리

@Aspect 를 사용해서 애노테이션 기반 프록시를 매우 편리하게 적용해보았다. 실무에서 프록시를 적용할 때는 대부분이 이 방식을 사용한다.

 

지금까지 우리가 진행한 애플리케이션 전반에 로그를 남기는 기능은 특정 기능 하나에 관심이 있는 기능이 아니다. 애플리케이션의 여러 기능들 사이에 걸쳐서 들어가는 관심사이다.

이것을 횡단 관심사(cross-cutting concerns)라고 한다.

 

 

지금까지 프록시를 사용해서 이러한 횡단 관심사를 어떻게 해결하는지 점진적으로 매우 깊이있게 학습하고 기반을 다져두었다.

이제 이 기반을 바탕으로 이러한 횡단 관심사를 전문으로 해결하는 스프링 AOP에 대해 본격적으로 알아보자