들어가며
- 이번 포스트는 스프링 빈의 생명 주기(Life Cycle)에 대해 알아보도록 한다.
스프링 빈의 생명 주기
- DB 커넥션이나 네트워크 소켓처럼 앱 시작 시점에 연결을 진행하고, 앱 종료 시점에 연결을 모두 종료하는 작업을 수행하기 위해서는 객체의 초기화 및 종료시에 작업을 할 수 있도록 해야한다.
- 다음과 같은 간단한 코드를 보자
public class NetworkClient{
private String url;
public NetworkClient(){
System.out.println("constructor called");
connect();
call("initializeing msg")
}
public void connect(){
System.out.println("connect : " + this.url);
}
public setUrl(String url){
this.url = url;
}
public void call(String msg){
System.out.println("url : " + this.url + "msg : " + msg);
}
public void disconnect(){
System.out.println("close : " + url);
}
}
@Configuration
public class LifeCycleConfig{
@Bean
public NetworkClient networkClient(){
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://somthing.dev");
return networkClient;
}
}
- 해당 코드들을 스프링 프레임워크 위에서 실행시키면 우리가 생각했던 http://something.dev가 출력되지 않고 null이 들어가 출력된다.
- 이는 이전에도 한번 언급하고 넘어간 적이 있었는데, 스프링 컨테이너에서 스프링 빈을 생성하는 시점과 컨테이너가 DI를 수행하는 시점이 별개로 나뉘어져 있다는 것을 알고 있어야 한다.
- NetworkClient의 생성자에서 url을 설정하는 코드가 있지 않고, Setter에 존재하고 있기 때문에 객체의 생성 시점에서 url이 초기화되지 않는 것이다. 객체가 생성된 뒤, setUrl을 통해서야 url 필드가 초기화된다.
스프링 빈 생명 주기와 초기화
- 스프링 빈은 다음과 같은 생명 주기를 가진다.
- 객체들이 먼저 생성된다.
- 이후 의존관계가 주입된다.
- 단 생성자 주입은 예외적인것을 유의하라. 생성자 주입은 객체의 생성과 동시에 의존관계 주입이 일어난다.
- 스프링 빈은 객체가 생성되고, 그 이후 의존관계가 주입된 뒤에야 사용할 수 있는 상태가 된다. 따라서 초기화 작업을 수행하려면 의존관계 주입 단계가 끝난 이후에 수행되어야 한다.
- 하지만 클라이언트가 해당 작업이 언제 끝나는지는 알 수 없다. 그렇다고 해당 단계가 끝났는지 아닌지를 계속해서 확인하는 리스너(Listener)를 만들기에는 너무 번거롭다.
- 다행이도 스프링 프레임워크는 의존관계 주입 이후 해당 스프링 빈이 사용 가능해지면 해당 스프링 빈에게 콜백 메서드를 통해 알릴 수 있다. 이를 이용하여 해당 스프링 빈 객체가 초기화 작업을 수행할 수 있다.
- 또한 스프링은 스프링 컨테이너가 종료되기 직전에도 콜백 메서드를 통해 소멸 콜백을 제공한다. 따라서 종료 시 수행되는 작업도 진행할 수 있다.
- 이를 모두 고려하면 스프링 빈은 세부적으로 다음과 같은 생명 주기를 가지게 된다.
- 스프링 DI 컨테이너 생성
- 스프링 빈 생성
- 의존관계 주입
- 초기화 콜백
- 스프링 빈 사용
- 소멸 전 콜백
- 스프링 종료
- 예외적으로 생명주기가 더 짧은, 즉 스프링 컨테이너와 생명주기가 같지 않은 스프링 빈들도 있는데 이 빈들도 역시 소멸 전 콜백을 제공한다. 이는 이후 빈 스코프에서 알아보도록 한다.
빈 생명 주기 콜백 지원 - 인터페이스
- 스프링은 3가지 방식으로 생명주기 콜백을 지원한다.
- 그 중 첫번째가 인터페이스를 활용한 지원이다.
public class NetworkClient implements InitializingBean, DisposableBean{
private String url;
public NetworkClient(){
System.out.println("constructor called");
}
public void connect(){
System.out.println("connect : " + this.url);
}
public setUrl(String url){
this.url = url;
}
public void call(String msg){
System.out.println("url : " + this.url + "msg : " + msg);
}
public void disconnect(){
System.out.println("close : " + url);
}
@Override
public void afterPropertiesSet() throws Exception{
connect();
call("initialize msg");
}
@Override
public void destroy() throws Exception{
disconnect();
}
}
- 스프링은 InitializingBean과 DisposableBean 인터페이스를 제공하여 콜백 메서드를 지원할 수 있다. afterPropertiesSet() 메서드가 스프링 빈의 의존관계 주입이 끝나고 초기화 작업을 수행할 수 있을 때 호출되며 destroy()메서드가 소멸 전 콜백으로 호출된다.
- 해당 방식의 문제점은 이 인터페이스 자체가 스프링 전용 인터페이스이기 때문에 스프링에 종속적이라는 것이다. 또한 해당 인터페이스의 메서드를 오버라이딩 하는 것이기 때문에 메서드의 이름을 변경할 수 없다.
- 또한 만약 해당 초기화 작업이 라이브러리에 수행되어야 하는 경우, 별다른 방법이 존재하지 않는다. 코드를 고칠 수 없기 때문이다.
빈 생명 주기 콜백 지원 - @Bean 설정
- @Bean 어노테이션 내부의 initMethod, destroyMethod 속성을 통해서도 지정해줄 수 있다.
public class NetworkClient{
private String url;
public NetworkClient(){
System.out.println("constructor called");
}
public void connect(){
System.out.println("connect : " + this.url);
}
public setUrl(String url){
this.url = url;
}
public void call(String msg){
System.out.println("url : " + this.url + "msg : " + msg);
}
public void disconnect(){
System.out.println("close : " + url);
}
public void init(){
connect();
call("initialize msg");
}
public void destroy(){
disconnect();
}
}
@Configuration
public class LifeCycleConfig{
@Bean(initMethod="init", destoryMethod="close")
public NetworkClient networkClient(){
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://somthing.dev");
return networkClient;
}
}
- 해당 방식의 장점은 초기화, 종료 메서드의 이름을 자유롭게 할 수 있다는 것이다. 또한 스프링 빈이 스프링 코드에 의존하지 않는다.
- 이때 destroyMethod의 기본값은 (inferred)라는 값으로 되어 있는데 이는 추론으로 close, shutdown라는 이름의 메서드를 자동으로 호출할 수 있다. 따라서 메서드 이름을 close로 바꾼다면 destroyMethod는 따로 설정해주지 않아도 호출된다.
- 또한 이 방식의 경우 외부 라이브러리 초기화에도 @Bean 어노테이션을 통해 지정할 수 있기 때문에 초기화와 종료 작업을 정상적으로 수행할 수 있다.
빈 생명 주기 콜백 지원 - 어노테이션 @PostConstruct, @PreDestory
- 어노테이션을 사용해서도 동일하게 작업할 수 있다.
public class NetworkClient{
private String url;
public NetworkClient(){
System.out.println("constructor called");
}
public void connect(){
System.out.println("connect : " + this.url);
}
public setUrl(String url){
this.url = url;
}
public void call(String msg){
System.out.println("url : " + this.url + "msg : " + msg);
}
public void disconnect(){
System.out.println("close : " + url);
}
@PostConstruct
public void init(){
connect();
call("initialize msg");
}
@PreDestory
public void destroy(){
disconnect();
}
}
@Configuration
public class LifeCycleConfig{
@Bean
public NetworkClient networkClient(){
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://somthing.dev");
return networkClient;
}
}
- 해당 방식은 최신 스프링에서 가장 권장하는 방법이다.
- 어노테이션을 붙임으로서 동일한 동작을 수행하기 떄문에 매우 편리하며 해당 어노테이션들은 스프링 전용이 아닌 자바 표준 기술이기 때문에 스프링이 아닌 다른 컨테이너에서도 동일하게 동작한다.
- 다만 외부 라이브러리에는 적용할 수 없다는 단점이 있다. 이는 @Bean 설정을 통해 해결하도록 하자.
'Spring & JPA > Spring' 카테고리의 다른 글
Spring Framework - 빈 스코프 - 2. Provider (0) | 2023.05.25 |
---|---|
Spring Framework - 빈 스코프 - 1. 빈 스코프와 프로토타입 스코프 (0) | 2023.05.25 |
Spring Framework - 의존관계 자동 주입 - 4. 정리 (0) | 2023.05.23 |
Spring Framework - 의존관계 자동 주입 - 3. @Primary와 @Qualifier (0) | 2023.05.22 |
Spring Framework - 의존관계 자동 주입 - 2. 생성자 주입의 장점과 lombok (0) | 2023.05.18 |