Back-End/Spring Boot

[Spring Boot] Bean 생명주기 (Life cycle) 콜백

s워니얌 2022. 10. 11. 00:02

 

📑 스프링 컨테이너 생명주기 

 

스프링컨테이너에 대표적인, 아주 기본적인 생명주기를 살보자면 아래와 같다.

1. ApplicationContext를 이용해 객체를 생성하고 스프링 컨테이너를 초기화 한다 .
2. getBean()과 같은 메서드를 이용해서 컨테이너에 있는 빈 객체를 사용한다.
3. close() 메서드를 이용해 컨테이너를 종료한다.

 

📌 컨테이너 초기화 작업 :  빈 객체 생성, 초기화 및 의존 객체를 주입

📌 컨테이너 종료 : 빈 객체를 소멸하는 작업

 

 

빈 객체의 생명 주기는 객체 생성 -> 의존 설정 -> 초기화 -> 소멸 과정이다.

 

1. 스프링 컨테이너를 초기화 할 때, 가장 먼저 빈 객체를 생성한다. 
2. 빈 객체 생성 후, 의존을 설정한다. 즉) 의존성 주입을 한다.
3. 모든 의존 설정이 완료되면, 빈 객체 초기화를 한다.
( 빈 객체 초기화 위해, 빈 객체의 지정한 메서드를 호출한다.)
4. 초기화 콜백 -> 사용 -> 소멸 전 콜백 
5. 스프링 컨테이너를 종료하면, 스프링 컨테이너는 빈 객체를 소멸시킨다. 

 

 

스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 수 있는 준비가 완료된다. (참고로 의존관계 주입은 ApplicationContext를 생성할 때 발생된다.)  따라서 초기화 작업은 의존관계 주입이 모두 완료되고 난 후에 호출해야한다. 그렇다면 어떻게 의존관계 주입이 모두 완료된 시점을 알 수 있을까?

 

스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백베서드를 통해 초기화 시점을 알려주는 다양한 기능을 제공해준다. 또한) 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다. 따라서 안전하게 종료 작업을 할 수 있다. 

 

 

 

 

📑  @PostConstruct, @PreDestory

 

스프링은 크게 3가지 방법으로 빈 생명주기 콜백을 지원한다.

1. 인터페이스 ( InitializingBean, DisposableBean)
2. 설정 정보에 초기화 메서드, 종료 메서드 지정
3. @PostConstruct, @PreDestroy 애노테이션 지원

 

 

위의 3가지 방법중에 3번을 대표적으로 사용하므로 3번에 대한 설명을 예제로 알아보려 한다.

다음과 같은 url을 저장하는 클래스가 있을 때 생성자를 만들어주고 set메서드로 url을 매개변수로 받는 메서드를 열어두었다. 서버와 연결했을때를 가정으로 connect라는 메서드와 서버 종료시 메서드인 disconnection 또한 생성해두었다. 

 

 

package hello.core.lifecycle;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

public class NetworkClient {

    private String url;

    public NetworkClient(){
        System.out.println("생성자 호출, url = " + url);
//        connect();
//        call("초기화 연결 메시지");
    }

    public void setUrl(String url){
        this.url = url;
    }

    //서비서 시작시 호출
    public void connect(){
        System.out.println("connect: " +url);
    }

    public void call(String message){
        System.out.println("call " + url + " message = " + message);
    }

    //서비스 종료시 호출
    public void disconnect(){
        System.out.println("close" + url);
    }

    //의존관계 주입이 끝나면 호출
    //의존관계 주입은 ApplicationContext를 생성할 때 실행
    @PostConstruct
    public void init() {
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메시지");

    }

    @PreDestroy
    public void close() {
        System.out.println("NetworkClient.close");
        disconnect();
    }
}

 

 

여기서 잠깐! 생성자로 간편하게 url을 넘겨받음 되는 거 아냐? public NetworkClinet()가 아닌 public NetworkClienct(String url)로 생서자 호출할 때 초기화까지 해버리면 편하지 않나? 의문이 들 수 있다. 

 

📌 객체의 생성과 초기화를 분리하자

 

생성자는 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다. 반면 초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는 등 무거운 동작을 수행한다. 따라서 생성자 안에서 무거운 초기화 작업을 함께 하는 것 보다 객체를 생성하는 부분과 초기화 하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋다.!! 물론 초기화 작업이 내부의 값들만 약간 변경한다면 무관하지만~~ 

 

 

위의 client 클래스를 가지고 테스트를 진행해보자.

 

package hello.core.lifecycle;

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class BeanLifeCycleTest {

    @Test
    public  void lifeCycleTest(){

        //ApplicationContext는 close 제공 안함.
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
    	NetworkClient client = ac.getBean(NetworkClient.class);
        ac.close();

    }

    @Configuration
    static class LifeCycleConfig{
       // @Bean(initMethod = "init", destroyMethod = "close")
        //스프링 컨테이너 실행 -> 스프링 빈으로 등록 -> setUrl() 호출 -> 의존관계 주입  -> 초기화
       @Bean
        public NetworkClient networkClient(){
            NetworkClient client = new NetworkClient();
            client.setUrl("http://hello-spring.dev");
            return client;
        }
    }
}

 

생성자 호출, url = null
NetworkClient.init
connect: http://hello-spring.dev
call http://hello-spring.dev message = 초기화 연결 메시지 16:21:27.002
[main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@73e9cf3
NetworkClient.close closehttp://hello-spring.dev

 

 

1. 테스트 코드에서 ac라는 스프링 컨테이너를 생성한다. 

2. LifeCycleConfig.class에 있는 @Bean이 붙은 메서드들을 스프링 컨테이너에 빈으로 저장한다. networkClient라는 이름을 가진 빈이 생성된다. (2번까지 모두 빈 생성 과정)

2-1. networkClient라는 빈에서 NetworkClient 타입의 객체가 생성된다.

2-2. 객체가 생성됨과 동시에 NetworkClient 생성자를 호출해 생성자 호출, url = null이 출력된다.

2-3 . NetworkClient 타입인 networkClient에 setUrl을 통해 url에 값을 저장한다.

3. 이렇게 생성된 객체가 @Bean 메서드에 반환되면 이후에 스프링 빈으로 등록되게 된다. 이렇게 스프링 빈으로 등록되면 다르 곳에서 의존관계 주입이 될 수 있다. 

4. 스프링 컨테이너 실행 -> 스프링 빈으로 등록  (setUrl() 호출) -> 의존관계 주입  -> 초기화(@PostConstruct) 

 

반응형