[스프링 핵심 원리 고급편 (6)] : 빈 후처리기

Loopy·2023년 1월 9일
0

스프링

목록 보기
16/16
post-thumbnail

들어가기

프록시 팩토리에는 다음과 같은 두가지 문제가 있다.

  1. 프록시를 생성하기 위한 설정 파일이 너무 많고 생성 코드가 적용할 클래스별로 중복된다. 10개의 객체를 빈으로 등록하고자 한다면 10개의 생성 코드가 필요하기 때문이다.

  2. 직접 @Bean 을 통해 등록하는 것이 아닌 컴포넌트 스캔 방식의 클래스들은 프록시를 적용할 방법이 없다. 부가 기능이 있는 프록시를 실제 객체 대신 스프링 컨테이너에 빈으로 등록해야 했기 때문이다.

이러한 문제들을 해결해주는 것이 바로, 빈 후처리기이다.

☁️ 빈 후처리기

빈 후처리기란, 빈을 조작하고 변경할 수 있는 후킹 포인트이다.

즉 스프링 컨테이너에 빈을 등록하기 전에 빈 객체를 조작할 수도 있고, 완전히 다른 객체로 바꿔치기 할 수도 있는 것이다.

즉, 여기서 앞선 문제 해결의 힌트를 얻자면 컴포넌트 스캔이 되는 대상이 되는 빈들은 중간에 조작할 방법이 없지만 빈 후처리기를 사용하여 프록시로 바꿔치기하면 될 것이다.

일반적인 스프링 빈 등작

@Bean 이나 컴포넌트 스캔으로 스프링 빈을 등록하면, 스프링은 대상 객체를 생성하고 스프링 컨테이너 내부의 빈 저장소(applicationContext)에 등록한다.

다른 객체로 바꿔치는 빈 후처리기 과정

  1. 생성 : 스프링 빈 대상이 되는 객체를 생성한다. ( @Bean , 컴포넌트 스캔 모두 포함)
  2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
  3. 후 처리 작업 : 빈 후처리기는 전달된 스프링 빈 객체를 조작하거나 다른 객체로 바뀌치기 할 수 있다.
  4. 등록: 빈 후처리기는 빈을 반환한다. 전달 된 빈을 그대로 반환하면 해당 빈이 등록되고, 바꿔치기 하면 다른 객체가 빈 저장소에 등록된다.

인터페이스 구성

빈 후처리기를 사용하려면 BeanPostProcessor 인터페이스를 구현하고, 스프링 빈으로 등록하면 된다.

 public interface BeanPostProcessor {
      Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException
      Object postProcessAfterInitialization(Object bean, String beanName) throwsBeansException
  }
  • postProcessBeforeInitialization() : 객체 생성 이후, @PostConstructor와 같은 객체 초기화 과정이 일어나기 이전에 호출
  • postProcessAfterInitialization() : 객체 생성 이후, @PostConstructor와 같은 객체 초기화 과정이 발생한 다음에 호출

🔖 참고 : @PostConstructor의 비밀
스프링 빈 생성 이후 초기화는 @PostConstructor가 붙은 초기화 메서드를 한번 호출하면 되는 것, 즉 생성된 빈을 한번 조작하는 행위이다. 따라서 스프링은 이러한 빈을 조작하는 행위를 하는 빈 후처리기인 CommonAnnotationBeanPostProcessor 을 자동으로 등록한다. (스프링도 스프링 내부의 기능을 확장하기 위해 빈 후처리기 사용한다.)

☁️ 빈 후처리기 장점

1. 프록시 생성 부분을 하나로 집중할 수 있다.

이전에는 빈이 생성되는 부분마다 프록시를 만들었는데, 어짜피 생성되는 빈들은 모두 스프링 컨테이너로 등록되고 넘어오기 때문에 중간에서 잡아서 프록시 객체로 변환하는 로직 하나만 만들어 놓아도 되는 것이다.

2. 컴포넌트 스캔처럼 스프링 컨테이너에 자동으로 등록되는 경우도 중간에서 가로채서 원본 대신에 프록시를 빈으로 등록할 수 있다.

이 덕분에, 수많은 스프링 빈이 추가되어도 프록시와 관련된 코드는 변하지 않는다.

☁️ 로그 추적기에 적용

빈 후처리기 로직

@Slf4j
public class PackageLogTracePostProcessor implements BeanPostProcessor {

    private final String basePackage;
    private final Advisor advisor;

    public PackageLogTracePostProcessor(String basePackage, Advisor advisor) {
        this.basePackage = basePackage;
        this.advisor = advisor;
    }

    // 빈 객체가 완전히 초기화 하고 난 후에 조작
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    
        /** 
           프록시 적용 대상 여부 체크 로직
        **/
        String packageName = bean.getClass().getPackageName();
        if (!packageName.startsWith(basePackage)) {
            return bean;
        }

        // 프록시 대상 여부이면 프록시 반환
        ProxyFactory proxyFactory = new ProxyFactory(bean);
        proxyFactory.addAdvisor(advisor);

        return proxyFactory.getProxy();
    }
}

스프링 뿐만 아니라 스프링 부트에서도 쓰는 모든 빈들에 대해 프록시를 적용하면 안되니, 패키지명 필터링을 통해 프록시 적용 대상 여부를 체크하는 로직을 추가해주었다.

프록시 대상 여부가 아닌 경우, 즉 컨트롤러/서비스/레포지토리가 들어가 있는 app 패키지 하위 클래스가 아니라면 원본을 반환시키고, 하위 클래스라면 프록시로 바꿔치기 해서 반환해주는 것이다.

참고로 여기서는 패키지로 설정했지만, 스프링 AOP에서는 이미 클래스/메서드 단위의 필터링 로직을 가지고 있는 PointCut 을 활용해서 프록시 적용 대상 여부를 체크할 수 있다.

🔖 PointCut의 용도
1. 프록시 적용 대상 여부를 체크해서 꼭 필요한 곳에만 프록시를 적용(빈 후처리기 - 자동 프록시 생성)
2. 프록시의 어떤 메서드가 호출 되었을 때 어드바이스를 적용할 지 판단 (프록시 내부)

빈 후처리기 설정 파일

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

    @Bean
    public PackageLogTracePostProcessor logTracePostProcessor(LogTrace logTrace) {
        return new PackageLogTracePostProcessor("hello.proxy.app", getAdvisor(logTrace));

    }
    private Advisor getAdvisor(LogTrace logTrace) {
        // pointcut : 필터링
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");

        // advice : 부가 기능 로직
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);
        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}

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

앞서서는 직접 빈 후처리기를 구현해서 빈으로 등록했지만, 스프링에서 제공하는 것을 사용하면 더욱 간단해진다.

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

위의 의존성을 추가해주면, 자동 프록시 생성기인 AnnotationAwareAspectJAutoProxyCreator 라는 빈 후처리기를 스프링 빈으로 등록해준다.

🔖 참고 사항
aspectjweaver 라는 aspectJ 관련 라이브러리를 등록하고, 스프링 부트가 AOP 관련 클래스를 자동으로 스프링 빈에 등록한다. 스프링 부트가 자동으로 처리하기 이전에는 @EnableAspectJAutoProxy 를 직접 사용해야 했다.

🔖 AnnotationAwareAspectJAutoProxyCreator

그렇다면 빈 후처리기에서의 프록시 적용 대상 여부 체크 로직을 어디서 구현해야 할까?

스프링의 자동 프록시 생성기는 프록시를 적용할 대상을 Advisor 를 통해 체크한다. Advisor 안에 들어있는 PointCut 만으로 어떤 스프링 빈에 프록시를 적용할지 알 수 있기 때문이다.

단, 앞서서 패키지 명으로 필터링 했던 것과 달리 프록시를 적용해야할 메서드가 하나라도 있는 클래스는 프록시를 생성하게 된다.

  1. 생성 : 스프링이 스프링 빈 대상이 되는 객체를 생성한다. ( @Bean , 컴포넌트 스캔 모두 포함)
  2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
  3. 모든 Advisor 빈 조회: 자동 프록시 생성기는 스프링 컨테이너에서 모든 Advisor 를 조회한다.
  4. 프록시 적용 대상 체크 : 앞서 조회한 Advisor 에 포함되어 있는 포인트컷을 사용해서 해당 객체가 프록시를 적용할 대상인지 아닌지 판단한다. 객체의 클래스 정보 뿐만 아니라 모든 메서드를 매칭해보면서, 조건이 하나라도 만족하면 프록시 적용 대상으로 판단한다.
  5. 프록시 생성 : 프록시 적용 대상이면 프록시를 생성하고 반환한다. 만약 프록시 적용 대상이 아니라면 원본 객체를 반환해서 원본 객체를 반환한다.
  6. 빈 등록 : 반환된 객체는 스프링 빈으로 등록된다.
@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);
        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}

비로써 우리는 이제 빈 후처리기를 구현 및 등록하지 않고, Advisor 만 빈으로 등록해주면 되기 때문에 매우 편리하게 프록시를 생성할 수 있게 되었다.

하지만 위의 방식으로 하면, 스프링 초기화 시에 빈을 등록할때 필요한 메서드 (ex) orderRepository()) 까지 필터링이 적용되어 프록시가 생성되고 어드바이스가 적용되는 문제가 발생한다.

// 아래와 같이 필요 없는 곳에 어드바이스가 적용되었다.
EnableWebMvcConfiguration.requestMappingHandlerAdapter()
EnableWebMvcConfiguration.requestMappingHandlerAdapter() time=63ms

🔖 AspectJExpressionPointcut

AspectJ라는 AOP에 특화된 포인트컷 표현식을 사용하여, 위의 문제를 해결할 수 있다. ApsectJ를 통해 프록시를 적용할 패키지 와 메서드명을 자유롭게 조합 가능하기 때문이다.

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

    @Bean
    public Advisor advisor2(LogTrace logTrace) {
        // pointcut : 필터링
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("execution(* hello.proxy.app..*(..)) && !execution(* hello.proxy.app..noLog(..))");    // 적용할 패키지 위치 지정

        // advice : 부가 기능 로직
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);
        return new DefaultPointcutAdvisor(pointcut, advice);
    }
 }
  • * : 모든 반환 타입
  • hello.proxy.app.. : 해당 패키지와 그 하위 패키지
  • *(..) : * 모든 메서드 이름
  • (..) : 파라미터는 상관 없음

📚 포인트컷의 두가지 사용법
1. 프록시 생성 여부 판단 : 생성 단계
자동 프록시 생성기는 해당 빈이 프록시를 생성해야할 필요가 있는지 포인트컷을 보고 판단한다. 모든 메서드를 체크하면서 하나라도 조건에 맞는다면 프록시를 생성한다.

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

☁️ @Aspect AOP(프록시)

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

즉, 직접 Advisor 를 구현해서 스프링 빈으로 등록하지 않고 어노테이션 한번으로 간단하게 해결 가능해지는 것이다.

Aspect

@Aspect  // 어노테이션 기반 프록시 적용
public class LogTraceAspect {

    @Around("execution(* hello.proxy.app..*(..))")   
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {   
        TraceStatus status = null;
        try {
            String message = joinPoint.getSignature().toShortString();
            status = logTrace.begin(message);

            // 실제 로직 호출
            Object result = joinPoint.proceed();

            logTrace.end(status);
            return result;
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }
}
  • @Aspect : 어노테이션 기반 프록시 적용시 필요
  • ProceedingJoinPoint : 내부에 실제 호출 대상, 전달 인자, 그리고 어떤 객체와 어떤 메서드가 호출되었는지 정보가 포함(어드바이스에서 살펴본 MethodInvocation 과 유사한 기능)
  • joinPoint.proceed() : 실제 호출 대상인 타겟을 호출

설정 파일

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

    @Bean
    public LogTraceAspect logTraceAspect(LogTrace logTrace) {
        return new LogTraceAspect(logTrace);     // 어드바이저 등록
    }
}

🔗 자동 프록시 생성기와 @Aspect

앞서서 자동 프록시 생성기가 스프링 컨테이너에서 Advisor 를 자동으로 가져와서 프록시 적용 대상 여부에 사용한다 했는데, 추가로 @Aspect가 붙은 클래스들을 Advisor로 변환해서 저장하는 역할까지 한다.

  1. @Aspect 를 보고 Advisor 로 변환해서 저장한다.

스프링 어플리케이션 로딩 시점에 스프링 컨테이너에서 @Aspect가 붙은 빈을 찾아내서 Advisor로 변환하고, @Aspect 어드바이저 빌더 내부 저장소에 캐싱해둔다.(저장)

  1. 어드바이저를 기반으로 프록시를 생성한다.

나머지 로직은 같고, Advisor 빈 뿐만 아니라 @Aspect Advisor 까지 모두 조회해서 프록시 적용 대상 체크 여부를 위한 정보에 활용하게 되는 부분이 다른 것을 볼 수 있다.

profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!

0개의 댓글