ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Boot] Bean 생명주기 (Life cycle) 콜백
    Back-End/Spring Boot 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) 

     

    반응형

    댓글

Designed by Tistory.