빈 생명주기 콜백

    빈 생명주기 콜백


    네트워크 소켓처럼 어플리케이션 시작 시점에 필요한 연결을 미리 해두고, 어플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면 객체의 초기화와 종료 작업이 미리 필요하다. 미리 연결을 해두면 빠르게 처리가 가능하고, 종료 전에 객체의 종료가 완료되면 안전하게 종료할 수 있는 장점이 있기 때문이다. 이를 코드로 미리 한번 해보는 작업을 해보려고 한다.

    public class NetworkClient {
    
    private String url;
    
        public NetworkClient() {
            System.out.println("생성자 호출, url = " + url);
            connect();
            call("초기화 연결 메세지");
        }
    
        private void connect() {
            System.out.println("connect : " + url);
        }
    
        private void call(String message) {
            System.out.println("call :  = " + url + " message = " + message);
        }
    
        public void setUrl(String url) {
            this.url = url;
        }
    
        public void close(){
            System.out.println("close: " + url);
        }
    }

     위 클래스를 만들어둔다. network라는 객체가 생성될 때, 초기화가 되는 형태로 코드를 짜둔다. 

    public class BeanLifeCycleTest {
    
        @Test
        void test1(){
            AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
            NetworkClient networkClient = ac.getBean(NetworkClient.class);
            ac.close();
        }
    
        @Configuration
        static class LifeCycleConfig{
            @Bean
            public NetworkClient networkClient(){
                NetworkClient networkClient = new NetworkClient();
                networkClient.setUrl("http://hello-spring.dev");
                return networkClient;
            }
        }
    }

    테스트 코드를 위처럼 짠다. LifeCycleConfig static class를 만들어 NetworkClient 빈 등록을 위한 설정정보로 지정해둔다. 이 때, 빈에는 "http://hello-spring.dev" url이 셋팅되면서 빈이 등록된다. 메인 테스트 코드에서는 LifeCycleTest를 스프링 컨테이너 빈에 등록하고, 스프링 빈을 찾아와서 ac.close()까지 하는 형태다. 위의 테스트를 실행하면 아래 결과가 나온다.

    예상했던 것과는 다르게 나온다. 생성자 호출 시에는 url = null이 되는 것은 맞으나, null로 연결되고, 초기화 연결 완료된 후에 나오는 값이 null이다. 굉장히 이상하다. 왜 이렇게 될까? 

     

    빈 생명주기 콜백


    너무 당연한 이야기지만, 객체를 생성하는 단계에는 url이 없다. url은 객체를 생성한 다음에 외부에서 수정자 주입을 통해서 설정되게 된다. 

    스프링 빈은 알다시피 스프링 빈 생성 → 빈 의존관계 주입의 단계가 끝나야 빈 객체가 사용될 준비가 완료된다. url을 셋팅하기 위해서는 빈 의존관계 주입이 완료되는 시점을 알아야 한다. 그렇다면 개발자가 어떻게 이 시점을 알 수 있을까?  스프링은 의존관계 주입이 완료되면, 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공한다. 또한, 스프링은 스프링 컨테이너가 소멸되기 전 소멸 콜백을 준다.

     

    스프링 빈의 이벤트 라이프 사이클은 아래와 같다.

    1. 스프링 컨테이너 생성(new ApplicationContext)
    2. 스프링 빈 생성(AutoAppConfig.class)
    3. 스프링 빈 의존관계 주입(@Autowired)
    4. 초기화 콜백
    5. 사용
    6. 소멸 전 콜백
    7. 스프링 종료

    위 스프링 빈 이벤트 라이프 사이클을 활용하면 안전하게 초기화를 할 수 있고, 안전하게 종료를 할 수 있다. 

     

    객체의 생성 / 초기화 분리


    객체의 생성과 초기화는 분리시키는 것이 좋다. 단일책임 원칙에 따라 생성자는 생성의 역할만, 수정자는 초기화의 역할만 하도록 나누는 것이다. 뿐만 아니라 외부와의 연결이 진행되는 초기화는 그 자체가 무거울 가능성이 크기 때문에 나눠서 하는 것이 좋다.

    이렇게 객체 생성과 초기화를 나눌 경우, 객체를 생성한 후 특정한 액션이 있을 때까지 초기화를 하지 않아도 된다. 즉, 언제 초기화를 할 수 있을지 취사선택할 수 있다는 장점이 있다.

     

    스프링 빈 생명주기 콜백함수


    스프링 빈 생명주기 콜백함수는 아래 세 가지로 구현할 수 있다고 한다.

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

    아래에는 위 방법들에 대해 좀 더 자세히 알아보고 정리하려고 한다.

     

    인터페이스(InitializingBean, DisposableBean)을 통한 콜백 함수 구현


    콜백함수 구현이 필요한 빈 객체 클래스에서 InitializingBean, DisposableBean을 구현하는 것으로 콜백 함수 구현이 가능하다. 위의 클래스 코드를 아래 코드로 수정하여 구현한다.

    public class NetworkClient implements InitializingBean, DisposableBean {
    
        private String url;
    
        public NetworkClient() {
            System.out.println("생성자 호출, url = " + url);
        }
    
        private void connect() {
            System.out.println("connect : " + url);
        }
    
        private void call(String message) {
            System.out.println("call :  = " + url + " message = " + message);
        }
    
        public void setUrl(String url) {
            this.url = url;
        }
    
        public void disconnect(){
            System.out.println("close: " + url);
        }
    
    
        @Override
        public void destroy() throws Exception {
            System.out.println("NetworkClient.destroy");
            disconnect();
        }
    
        @Override
        public void afterPropertiesSet() throws Exception {
            System.out.println("NetworkClient.afterPropertiesSet");
            connect();
            call("초기화 연결 메세지");
        }
    }

    주요한 변화는 두 개의 콜백 인터페이스를 구현했기 때문에 재정의된 메서드가 2개 추가가 되었다. 그리고 이 재정의 메서드의 의미를 보았을 때, connect(), disconnect() 함수를 생성자에 넣는 것이 아니라 바꾸는 것이 적절하기 때문에 connect()는 초기화 콜백 함수에 넣었고, disconnect()는 소멸 콜백 함수에 넣었다.

        @Configuration
        static class LifeCycleConfig{
    
            @Bean
            public NetworkClient networkClient(){
                NetworkClient networkClient = new NetworkClient();
                networkClient.setUrl("http://hello-spring.dev");
                return networkClient;
            }
        }

    빈 등록을 할 때를 살펴본다. networkClient 빈을 등록할 때, 먼저 netWorkClient 객체를 하나 생성한다. 생성할 때, 생성자는 soutv로 null값을 출력한다. 이후 수정자를 통해 url 값이 등록되고, 빈 객체로 등록된다. 빈 객체로 등록이 완료되고 의존관계 주입까지 완료되면 초기화 콜백 함수가 불러지게 된다. 여튼, 위 수정된 코드로 다시 한번 테스틀 돌리면 아래 결과가 나오게 된다.

    위 방법의 단점은 이 인터페이스들이 스프링 전용 인터페이스라는 것이다. 따라서, 스프링 코드에 종속적이 되고, 초기화 및 소멸 메서드의 이름을 변경할 수 없게 된다. 스프링 초기에 나온 것들이기 때문에 현재는 거의 사용되지 않는다고 한다.

     

    설정 정보에 초기화 메서드, 종료 메서드 등록 (initMethod, destoryMethod)


    클래스 내에 초기화 메서드, 종료 메서드를 만들어 두고 이름 @Bean에 명시하는 방식으로 초기화, 소멸 콜백을 지정하는 방법이 있다. 

        public void init(){
            System.out.println("NetworkClient.init");
            connect();
            call("초기화 연결 메세지");
        }
    
        public void disconnect(){
            System.out.println("NetworkClient.disconnect");
            System.out.println("close: " + url);
        }

    먼저 빈 객체 클래스에 위의 메서드들을 구현해둔다.

        @Configuration
        static class LifeCycleConfig{
    
            @Bean(initMethod = "init", destroyMethod = "disconnect")
            public NetworkClient networkClient(){
                NetworkClient networkClient = new NetworkClient();
                networkClient.setUrl("http://hello-spring.dev");
                return networkClient;
            }
        }

    빈 객체를 등록할 때, @Bean 정보 옆에 initMethod, destoryMethod로 메서드 명을 명시화해주면 자동으로 초기화 콜백함수, 소멸 콜백 함수로 지정이 되어 각 단계에 맞게 함수들이 호출된다. 

    빈 설정정보에 초기화 / 소멸 콜백 함수를 명시화하는 방법의 장/단점은 아래와 같다

    • 장점
      1. 메서드 명을 자유롭게 설정할 수 있다.
      2. 스프링 빈이 스프링 코드에 의존하지 않는다
      3. 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다. (가장 큰 장점)
    • 단점 
      빈을 등록할 때만 사용할 수 있다.

     

    destoryMethod의 숨겨진 기능

    destoryMethod를 타고 올라가면 'INFER_METHOD'가 Default 값으로 등록된 것을 볼 수 있다. INFER는 추론이란 뜻이다. 이름 그대로 관습적으로 사용하는 종료 메서드를 '추론'해서 자동으로 실행해준다. 예를 들면, close, shutdown 같은 것들이다.

    이런 이유 때문에 destoryMethod를 명시하지 않을 경우, 추론을 통해 자동적으로 종료 콜백 함수를 실행해준다. 그렇다면 종료 콜백 함수를 실행하지 않고 싶을 때는 어떻게 해야할까?

    destroyMethod = ""

    위의 코드를 명시해주면, 소멸 콜백 함수를 사용하지 않는다.

     

    @PostConstruct, @PreDestroy 애노테이션 지원


    앞서 여러 콜백 메서드를 등록하는 방법에 대해 정리가 되었다. 그렇지만 결론은 이걸 사용하면 된다. @PostConstruct, @PreDestory를 import해보면 javax.annotation가 들어온다. 여기서 javax는 자바 진영의 표준 라이브러리를 의미한다.

    즉, 스프링에 종속적이지 않기 때문에 스프링을 제외한 다른 컨테이너를 사용할 때도 동작한다. 또한,  @ComponentScan과 매우 잘 어울린다. 유일한 단점은 외부 라이브러리에 사용하지 못한다는 것이다. 외부 라이브러리의 코드를 수정해야하기 때문이다. 이럴 때는 @Bean 설정정보를 활용해서 대응하면 된다.

        @PostConstruct
        public void init(){
            System.out.println("NetworkClient.init");
            connect();
            call("초기화 연결 메세지");
        }
    
        @PreDestroy
        public void disconnect(){
            System.out.println("NetworkClient.disconnect");
            System.out.println("close: " + url);
        }

    간단하게 @PostConstruct(초기화 콜백), @PreDestory(소멸 콜백) 어노테이션을 달아주면 된다.

     

    최종 정리


    빈 생명주기 콜백함수를 정리하면 다음과 같다.

    • 기본적으로 @PostConstructor, @PreDestory 어노테이션으로 콜백 함수를 사용한다
    • 외부 라이브러리 사용 시에는 @Bean 설정 정보에 initMethod, destroyMethod를 설정해서 사용한다
    • destoryMethod는 infer 기능이 있어 명시하지 않을 경우 관습적인 close, disconnect 같은 메서드들을 추론해서 자동으로 종료해준다. 

    댓글

    Designed by JB FACTORY