[Spring] 빈 후처리기

Donghoon Jeong·2024년 6월 3일
0

Spring

목록 보기
8/15
post-thumbnail

이전 포스팅에서 언급한 한계점들을 해결하기 위해서 스프링에서는 빈 후처리기라는 기능을 제공하고 있습니다. 이번 포스팅에서는 스프링이 제공하는 빈 후처리기 기능에 대해서 알아보겠습니다.

들어가기에 앞서 해당 블로그 내용의 이해를 돕기 위해 AOP에서 자주 등장하는 용어에 대해서 정리를 하고 알아보도록 하겠습니다.

조인 포인트, 어드바이스, 포인트 컷, 어드바이저, 타겟

  • Join Point (조인 포인트)

    추상적인 개념으로, 어드바이스가 적용될 수 있는 모든 위치를 나타냅니다. 메소드 실행 시점, 생성자 호출 시점, 필드 값 접근 시점 등이 있지만, 스프링 AOP는 프록시 방식을 사용하므로 조인 포인트는 항상 메소드 실행 지점입니다.

  • Advice (어드바이스)

    프록시가 호출하는 부가 기능입니다. 단순하게 프록시 로직이라 생각하면 쉽게 이해할 수 있습니다.

  • Pointcut (포인트 컷)

    조인 포인트 중에서 어드바이스가 적용될 위치를 선별하는 기능입니다. 스프링 AOP에서는 프록시 기반이기 때문에 조인 포인트가 메소드 실행 시점뿐이며, 포인트 컷도 주로 메소드 실행 시점에 적용됩니다.

  • Advisor (어드바이저)

    단순하게 하나의 포인트 컷과 하나의 어드바이스를 가지고 있는 것입니다. 쉽게 이야기해서 포인트 컷 1 + 어드바이스 1입니다.

  • Target (타겟)

    어드바이스가 적용되는 대상이 되는 객체로, 포인트 컷에 의해 결정됩니다. 주로 비즈니스 로직을 포함하는 객체가 해당됩니다.

이렇게 구분한 것은 역할과 책임을 명확하게 분리한 것입니다. 포인트컷은 대상 여부를 확인하는 필터 역할만 담당하고 어드바이스는 깔끔하게 부가 기능 로직만 담당합니다.

정리하면 부가 기능 로직을 적용해야 하는데, 포인트 컷으로 어디에 적용할지 선택하고, 어드바이스로 어떤 로직을 적용할지 선택하는 것이다. 그리고 어디에 어떤 로직을 모두 알고 있는 것이 어드바이저입니다.


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

일반적으로 @Bean 어노테이션이나 컴포넌트 스캔으로 스프링 빈을 등록하면, 스프링은 대상 객체를 생성하고 스프링 컨테이너 내부의 빈 저장소에 등록합니다. 이후에는 스프링 컨테이너를 통해 등록한 스프링 빈을 조회해서 사용하면 됩니다.

하지만 우리가 지금까지 알아본 프록시를 적용하기 위해서는 스프링 빈 저장소에 프록시 객체를 등록해야합니다. 스프링은 프록시를 생성하기 위한 빈 후처리기를 이미 만들어서 제공하고 있습니다.

스프링에서 제공하는 빈 후처리기를 사용하기 위해서는 아래와 같은 의존성을 추가해야합니다.

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

AnnotationAwareAspectJAutoProxyCreator

스프링 부트 자동 설정으로 AnnotationAwareAspectJAutoProxyCreator라는 빈 후처리기가 스프링 빈에 자동으로 등록됩니다. 이름 그대로 자동으로 프록시를 생성해주는 빈 후처리기입니다.

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

위에서 설명했듯이 어드바이저 안에는 포인트 컷어드바이스가 포함되어 있어 어드바이저만 알고 있으면 포인트 컷으로 어떤 스프링 빈에 프록시를 적용해야 할지 알 수 있습니다. 그리고 어드바이스로 부가 기능 적용이 동시에 가능합니다.

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

1. 생성: @SpringBootApplication이 실행되며 @Bean과 @Component가 적용된 모든 스프링 빈 대상이 되는 객체를 생성합니다.

2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 자동 프록시 생성기(빈 후처리기)에 전달합니다.

3. 모든 어드바이저 빈 조회: AnnotationAwareAspectJAutoProxyCreator는 스프링 컨테이너 내의 모든 어드바이저를 조회합니다.

4. 프록시 적용 대상 체크: 조회한 어드바이저에 포함된 포인트 컷을 사용해 해당 객체가 프록시 적용 대상인지 판단합니다. 객체의 클래스 정보와 모든 메서드를 포인트 컷에 매칭하여 조건이 하나라도 만족하면 프록시 적용 대상이 됩니다.

5. 프록시 생성: 프록시 적용 대상이면 프록시를 생성하여 반환하고 그렇지 않으면 원본 객체를 반환합니다.

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

포인트 컷의 역할

포인트 컷은 2가지 상황에서 사용됩니다

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

    자동 프록시 생성기는 포인트 컷을 사용해 해당 빈이 프록시를 생성할 필요가 있는지 체크합니다. 클래스와 메서드 조건을 모두 비교하여 조건에 맞는 것이 하나라도 있으면 프록시를 생성합니다.

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

    프록시가 호출되었을 때 부가 기능인 어드바이스를 적용할지 포인트 컷을 보고 판단합니다. 프록시가 호출되면 포인트 컷 조건에 만족하는 메서드에 대해 어드바이스를 호출하고, 그렇지 않은 메서드는 바로 호출합니다.


예제 코드

설명했던 내용을 예제 코드를 통해 이해를 돕겠습니다.

@Slf4j
public class Target {
    void run(){
        log.info("Target.run() 실행");
    }

    void save() {
        log.info("Target.save() 실행");
    }
}

스프링 빈의 등록 대상이 되는 Target 클래스입니다. 해당 클래스의 메서드 중 하나라고 포인트 컷의 조건과 일치할 경우, 적용 대상이 되기 때문에 프록시가 생성되어 스프링 빈으로 등록됩니다.

@Slf4j
@Aspect
public class MyAdvice implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        log.info("부가기능 실행");
        return invocation.proceed();
    }
}

직접 구현한 간단한 어드바이스입니다. 실제 요청 대상 메서드를 호출하기 전에 로그를 출력하여 부가기능의 역할을 합니다.

@Configuration
public class AdvisorConfig {

    @Bean
    public Target target(){
        return new Target();
    }

    @Bean
    public Advisor myAdvisor(){
        NameMatchMethodPointcut nameMatchMethodPointcut = new NameMatchMethodPointcut();
        nameMatchMethodPointcut.setMappedName("run");
        return new DefaultPointcutAdvisor(nameMatchMethodPointcut, new MyAdvice());
    }
}

Target 클래스와 어드바이저를 빈 등록하는 설정 파일입니다. DefaultPointcutAdvisor를 통해 어드바이저를 구현하였으며, 포인트 컷NameMatchMethodPointcut을 사용하여 run이라는 이름의 메서드를 가진 클래스들을 프록시 적용 대상으로 판별합니다.

다음과 같이 코드를 작성할 경우, Target 클래스의 run 메서드가 포인트 컷과 일치하기 때문에 Target 클래스의 프록시 객체가 스프링 컨테이너에 등록될 것을 예상할 수 있습니다.

또한 run 메서드를 실행할 때, 포인트 컷의 조건과 일치하기 때문에 Target 클래스의 run 메서드가 실행되기 전에 어드바이스의 부가기능이 먼저 실행될 것을 예상할 수 있고 save 메서드의 경우에는 포인트 컷에 해당되지 않기 때문에 해당 메서드를 실행할 경우 어드바이스는 실행되지 않고 Target 클래스의 메서드만 실행될 것을 예상할 수 있습니다.

이러한 방법으로 위에서 설명한 포인트 컷이 2가지 상황에서 사용된다는 것을 다시 한번 알 수 있습니다.

테스트 코드를 통해 예상과 실제 결과가 일치하는지 확인해 보겠습니다.

@Slf4j
@Import(AdvisorConfig.class)
@SpringBootTest
class AdvisorConfigTest {

    @Autowired
    Target target;

    @Test
    void autoProxyCreator1(){
        log.info("target = {}", target.getClass());
        target.run();
    }

    @Test
    void autoProxyCreator2(){
        log.info("target = {}", target.getClass());
        target.save();
    }
}

// 실행 결과
target = class hello.proxy.myTest.Target$$EnhancerBySpringCGLIB$$307ef371
부가기능 실행
Target.run() 실행
target = class hello.proxy.myTest.Target$$EnhancerBySpringCGLIB$$307ef371
Target.save() 실행

테스트 결과를 확인했을 때, Target 클래스를 로그로 찍어보았을 때, EnhancerBySpringCGLIB가 적용되어 프록시가 빈으로 등록된 것을 확인할 수 있습니다. 또한 run 메서드는 어드바이스의 부가기능이 적용되었지만, save 메서드는 부가기능이 적용되지 않은 것을 확인할 수 있습니다.


Reference

스프링 핵심 원리 - 고급편

profile
정신 🍒 !

0개의 댓글