- 스프링의 빈 생애주기 관리
스프링은 DI 컨테이너로서 기능하면서, 빈의 생명주기를 관리한다. 빈의 스코프에 따라서 객체를 생성하고, 의존성의 주입하여 사용할 수 있게 해준다. 또 때가 되면, 해당 객체를 제거한다. 이렇게 객체의 생성과 초기화, 제거를 아우르는 흐름을 생명 주기라고 하고, 스프링에서는 개발자가 특정 시점에 동작하는 코드를 작성할 수 있도록 한다. 이 글에서는 BeanPostProcessor, @PostConstruct, @PreDestroy를 다루겠다.
- 빈의 초기화 시점 : BeanPostProcessor
BeanPostProcessor를 상속받아서 메소드를 구현하면 초기화 전후에 실행되는 코드를 작성할 수 있다. 여기서 말하는 초기화는 스프링에 의해 의존성 주입이 완료된 이후에 개발자에 이루어지는 초기화(@PostConstruct, InitializingBean)를 말한다.
@Component
@Scope(scopeName = "singleton")
public class Component1 {
private String str;
public Component1() {
System.out.println("Component1 Constructor");
System.out.println(toString());
}
public String getStr() {
return str;
}
public void setStr(String str) {
this.str = str;
}
@Override
public String toString() {
return "Component1 [str=" + str + "]";
}
}
@Component
@Scope(scopeName = "prototype")
public class Component2 {
@Autowired
private Component1 comp1;
public Component2() {
System.out.println("Component2 Constructor");
System.out.println(toString());
}
public Component1 getComp1() {
return comp1;
}
public void setStr(Component1 comp1) {
this.comp1 = comp1;
}
@Override
public String toString() {
return "Component2 [comp1=" + comp1 + "]";
}
}
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof Component1 || bean instanceof Component2) {
System.out.println("[Before Initialization ] - " + bean.getClass().getName());
System.out.println(bean);
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof Component1 || bean instanceof Component2) {
System.out.println("[After Initialization] - " + bean.getClass().getName());
System.out.println(bean);
}
return bean;
}
}
...
ConfigurableApplicationContext context =
new AnnotationConfigApplicationContext(ApplicationContextConfig.class);
Component2 comp2 = context.getBean("component2", Component2.class);
...
Componene1과 Component2를 빈으로 등록하고 생성자가 호출되는 시점에, toString()이 출력되도록 하였다. 또 BeanPostProcessor를 상속받은 MyBeanPostProcessor를 만들어서, 빈이 생성된 이후 & 초기화 전후로 내용을 출력하도록 하였다. 그리고 실제 실행 결과는 다음과 같이 나왔다.
Component1 Constructor
Component1 [str=null]
[Before Initialization ] - com.study.spring3.beans.Component1
Component1 [str=null]
[After Initialization] - com.study.spring3.beans.Component1
Component1 [str=null]
Component2 Constructor
Component2 [comp1=null]
[Before Initialization ] - com.study.spring3.beans.Component2
Component2 [comp1=Component1 [str=null]]
[After Initialization] - com.study.spring3.beans.Component2
Component2 [comp1=Component1 [str=null]]
Component1은 singleton Scope이므로 Context에 빈으로 등록되면서 Component1의 생성자가 호출되었다. 그리고 MyBeanPostProcessor에 정의된 초기화 전후 호출되는 메소드가 실행되었다. 이후 Componenet2는 prototype Scope이므로 getBean으로 불러지면서 생성자가 호출된다. 그리고 MyBeanPostProcessor에 정의된 초기화 전후 호출되는 메소드가 실행되었다. 여기서 눈여겨 볼 것은 Component2의 생성자 호출 시점에는 comp1 속성이 null이지만, 이후 초기화 전후로 호출되는 메소드에는 comp1에 객체가 할당되었다는 점이다. 즉, 생성자 호출과, 초기화 전후 메소드 호출 사이에 스프링이 의존성을 주입한 것이다.
- 빈의 생성과 제거 시점 : @PostConstruct, @PreDestroy
@PostConstruct, @PreDestroy 어노테이션을 사용하면 객체의 생성과 삭제 시점에 동작하는 코드를 작성할 수 있다. 자세히 말하면, @PostConstruct는 객체의 생성자가 호출되고 의존성 주입이 일어나고 난 이후이다. @PreDestroy는 정의된 스코프에 따라, 컨테이너가 객체를 제거하기 전에 호출된다.
@Component
@Scope(scopeName = "singleton")
public class Component1 {
private String str;
...
@PostConstruct
void postConstructor() {
System.out.println("Component1 postConstructor");
}
@PreDestroy
void preDestroy() {
System.out.println("Component1 preDestroy");
}
}
@Component
@Scope(scopeName = "prototype")
public class Component2 {
@Autowired
private Component1 comp1;
...
@PostConstruct
void postConstructor() {
System.out.println("Component2 postConstructor");
}
@PreDestroy
void preDestroy() {
System.out.println("Component2 preDestroy");
}
}
...
ConfigurableApplicationContext context =
new AnnotationConfigApplicationContext(ApplicationContextConfig.class);
Component2 comp2 = context.getBean("component2", Component2.class);
context.close();
...
위에서 사용한 컴포넌트에 @PostConstructor, @PreDestroy를 적용한 메소드를 추가하였다. c 그리고 실제 실행 결과는 다음과 같이 나왔다.
Component1 Constructor
Component1 [str=null]
[Before Initialization ] - com.study.spring3.beans.Component1
Component1 [str=null]
Component1 postConstructor
[After Initialization] - com.study.spring3.beans.Component1
Component1 [str=null]
Component2 Constructor
Component2 [comp1=null]
[Before Initialization ] - com.study.spring3.beans.Component2
Component2 [comp1=Component1 [str=null]]
Component2 postConstructor
[After Initialization] - com.study.spring3.beans.Component2
Component2 [comp1=Component1 [str=null]]
Component2 Constructor
Component2 [comp1=null]
[Before Initialization ] - com.study.spring3.beans.Component2
Component2 [comp1=Component1 [str=null]]
Component2 postConstructor
[After Initialization] - com.study.spring3.beans.Component2
Component2 [comp1=Component1 [str=null]]
Component1 preDestroy
천천히 흐름을 보면, 컴포넌트의 생성자가 호출되고, 컨테이너에 의해 의존성 주입이 일어나고 beforeInitialization 메소드가 호출된다. 그 후 @PostConstruct로 정의된 메소드가 호출되고 afterInitialization 메소드가 호출된다. 그리고 마지막에 preDestroy 메소드가 호출된다. 하지만 한가지 이상한 점은 Component2의 preDestroy가 보이지 않는다는 것이다. 스프링 컨테이너는 자신이 관리하는 범위에 한해서, @preDestroy를 호출 할 수 있다. 하지만 prototype scope의 경우, 생성 이후 객체의 사용은 자바 코드에 따라 달라지므로 컨테이너가 알 수 있는 영역이 아니다.
- 어디에 사용할까?
그렇다면 위와같은 생애주기 관련 기능들은 언제 사용하면 될까? 대표적으로 속성 값의 유효성 검사를 예로 들 수 있다. BeanPostProcessor를 사용하여 초기화 이후 객체의 속성값을 검사하여 유효한 값이 아니면 에러를 발생 시키게 할 수 있다. 또 생성과 제거 시점에 관한 기능은 자원의 할당과 반환에 사용될 수 있을 것이다.
- 참고 : Spring Bean Lifecycle