빈 후처리기
by 키위먹고싶다빈 후처리기 - BeanPostProcessor
객체 조작도 가능하고 완전 다른 객체로 바꿔치기도 가능하다.
스프링 빈 대상 객체 생성(@Bean, 컴포넌트 스캔 모두) -> 객체를 빈 저장소에 등록하기 전에 빈 후 처리기에 전달 -> 빈 후 처리기는 전달된 스프링 빈 객체를 조작하거나 다른 객체로 바꿔치기 함 -> 빈 후처리기는 빈을 반환해서 빈 저장소에 객체 저장.
@Slf4j
public class PackageLogTracePostProcessor implements BeanPostProcessor {
private final String basicPackage;
private final Advisor advisor;
public PackageLogTracePostProcessor(String basicPackage, Advisor advisor) {
this.basicPackage = basicPackage;
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(basicPackage)) {
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;
}
}
이 클래스는 원본 객체를 프록시 변환하는 역할을 한다. 이때 프록시 팩토리는 advisor가 필요하므로 이 부분은 외부에서 주입받도록 했다.
@Slf4j
@Configuration
@Import({AppV1Config.class, AppV2Config.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);
}
}
모든 스프링 빈들에 프록시를 적용하는것이 아니라 hello.proxy.app관련된 부분에만 적용한다. 다른 패키지객체들은 원본 객체를 그대로 반환한다. 프록시 적용대상은 스프링 컨태이너에 원본객체가 아니라 프록시 객체가 스프링 빈으로 등록된다. 여기에 프록시를 적용할 패키지 정보와 어드바이저를 넘긴다.
이제 프록시를 생성하는 코드가 설정 파일에 필요 없다. 순수한 빈 등록만 고려한다. 프록시를 생성하고 프록시를 스프링 빈으로 등록하는 것은 빈 후처리기가 모두 처리해준다.
프록시를 사용하다보면 문제점들이 있다.
프록시를 직접 스프링 빈으로 등록하기에는 너무 많은 프록시 관련 설정들이 있다. 스프링 빈이 100개 있다고 가정하자. 만약 여기에 프록시를 통해 부가기능을 적용하려면 100개의 프록시 설정 코드가 들어가야 한다.
컴포넌트 스캔을 사용하면 프록시 적용이 불가능 했는데 그 이유는 컴포넌트 스캔으로 이미 스프링 컨테이너에 실제 객체를 스프링 빈으로 등록을 다 해버린 상태이기 때문이다. 원본 객체를 스프링 빈에 등록하는것이 아니라 프록시를 원본 객체 대신 스프링 컨테이너에 빈으로 등록해야 하는데 컴포넌트 스캔은 원본 객체를 스프링 빈으로 자동으로 등록하기 때문에 프록시 적용이 불가능하다.
빈 후처리기를 사용하면 컴포넌트 스캔처럼 스프링이 직접 대상을 빈으로 등록하는 경우에도 중간에 빈 등록 과정을 가로채서 원본 대신에 프록시를 스프링 빈으로 등록할 수 있다. 그리고 프록시를 생성하는 부분을 하나로 집중 할 수 있다.
스프링은 프록시를 생성하기 위한 빈 후처리기를 제공한다.
프록시 적용 대상 여부는 간단히 패키지를 기준으로 설정할 수 잇지만 포인트 컷을 사용하는게 더 깔끔하다.
포인트컷은 이미 클래스와 메서드 단위의 필터 기능을 가지고 있기 때문에 프록시 적용 대상여부를 정밀하게 설정할 수 있다. 어드바이저는 포인트 컷을 가지고 있으므로 어드바이저를 통해 포인트컷을 확인 할 수 있다.
포인트컷은 두곳에 사용된다.
1. 프록시 적용 대상 여부를 체크해서 꼭 필요한곳에만 프록시를 적용한다. (빈 후처리기 - 자동 프록시 생성)
2. 프록시의 어떤 메서드가 호출 되었을 때 어드바이스를 적용할 지 판단한다. (프록시 내부)
스프링이 제공하는 빈 후처리기
build.gradle에 추가
implementation 'org.springframework.boot:spring-boot-starter-aop' //추가
이 라이브러리를 추가하면 aspectJ관련 라이브러리를 등록하고 스프링 부트가 aop관련 클래스를 자동으로 스프링 빈에 등록한다.
자동 프록시 생성기 - AutoProxyCreator (postprocessor)
- AnnotationAwareAsepctJAutoProxyCreator라는 빈 후처리기가 스프링 빈에 자동으로 등록된다.
- 이름 그대로 자동으로 프록시를 생성해주는 빈 후처리기이다.
- 이 빈 후처리기는 스프링 빈으로 등록된 Advisor들을 자동으로 찾아서 프록시기 필요한 곳에 자동으로 프록시를 적용해준다.
- Advisor안에는 포인트컷과 advice가 이미 포함되어 있다. 따라서 Advisor만 알고 있으면 그 안에 있는 포인트컷으로 어떤 스프링 빈에 프록시를 적용해야 할 지 알 수 있다. 그리고 부가 기능을 advice로 적용한다.
@Configuration
@Import({AppV1Config.class, AppV2Config.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);
}
}
포인트 컷은 2가지에 사용된다.
1. 프록시 적용 여부 판단 - 생성단계
- 자동 프록시 생성기는 포인트컷을 사용해서 해당 빈이 프록시를 생성할 필요가 있는지 없는지 체크한다
- 클래스 + 메서드 조건을 모두 비교하고 모든 메서드를 채크할때 포인트컷 조건에 하나하나 매칭한다. 만약 조건에 맞는 것이 하나라도 있으면 프록시를 생성한다. (ex > orderControllerV1은 request(), noLog()가 있는데 request()가 조건에 만족으하므로 프록시를 생성한다.)
- 만약 조건에 맞는것이 하나도 없으면 프록시를 생성하지 않는다. (동적 프록시나 CGLIB가 아니라 원본 객체 그대로 스프링 빈에 등록된다.)
2. 어드바이스 적용 어부 판단 - 사용단계
- 프록시가 호출되었을때 부가 기능인 어드바이스를 적용하지 말지 포인트컷을 보고 판단한다.
- orderControllerV1은 이미 프록시가 걸려 있다
- request()는 현재 포인트컷 조건에 만족하므로 프록시는 어드바이스를 먼저 호출하고, target을 호출한다.
- noLog()는 현재 포인트 컷 조건에 만족하지 않으므로 어드바이스를 호출하지 않고 바로 target만 호출한다.
결론 : 프록시를 모든 곳에 생성하는 것이 아니라 자동 프록시 생성기가 모든 스프링 빈에 프록시를 적용하는 것이 아니라 포인트컷으로 한번 필터링해서 어드바이스가 사용될 가능성이 있는 곳에만 프록시를 생성한다.
애플리케이션 서버를 실행하면 스프링이 초기화 되면서 기대하지 않은 로그들이 올라오는데 그 이유는 사용한 포인트컷이 단순히 메서드 이름에 request*, order*, save*만 포함되어 있으면 매칭 된다고 판단하기 때문이다.
스프링 내부에서 사용하는 빈에도 메서드 이름에 request라는 단어만 들어가 이씅면 프록시가 만들어지고 어드바이스도 적용되는 것이다. 결론적으로 패키지에 머스드 이름까지 함께 지정할 수 있는 매우 정밀한 포인트컷이 필요하다.
AspectJExpressionPointCut
// @Bean
public Advisor advisor2(LogTrace logTrace) {
//pointcut
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* hello.proxy.app..*(..))");
//advice
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
@Bean
public Advisor advisor3(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 패키지와 하위 패키지의 모든 메서드는 포인트컷의 매칭하되, noLog() 메서드는 제외하라는 뜻이다.
하나의 프록시, 여러 Advisor 적용
어떤 스프링 빈이 advisor1, advisor2가 제공하는 포인트 컷 조건을 모두 만족해도 프록시 자동 생성기는 프록시를 하나만 생성하고 내부에 여러 advisor를 포함시킨다. 자동 프록시 생성기 덕분에 Advisor만 스프링 빈으로 등록하면 된다.
@Aspect AOP
스프링 애플리케이션에 프록시를 적용하려면 포인트컷과 어드바이스로 구성된 Advisor를 만들어서 스프링 빈으로 등록하면 된다. 그러면 나머지는 자동 프록시 생성기가 자동으로 처리한다. 자동 프록시 생성기는 스프링 빈으로 등록된 어드바이저들을 찾고, 스프링 빈들에 자동으로 프록시를 적용해준다.(포인트컷이 매칭되는 경우)
스프링은 @Asepct애노테이션으로 편리하게 포인트컷과 어드바이스로 구성된 어드바이저 생성기능을 지원한다.
@Slf4j
@Aspect
public class LogTraceAspect {
private final LogTrace logTrace;
public LogTraceAspect(LogTrace logTrace) {
this.logTrace = logTrace;
}
@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); //OrderController.request()
//로직 호출
Object result = joinPoint.proceed();
logTrace.end(status);
return result;
}catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
@Aspect애노테이션을 사용해서 스프링이 편리하게 프록시를 만들어준다고 생각하자.
@Aspect : 애노테이션 기반 프록시 적용할때 필요하다
@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 애노테이션을 붙여서 컴포넌트 스캔을 사용해서 스프링 빈으로 등록해도 된다.
@Import(AopConfig.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();
}
}
AnnotatinoAwareAsepctAutoProxyCreator는 Advisor를 자동으로 찾아와서 필요한 곳에 프록시를 생성하고 적용해준다고 했다. 자동 프록시 생성기는 한가지 일을 더 하는데 @Aspect를 찾아서 이것을 Advisor로 만들어준다.
@Aspect를 Advisor로 변환해서 저장하는 기능을 한다.
1. @Aspect를 보고 어드바이저 (Advisor)로 변환해서 저장한다.
2. 어드바이저를 기반으로 프록시를 생성한다.
'spring' 카테고리의 다른 글
인증과 인가, spring security[Session], jwt[Token] (0) | 2022.04.15 |
---|---|
AOP(Aspect-Oriented-Programming) (0) | 2022.03.13 |
파일 업로드 (0) | 2022.02.19 |
스프링 타입 컨버터 (0) | 2022.02.16 |
API 예외 처리 (0) | 2022.02.12 |
블로그의 정보
kiwi
키위먹고싶다