4 분 소요

 인프런 스프링 핵심 원리 - 고급편을 학습하고 정리한 내용 입니다.

빈 후처리기 - 소개

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

빈 후처리기 - BeanPostProcessor

스프링이 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장소에 등록하기 직전에 조작하고 싶다면 빈 후처리기를 사용하면 된다.

빈 포스트 프로세서(BeanPostProcessor)는 번역하면 빈 후처리기인데, 이름 그대로 빈을 생성한 후에 무언가를 처리하는 용도로 사용한다.

빈 후처리기 기능

빈 후처리기의 기능은 막강하다. 객체를 조작할 수도 있고, 완전히 다른 객체로 바꿔치기 하는 것도 가능하다.

빈 후처리기 과정

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

빈 후처리기 - 예제 코드 1

일반적인 스프링 빈 등록 과정

빈 후처리기를 학습하기 전에 먼저 일반적인 스프링 빈 등록 과정을 코드로 작성해보자.

간단하게 테스트 코드에서 스프링 컨테이너를 선언하고 빈을 등록 해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class BasicTest {  
  
    @Test  
    void basicConfig() {  
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BasicConfig.class);  
  
        // A는 빈으로 등록된다.  
        A a = applicationContext.getBean("beanA", A.class);  
        a.helloA();  
  
        // B는 빈으로 등록되지 않는다.  
        Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean(B.class));  
    }  
  
    @Slf4j  
    @Configuration    
    static class BasicConfig {  
        @Bean(name = "beanA")  
        public A a() {  
            return new A();  
        }  
    }  
  
    @Slf4j  
    static class A {  
        public void helloA() {  
            log.info("hello A");  
        }  
    }  
  
    @Slf4j  
    static class B {  
        public void helloB() {  
            log.info("hello B");  
        }  
    }  
}
1
new AnnotationConfigApplicationContext(BasicConfig.class)

스프링 컨테이너를 생성하면서 BasicConfig.class를 넘겨주었다. BasicConfig.class 설정 파일은 스프링 빈으로 등록된다.

BasicConfig.class

1
2
3
4
5
6
7
8
@Slf4j  
@Configuration  
static class BasicConfig {  
    @Bean(name = "beanA")  
    public A a() {  
        return new A();  
    }  
}

beanA라는 이름으로 A 객체를 스프링 빈으로 등록했다.

이제 조회를 해보자.

1
2
3
// A는 빈으로 등록된다.  
A a = applicationContext.getBean("beanA", A.class);  
a.helloA();

getBean 메서드를 사용해서 beanA라는 이름으로 A타입의 스프링 빈을 찾을 수 있다.

1
2
// B는 빈으로 등록되지 않는다.  
Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean(B.class));  

B는 빈으로 등록하지 않았기 때문에 org.junit.jupiter.api.Assertions.assertThrows()를 사용해서 B타입의 빈을 찾으면 예외가 터지는지 확인했다.

참고로 org.assertj.core.api.Assertions.assertThatThrownBy를 사용하면 다음과 같이 작성하면 된다.

1
2
assertThatThrownBy(() -> applicationContext.getBean("beanB", B.class))  
        .isInstanceOf(NoSuchBeanDefinitionException.class);

잘 통과 된다.

빈 후처리기 - 예제 코드 2

빈 후처리기 적용

이번에는 빈 후처리기를 통해서 A 객체를 B 객체로 바꿔치기 해보자.

BeanPostProcessor 인터페이스 - 스프링 제공

1
2
3
4
5
6
7
8
9
10
11
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같은 초기화가 발생한 다음에 호출되는 포스트 프로세서이다.

BeanPostProcessorTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class BeanPostProcessorTest {  
  
    @Test  
    void basicConfig() {  
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanPostProcessorConfig.class);  
  
        // A는 빈으로 등록된다.  
        A a = applicationContext.getBean("beanA", A.class);  
        a.helloA();  
  
        // B는 빈으로 등록되지 않는다.  
        Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean(B.class));  
    }  
  
    @Slf4j  
    @Configuration    
    static class BeanPostProcessorConfig {  
        @Bean(name = "beanA")  
        public A a() {  
            return new A();  
        }  
  
        @Bean  
        public AToBPostProcessor postProcessor() {  
            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);  
            if (bean instanceof A) {  
                return new B();  
            }  
            return bean;  
        }  
    }  
}

자 이전 코드와 똑같은 코드인데

1
2
3
4
5
6
7
8
9
10
11
@Slf4j  
static class AToBPostProcessor implements BeanPostProcessor {  
	@Override  
	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {  
		log.info("beanName: {}, bean: {}", beanName, bean);  
		if (bean instanceof A) {  
			return new B();  
		}  
		return bean;  
	}  
}

다음과 같이 BeanPostProcessor인터페이스를 구현한 후처리기를 만들었다.

이 빈 후처리기는 A 객체를 B 객체로 바꿔치기한다. 파라미터로 넘어오는 빈(bean)객체가 A의 인스턴스이면 새로운 B객체를 생성해서 반환한다. 여기서 A대신에 반환된 값인 B가 스프링 컨테이너에 등록된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j  
@Configuration    
static class BeanPostProcessorConfig {  
	@Bean(name = "beanA")  
	public A a() {  
		return new A();  
	}  

	@Bean  
	public AToBPostProcessor postProcessor() {  
		return new AToBPostProcessor();  
	}  
}

다음과 같이 Bean등록을 A 클래스와, 빈 후처리기를 등록했다.

이러고 테스트를 돌려보면..

beanA이름으로 B객체가 등록되어 있다고 오류가 나온다! 즉 바꿔치기 됐다는 것이다.

그럼 테스트 코드를 고쳐보자.

1
2
3
4
5
6
7
8
9
10
11
@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));  
}

자 이제 beanA이름으로 B.class를 찾는다.

그리고 A클래스로 등록된 빈을 찾으면 에러가 터지는지 확인한다.

잘 동작한다. 후처리기가 먼저 동작하고, B클래스의 helloB 메서드가 동작했다.

댓글남기기