들어가며

  • 이번 포스트는 스프링 빈의 생명 주기(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 설정을 통해 해결하도록 하자.
복사했습니다!