FireDrago

[Spring] 빈 후처리기 , @Aspect 본문

카테고리 없음

[Spring] 빈 후처리기 , @Aspect

화이용 2024. 5. 16. 13:30

빈 후처리기

빈 후처리기를(BeanPostProcessor) 사용하면, 빈 저장소에 등록할 목적으로 생성한 객체를 등록전에 조작할 수 있다.

객체를 조작할 수도 있고, 완전히 다른 객체로 바꿔치기 하는 것도 가능하다.

빈 후처리기를 사용하면 개발자가 등록하는 모든 빈을 중간에 조작할 수 있다. 이 말은 빈 객체를 프록시로 교체할수 있다.

 

public interface BeanPostProcessor {

    /**
    * 객체 생성 이후에 초기화 발생 전에 호출된다.
    */
    @Nullable
    default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
       return bean;
    }
	
    /**
    * 객체 생성 이후에 초기화 발생 후에 호출된다.
    */
    @Nullable
    default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
       return bean;
    }

}

빈 후처리기를 사용하려면 BeanPostProcessor 인터페이스를 구현하고, 스프링 빈으로 등록하면 된다.
postProcessBeforeInitialization : 객체 생성 이후에 @PostConstruct 같은 초기화가 발생하기
전에 호출되는 포스트 프로세서이다.
postProcessAfterInitialization : 객체 생성 이후에 @PostConstruct 같은 초기화가 발생한 다음
에 호출되는 포스트 프로세서이다

 

빈 후처리기 - 프록시 등록

@Slf4j
public class PackageLogTracProxyPostProcessor implements BeanPostProcessor {

    private final String basePackage;
    private final Advisor advisor;

    public PackageLogTracProxyPostProcessor(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);
        Object proxy = proxyFactory.getProxy();
        log.info("create proxy: target={} proxy={}", bean.getClass(), proxy.getClass());
        return proxy;
    }
}

PackageLogTraceProxyPostProcessor 는 원본 객체를 프록시 객체로 변환하는 역할을 한다. 

이때 프록시 팩토리를 사용하는데, 프록시 팩토리는 advisor 가 필요하기 때문에 이 부분은 외부에서 주입 받도록 했다.

 

패키지 네임을 활용하여, 특정 패키지 경로의 빈에만 프록시를 적용하도록 했다.

다른 빈들은 원본 빈을 그대로 반환한다. 해당 빈은 프록시를 생성하여 프록시가 빈으로 등록될 수 있도록 했다.

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

    @Bean
    public PackageLogTracProxyPostProcessor logTracProxyPostProcessor(LogTrace logTrace) {
        return new PackageLogTracProxyPostProcessor("hello.proxy.app", getAdvisor(logTrace));
    }

    private Advisor getAdvisor(LogTrace logTrace) {
        // 포인트 컷 생성
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");
        // 어드바이스 생성
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);
        // 어드바이저 = 포인트컷 + 어드바이스
        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}

@Bean logTraceProxyPostProcessor() : 특정 패키지를 기준으로 프록시를 생성하는

빈 후처리기를 스프링 빈으로 등록한다. 빈 후처리기는 스프링 빈으로만 등록하면 자동으로 동작한다. 

여기에 프록시를 적용할 패키지 정보( hello.proxy.app )와 어드바이저( getAdvisor(logTrace) )를 넘겨준다

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

@Import(BeanPostProcessorConfig.class) : 설정파일을 등록한다.

 

빈 후처리기 프록시 등록 장점

1. 수동 등록 빈 뿐만아니라, 컴포넌트 스캔을 사용한 빈도 등록될때 자동으로 프록시가 등록된다.

2. 프록시 객체를 일일히 빈으로 만들어서 등록할 필요가 없다.

 

 

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

빈 후처리기를 직접 만들어 등록할때, 특정 패키지 경로를 기준으로 대상 빈을 체크했다.

그런데 어드바이저의 포인트컷은 이미 클래스, 메서드 단위의 필터기능을 가지고 있다.

이를 빈 후처리기에서도 사용할 수 있다.  즉 포인트컷은 두가지 사용기능이 있는것이다.

포인트컷의 두가지 기능

1. 프록시 적용 대상 여부를 체크해서 꼭 필요한 곳에만 프록시를 적용한다. (빈 후처리기 - 자동 프록시 생성)

2. 프록시가 생성되고, 어떤 메서드가 호출 되었을때, 어드바이스를 적용할 지 판단한다. (프록시 내부)

 

스프링 부트는 AnnotationAwareAspectJAutoProxyCreator 라는 빈 후처리기를 자동으로 등록해준다. 

이 빈 후처리기는 스프링 빈으로 등록된 Advisor 들을 자동으로 찾아서 프록시가 필요한곳에 프록시를 적용해준다.

Advisor  에는 이미 pointcut이 포함되어 있기때문에 어떤 스프링 빈에 프록시를 적용해야 할지 알 수 있다.

또한 @Aspect 도 자동으로 인식해서 프록시를 만들고 AOP를 적용해준다. 

스프링부트 자동 설정을 사용하기 위해서 반드시 build.gradle에 추가해야 한다.

스프링 부트 빈 후처리기 ( AnnotationAwareAspectJAutoProxyCreator ) 기능

1. 어드바이저를 찾아 프록시 자동 적용

2. @Aspect 찾아 프록시 만들고, AOP 적

 

@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AutoProxyConfig {
@Bean
    public Advisor advisor3(LogTrace logTrace) {
        // 포인트컷 설정
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        // AspectJ 표현식을 통해 포인트컷을 설정한다.
        pointcut.setExpression("execution(* hello.proxy.app..*(..)) && " +
                "!execution(* hello.proxy.app..noLog(..))");
        // 어드바이스 설정
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);
        // 어드바이저 빈 설정
        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}

설정파일을 통해 어드바이저를 빈으로 등록했다. 이렇게만 해도 이제 프록시가 자동으로 생성되고 실행된다.

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

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

실행하면, 어드바이스의 부가기능이 no-log 메서드를 제외한 다른 메서드를 호출할때, 작동하는 것을 확인 할 수 있다.

 

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

프록시 자동 생성기는 프록시를 하나만 생성한다.

여러 어드바이저의 조건을 만족한다고 해도 프록시 하나에 여러 어드바이저를 포함시키는 방식을 사용한다.

 

 

@Aspect 애노테이션으로 어드바이저 만들기

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

그러면 나머지는 앞서 배운 자동 프록시 생성기가 모두 자동으로 처리해준다. 

 

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

@Slf4j
@Aspect
public class LogTraceAspect {

    private final LogTrace logTrace;

    public LogTraceAspect(LogTrace logTrace) {
        this.logTrace = logTrace;
    }
    
    // @Around 사용하여 포인트컷 설정
    @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 : 애노테이션 기반 프록시를 적용할 때 필요하다.

@Around("execution(* hello.proxy.app..*(..))") :@Around 의 값에 포인트컷 표현식을 넣는다. 

표현식은 AspectJ 표현식을 사용한다. @Around 의 메서드는 어드바이스( Advice )가 된다.

ProceedingJoinPoint joinPoint : 어드바이스에서 살펴본 MethodInvocation invocation 과 유사한 기능이다. 

내부에 실제 호출 대상, 전달 인자, 그리고 어떤 객체와 어떤 메서드가 호출되었는지 정보가 포함되어 있다.

joinPoint.proceed() : 실제 호출 대상( target )을 호출한다.

 

@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 애노테이션을 붙여서 컴포넌트 스캔을 사용해서 스프링 빈으로 등록해도된다.

 

자동 프록시 생성기의 두가지 역할

자동 프록시 생성기( AnnotationAwareAspectJAutoProxyCreator )는

1. Advisor 를 자동으로 찾아와서 필요한 곳에 프록시를 생성하고 적용해준다고 했다. 

2. @Aspect 를 찾아서 이것을 Advisor 로 만들어준다.

쉽게 이야기해서 지금까지 학습한 기능에 더해 @AspectAdvisor  로 변환해서 저장하는 기능도 한다.

그래서 이름 앞에 AnnotationAware (애노테이션을 인식하는)가 붙어 있는 것이다.

 

지금까지 프록시를 사용해서 이러한 횡단 관심사를 어떻게 해결하는지 점진적으로 매우 깊이있게 학습하고 기반을 다져
두었다. 이제 이 기반을 바탕으로 이러한 횡단 관심사를 전문으로 해결하는 스프링 AOP에 대해 본격적으로 알아보자