Spring
[스프링] 빈 후처리기란?
문승주
2024. 5. 11. 23:30
반응형
본 내용은 인프런의 김영한 님의 강의 "스프링 핵심 원리 - 고급편" 내용을 바탕으로 정리한 내용입니다.
빈 후처리기란?
- 스프링 프레임워크에서 빈으로 등록할 목적으로 생성된 객체를 빈 저장소에 등록하기 전에 조작하는 기능이다.
- 이를 통해 생성된 객체를 조작하거나 완전히 다른 객체로 변경하는 것도 가능하다.
- 빈 후처리기를 사용하면 컴포넌트 스캔을 사용하는 빈까지 모두 프록시를 적용할 수 있다.
빈 후처리기를 사용한 빈 등록 과정
- 생성: 스프링 빈 대상이 되는 객체를 생성한다.
- 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
- 후 처리 작업: 빈 후처리기는 전달된 스프링 빈 객체를 조작하거나 다른 객체로 바뀌치기 할 수 있다.
- 등록: 빈 후처리기는 빈을 반환한다. 전달 된 빈을 그대로 반환하면 해당 빈이 등록되고, 바꿔치기 하면 다른 객체가 빈 저장소에 등록된다.
BeanPostProcessor 인터페이스
- 스프링에서 빈 후처리기 사용시 구현하는 인터페이스
- postProcessBeforeInitialization : 객체 생성 이후에 @PostConstruct 같은 초기화가 발생하기 전에 호출되는 포스트 프로세서
- postProcessAfterInitialization : 객체 생성 이후에 @PostConstruct 같은 초기화가 발생한 다음 에 호출되는 포스트 프로세서
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException
}
빈 후처리기 예제
BeanPostProcessorTest.class
- A 객체 bean 등록 시 B 객체가 등록된다.
package hello.proxy.postprocessor;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
public class BeanPostProcessorTest {
@Test
void basicConfig() {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanPostProcessorConfig.class);
//beanA 이름으로 B 객체가 빈으로 등록된다.
B b = applicationContext.getBean("beanA", B.class);
b.helloB();
//A는 빈으로 등록되지 않는다.
Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean(A.class));
}
@Slf4j
@Configuration static class BeanPostProcessorConfig {
@Bean(name = "beanA")
public A a() {
return new A();
}
@Bean
public AToBPostProcessor helloPostProcessor() {
return new AToBPostProcessor();
}
}
@Slf4j
static class A {
public void helloA() {
log.info("hello A");
}
}
@Slf4j
static class B {
public void helloB() {
log.info("hello B");
}
}
@Slf4j
static class AToBPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.info("beanName={} bean={}", beanName, bean);
// A 인스턴스가 넘어오면 B를 반환한다.
if (bean instanceof A) {
return new B();
}
return bean;
}
}
}
테스트 결과
BeanPostProcessorTest$AToBPostProcessor - beanName=beanA bean=hello.proxy.postprocessor.BeanPostProcessorTest$A@68be8808
BeanPostProcessorTest$B - hello B
- 테스트 실행 시
postProcessAfterInitialization
메서드가 호출되며, 여기서 A 타입의 빈을 B 타입의 빈으로 변환한다.
빈 후처리기 적용 예시
PackageLogTracePostProcessor.class
- 특정 패키지에 속하는 빈에 대해서만 프록시를 생성하여 반환하도록 설정한다.
package hello.proxy.config.v4_postprocessor.postprocessor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
@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 {
log.info("param beanName={} bean={}", beanName, bean.getClass());
//프록시 적용 대상 여부 체크
//프록시 적용 대상이 아니면 원본을 그대로 진행
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;
}
BeanPostProcessorConfig.class
basePackage
로"hello.proxy.app"
을 지정하여 해당 패키지에 속하는 빈들에 대해서만 프록시를 생성하도록 설정한다.getAdvisor
메서드에서는NameMatchMethodPointcut
을 사용하여 특정 메서드 이름 패턴에 매칭되는 경우에만LogTraceAdvice
를 적용하는Advisor
를 생성한다.
package hello.proxy.config.v4_postprocessor;
import hello.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;
import hello.proxy.config.v4_postprocessor.postprocessor.PackageLogTracePostProcessor;
import hello.proxy.trace.logtrace.LogTrace;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.Advisor;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
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);
}
}
ProxyApplication.class
- 빈 후처리기를 사용하여 컴포넌트 스캔으로 등록된 빈도 모두 프록시가 적용된다.
scanBasePackages
속성을 통해"hello.proxy.app"
패키지와 그 하위 패키지를 스캔하여 빈을 등록합니다.@Import(BeanPostProcessorConfig.class)
를 사용하여BeanPostProcessorConfig
클래스를 임포트하고, 해당 클래스에서 정의한 빈들을 사용할 수 있게 한다.
package hello.proxy;
import hello.proxy.config.v4_postprocessor.BeanPostProcessorConfig;
import hello.proxy.trace.logtrace.LogTrace;
import hello.proxy.trace.logtrace.ThreadLocalLogTrace;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
@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();
}
}
AutoProxyCreator(자동 프록시 생성기)
- 스프링 부트 자동 설정으로 AnnotationAwareAspectJAutoProxyCreator 라는 빈 후처리기가 스프링 빈에 자동으로 등록된다
- 스프링 빈으로 등록된 Advisor 들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해준다.
- Advisor 안에 있는 Pointcut 을 통해 어떤 스프링 빈에 프록시를 적용해야 할지 알 수 있다.
자동 프록시 생성기의 작동 과정
- 생성: 스프링이 스프링 빈 대상이 되는 객체를 생성한다.
- 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
- 모든 Advisor 빈 조회*: 빈 후처리기는 스프링 컨테이너에서 모든 Advisor 를 조회한다.
- 프록시 적용 대상 체크: 앞서 조회한 Advisor 에 포함되어 있는 포인트컷을 사용해서 해당 객체가 프록시를적용할 대상인지 아닌지 판단한다. 여러개의 메서드 중에 하나만 포인트컷 조건에 만족해도 프록시 적용 대상이 된다.
- 프록시 생성: 프록시 적용 대상이면 프록시를 생성하고 반환해서 프록시를 스프링 빈으로 등록한다.
- 빈 등록: 반환된 객체는 스프링 빈으로 등록된다
포인트컷의 2가지 사용방법
1. 프록시 적용 여부 판단 - 생성 단계
- 자동 프록시 생성기는 포인트컷을 사용해서 해당 빈이 프록시를 생성할 필요가 있는지 없는지 체크한다. 만약 조건에 맞는 것이 하나라도 있으면 프록시를 생성한다.
- 2. 어드바이스 적용 여부 판단 - 사용 단계
- 프록시가 호출되었을 때 부가 기능인 어드바이스를 적용할지 말지 포인트컷을 보고 판단한다.
AspectJExpressionPointcut
- 스프링 프레임워크 AOP 에서 포인트컷을 정의하는 데 사용되는 클래스이다.
- AspectJ라는 AOP에 특화된 포인트컷 표현식을 적용할 수 있다.
자동 프록시 생성시 사용 예시
- hello.proxy.app 하위 패키지 이거나 noLog 메서드가 아닌 대상만 프록시를 적용한다. (실행할 application 파일은 생략)
package hello.proxy.config.v5_autoproxy;
import hello.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;
import hello.proxy.trace.logtrace.LogTrace;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AutoProxyConfig {
@Bean
public Advisor advisor(LogTrace logTrace) {
//pointcut
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
// hello.proxy.app 하위 패키지 이거나 noLog 메서드가 아닌 대상만 프록시 적용
pointcut.setExpression("execution(* hello.proxy.app..*(..)) && !execution(* hello.proxy.app..noLog(..))");
//advice
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
반응형