본문 바로가기

WEB

[WEB] 빈 생명주기 콜백

실무 개발을 하다보면 DBCP(데이터베이스 커넥션 풀)를 생성하거나, 소켓을 초기화 하는 등 에플리케이션의 시작 시점에 작업을 미리해두고, 마찬가지로 종료 시점에 해당 연결을 끊는 작업을 하는 경우가 많다. 이번 포스팅에서는 스프링이 어떤 방식으로 어플리케이션의 시작 시점과 종료 시점에 작업들을 처리하는 지에 대해 알아보자.

 

이 포스팅은 김영한님의 '스프링 핵심 원리 - 기본편' 강의에 의존하고 있습니다.

 


빈 생명주기와 콜백 메서드

public class NetworkClient {
	
    private String url;
    
    public void setUrl(String url) {
    	this.url = url;
    }
    
    public NetworkClient() {
    	System.out.println("create Client(url=" + url + ")");
        connect();
        call("init message");
    }
    
    public void connect() {
    	System.out.println("connect: " + url);
    }
    
    public void call(String message) {
    	System.out.println("call " + url + " : " + message);
    }
    
    public void disconnect() {
    	System.out.println("disconnect: " + url);
    }
}

다음과 같이 네트워크 통신을 하는 클라이언트 객체가 있다고 하자. 클라이언트를 생성하면 클라이언트 생성 메세지를 출력한후, 연결 및 초기화 메세지를 보낸다.

 

만약 이 클라이언트를 생성하여 url을 설정하면 어떤 결과가 발생할까?

public class LifeCycleExample {
	
    public static void main(String[] args) {
    	ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        NetworkClient client = ac.getBean(NetworkClient.class);
        ac.close()
    } 
}

// AppConfig.class

@Configuration
public class AppConfig {
	
    @Bean
    public NetworkClient networkClient() {
    	NetworkClient client = new NetworkClient();
        client.setUrl("http://hello.dev");
        return client;
    }
}

 

main 메서드를 실행하면 다음과 같은 결과가 발생한다.

create client(url=null)
connect : null
call null : init message

url이 모두 null로 출력되게 된다. 왜냐하면 연결을 하는 초기화 작업이 url 의존관계를 주입하는 시점보다 먼저 처리되었기 때문이다.


스프링 빈의 이벤트 생명주기 

빈을 생성하는 시점에는 다음과 같은 생명주기를 가진다.

 

  • 객체 생성 → 의존관계 주입

이 시점이 끝난 이후 초기화 작업이 진행되어야 하고, 그 이후에 비로소 해당 스프링 빈을 사용할 수 있다. 그후, 소멸 직전에 소멸전 작업이 처리되고, 스프링 빈이 소멸되게 된다. 이 과정을 정리하면 다음과 같다.

 

  • 스프링 빈 생성 → 의존관계 주입 → 초기화 작업 → 사용 → 소멸전 작업 → 스프링 빈 종료 

이때 초기화 작업을 초기화 콜백이라고 하고, 소멸전 작업을 소멸전 콜백이라고 한다.

 

지금부터 초기화 콜백과 소멸전 콜백 구현을 하는 방법에 대해 알아보자.


 

방법1 : 인터페이스(InitializingBean, DisposableBean) 사용

InitializingBean은 초기화 콜백 메서드를 제공하고, DisposableBean은 소멸전 콜백 메서드를 제공한다. 

public class NetworkClient implements InitializingBean, DisposableBean {
	
    private String url;
    
    public void setUrl(String url) {
    	this.url = url;
    }
    
    public NetworkClient() {
    	System.out.println("create Client(url=" + url + ")");
    }
    
    public void connect() {
    	System.out.println("connect: " + url);
    }
    
    public void call(String message) {
    	System.out.println("call " + url + " : " + message);
    }
    
    public void disconnect() {
    	System.out.println("disconnect: " + url);
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {
    	connect();
        call("init message");
    }
    
    @Override
    public void destroy() throws Exception {
    	disconnect();
    }
}

afterPropertiesSet() 메서드는 객체가 생성되고 의존관계 주입이 완료된 시점에 호출되어 초기화 작업을 처리하고, destroy() 메서드는 객체가 소멸되지 전 시점에 호출되어 소멸전 작업을 처리한다.

 

다시 LifeCycleExample.main 메서드를 실행해 보면

create client(url=null)
connect : http://hello.dev
call http://hello.dev : init message

connect() / call() 메서드 실행 시 정상적으로 url을 출력하는 것을 볼 수 있다.

 

이때 사용하는 인터페이스(InitializingBean, DisposableBean)는 스프링 전용 인터페이스로, 코드가 스프링에 의존한다는 문제가 발생한다. 스프링에 의존하는 코드는 여러 문제를 일으키는데 다음과 같다.

 

  • 초기화, 소멸 메서드의 이름을 변경할 수 없다.
  • 내가 코드를 고칠 수 업슨ㄴ 외부 라이브러리에 적용할 수 없다.

 

빈 등록 초기화, 소멸 메서드 지정

설정 정보에 초기화, 소멸 메서드를 지정할 수 있다.

public class NetworkClient {
	
    private String url;
    
    public void setUrl(String url) {
    	this.url = url;
    }
    
    public NetworkClient() {
    	System.out.println("create Client(url=" + url + ")");
    }
    
    public void connect() {
    	System.out.println("connect: " + url);
    }
    
    public void call(String message) {
    	System.out.println("call " + url + " : " + message);
    }
    
    public void disconnect() {
    	System.out.println("disconnect: " + url);
    }
    
    public void init() throws Exception {
    	connect();
        call("init message");
    }
    
    public void destroy() throws Exception {
    	disconnect();
    }
}
public class LifeCycleExample {
	
    public static void main(String[] args) {
    	ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        NetworkClient client = ac.getBean(NetworkClient.class);
        ac.close()
    } 
}

// AppConfig.class

@Configuration
public class AppConfig {
	
    @Bean(initMethod = "init", destroyMethod = "close")
    public NetworkClient networkClient() {
    	NetworkClient client = new NetworkClient();
        client.setUrl("http://hello.dev");
        return client;
    }
}

다음과 같이 @Bean(initMethod = "init", destroyMethod = "destroy") 애노테이션에 초기화, 소멸 메서드를 지정하면 된다.

 

설정 정보에 초기화, 소멸 메서드를 지정하는 방법은 메서드의 이름을 자유롭게 지정할 수 있고, 스프링 빈이 스프링 코드에 의존하지 않는다. 또한 코드가 아니라 설정 정보를 사용함으로 외부라이러리에도 초기화, 종료 메서드를 지정할 수 있다.

 

(종료 메서드 추론)

@Bean 애노테이션의 destroyMethod 속성은 종료 메서드 추론이라는 매우 특별한 기능을 제공한다. destroyMethod의 기본값은 (inferred) 로 등록되어 있는데 이는 close, shutdown이 이름인 메서드를 자동으로 호출하게 된다. 즉, 스프링 빈의 종료 메서드의 이름이 close 또는 shutdown이면 destroyMethod 속성을 지정해주지 않아도, 자동으로 찾아서 호출되게 된다.


 

방법3 : @PostConstruct, @PreDestroy

실제로 가장 많이 사용하는 방법으로 초기화, 종료메서드에 @PostConstruct, @PreDestroy 애노테이션을 지정함으로써 초기화, 종료 메서드를 설정하는 방법이다. 코드로 보면 다음과 같다.

public class NetworkClient {
	
    private String url;
    
    public void setUrl(String url) {
    	this.url = url;
    }
    
    public NetworkClient() {
    	System.out.println("create Client(url=" + url + ")");
    }
    
    public void connect() {
    	System.out.println("connect: " + url);
    }
    
    public void call(String message) {
    	System.out.println("call " + url + " : " + message);
    }
    
    public void disconnect() {
    	System.out.println("disconnect: " + url);
    }
    
    
    @PostConstruct
    public void init() throws Exception {
    	connect();
        call("init message");
    }
    
    @PreDestroy
    public void destroy() throws Exception {
    	disconnect();
    }
}

 

@PostConstruct, @PreDestroy 애노테이션은 자바 표준으로 스프링에 의존하지 않는다. 또한 애노테이션을 지정하는 방식으로 간편하고, 컴포넌트 스캔 방식과도 잘 어울린다. 단, 외부 라이브러리를 초기화 종료 하는 경우에는 사용할 수 없다.

 

최신 스프링은 이 방식을 가장 권장하고 있다.