스프링 핵심 원리 08] 빈 생명주기 콜백

컴업·2022년 1월 18일
0

본 포스트는 Inflearn 김영한 선생님 강의를 정리한 것 입니다!

이번 포스트에는 빈이 생성되고 종료되는 생명주기에 대해 알아보도록 하겠습니다.

DB 커넥션 풀이나 네트워크 소켓처럼 애플리케이션 시작 시점에 여러개를 미리 연결해두고 종료 시점에 모두 종료하는 경우 이를 위한 초기화, 종료 작업이 필요합니다.

스프링 빈을 통해 이러한 초기화 작업과 종료 작업을 어떻게 진행하는지 알아보겠습니다.


참고
여기서 초기화란 단순히 객체를 생성하는 것이 아니라 실제 자신이 맡은 일을 수행 할 수 있도록 모든 준비를 끝마치는 것을 의미합니다.


1. 빈 생명주기 콜백 시작

외부 네트워크에 미리 연결하는 객체를 하나 생성한다고 가정해보겠습니다.

NetworkClient는 애플리케이션 시작시점에 connect()를 호출해 연결을 맺어두고, 애플리케이션이 종료되면 disconnect()를 호출해 연결을 끊습니다.

package hello.core.lifecycle;

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);
    }
}

이제 이 객체를 스프링 컨테이너에 등록하고 초기화 작업을 진행해 보도록 하겠습니다.

package hello.core.lifecycle;

import org.junit.jupiter.api.Test;
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() {
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        NetworkClient client = 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;
        }
    }
}

사실 스프링 컨테이너도 close()메소드로 종료할 수 있지만 실제로 종료할 일이 많지 않아 기본 인터페이스인 ApplicationContext보다 한참 하위인 ConfigurableApplicationContext에서나 제공받을 수 있습니다.

아무튼 NetworkClient 객체를 만들고, setUrl을 통해 초기화 작업에 필요한 url을 주입해주었습니다.

그러나 실행해보면 다음과 같은 결과가 호출됩니다.

생성자 호출, url = null
connect: null
call: null message = 초기화 연결 메시지

객체를 생성하는 단계에는 url이 아직 없고 객체를 생성한 다음에 외부에서 수정자 주입을 통해 setUrl()이 호출되어야 url이 존재하기 때문에 정상적인 초기화작업을 할 수 없었던 것입니다.

스프링 빈은 이와 비슷하게 객체 생성 -> 의존관계 주입이라는 라이프사이클을 가집니다. (생성자 주입 제외)

때문에 NetworkClient의 초기화 작업이 url 주입이 끝난 후에 호출되어야 하는 것 처럼, 스프링 빈의 초기화 작업을 하려면 객체 생성이 일어나고 의존관계 주입이 끝난 뒤에 해야합니다.

그렇다면 개발자는 의존관계 주입이 끝난 시점을 어떻게 알 수 있을까요?

스프링은 의존관계 주입이 끝나면 스프링 빈에게 콜백 메서드를 통해 초기화 시점을 알려주는 다양한 기능을 제공합니다. 또한 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 줍니다.

따라서 스프링 빈의 이벤트 라이프 사이클은 다음과 같이 정리할 수 있습니다.
스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸전 콜백 -> 스프링 종료

참고
NetworkClient 예시에서 setUrl말고 그냥 생성자에서 url을 넣어주면 돼지 않을까 생각할 수 있습니다. 그러나 생성자는 파라미터를 받고, 메로리를 할당해서 객체를 생성하는 책임을 가집니다. 반면 초기화는 이렇게 생성된 값들을 활용해 외부 커넥션을 연결하는등 무거운 작업을 수행합니다.
따라서 생성자 안에서 이 모든 작업을 함께 하기 보다는 객체를 생성하는 부분, 초기화 하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋습니다.

참고
싱글톤 빈들은 스프링 컨테이너가 종료될 때 함께 소멸되기 때문에 종료 직전에 콜백이 일어납니다.
그에 반해 컨테이너와 무관하게 해당빈이 소멸되는 경우도 있는데 이 경우 빈 소멸 전에 콜백이 일어납니다.

2. 인터페이스 InitializingBean, DisposableBean

의존관계 주입이 끝났음을 알려주는 방법 첫번째, InitializingBean, DisposableBean인터페이스를 사용한다.

package hello.core.lifecycle;

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

public class NetworkClient implements InitializingBean, DisposableBean {
    
    private String url;
    
    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
    }
    
    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);
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {
        connect();
        call("초기화 연결 메세지");
    }
    
    @Override
    public void destroy() throws Exception {
        disConnect();
    }
}

InitializingBean, DisposableBean 인터페이스를 각각 받으면 afterPropertiesSet(), destroy()메서드를 제공합니다.

afterPropertiesSet()은 의존관계 주입이 끝날 시 호출되는 콜백메서드로 필요한 초기화 작업을 여기서 할 수 있습니다.

destroy()는 이름에서 유추할 수 있듯 소멸 전에 호출되는 콜백메서드 입니다.

생성자 호출, url = null
NetworkClient.afterPropertiesSet
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 초기화 연결 메세지  
13:24:49.043 [main] DEBUG
org.springframework.context.annotation.AnnotationConfigApplicationContext -
Closing NetworkClient.destroy
close + http://hello-spring.dev

출력 결과를 보면 정상적으로 url주입 후 메서드가 호출되었고, 스프링 컨테이너 종료가 호출되자 소멸 메서드가 호출된것을 확인할 수 있습니다.

이러한 인터페이스를 사용하는 방법은 2003년에야 나온 방법으로 지금은 더 나은 방법들을 주로 사용하고 있습니다.

그 이유는 다음과 같습니다

  • 스프링 전용 인터페이스이므로 코드가 스프링 전용 인터페이스에 의존한다.
  • 초기와, 소멸 메서드 이름 변경이 불가능하다.
  • 내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.
    외부 라이브러리를 빈으로 등록하려하는데 고칠 수 없다면 인터페이스들 땡겨와서 메서드를 만들 수 가 없으므로

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

의존관계 주입이 끝났음을 알려주는 방법 두번째로, 초기화 소멸메서드를 지정할 수 있습니다.

package hello.core.lifecycle;

public class NetworkClient {

    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
    }

    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);
    }

    public void init() {
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메서드");
    }

    public void close() {
        System.out.println("NetworkClient.close");
        disConnect();
    }
}

먼저 NetworkClient에서 아까 땡겨온 인터페이스와 메서드 제거하고, init(), close()라는 초기화, 소멸 메서드를 작성해주었습니다. (이름 달라도 괜찮습니다.)

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

그런 뒤 @Bean에서 initMethod, destroyMethod를 아까 만들었던 메서드로 지정해 주었습니다.

생성자 호출, url = null
NetworkClient.init
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 초기화 연결 메서드
13:26:44.433 [Test worker] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@1a5a4e19, started on Tue Jan 18 13:26:44 KST 2022
NetworkClient.close
close + http://hello-spring.dev

정상적으로 초기화 된것을 확인 할 수 있습니다.

위 방법은 메서드 이름을 자유롭게 고를 수 있고, 스프링 빈이나 코드에 의존하지 않습니다.

또한 설정정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있습니다(이름만 지정하면 되니까요).

참고: 종료 메서드 추론
@Bean의 destroyMethod속성은 지정하지 않으면 close, shutdown이라는 이름의 종료 메서드를 자동으로 찾아 호출합니다.

destroyMethod의 기본값이 inferred이기 때문입니다.

덕분에 메서드 이름을 close, shutDown으로 하면 굳이 설정정보를 등록하지 않아도 자동으로 소멸 메서드가 호출됩니다.

이 기능을 사용하지 않으려면 destroyMethod=""로 공백을 지정하면 됩니다.

4. 애노테이션 @PostConstruct, @PreDestroy

결론적으로 그냥 이걸 쓰시면 됩니다.

package hello.core.lifecycle;

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

public class NetworkClient {
    
    private String url;
    
    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
    }
    
    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);
    }
    
    @PostConstruct
    public void init() {
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메서드");
    }
    
    @PreDestroy
    public void close() {
        System.out.println("NetworkClient.close");
        disConnect();
    }
}

초기화 메서드에 @PostConstruct, 소멸 메서드에 @PreDestroy 애노테이션을 붙여줍니다.

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

아까 추가했던 @Bean의 추가 설정도 제거합니다.

생성자 호출, url = null
NetworkClient.init
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 초기화 연결 메서드
13:26:44.433 [Test worker] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@1a5a4e19, started on Tue Jan 18 13:26:44 KST 2022
NetworkClient.close
close + http://hello-spring.dev

출력을 확인해보면 정상적으로 초기화, 소멸 메서드가 호출된 것을 확인할 수 있습니다.

이 방법은 매우 간편할 뿐만 아니라, 최신 스프링에서 가장 권장하는 방법이기도 합니다.

또 javax는 스프링에 종속적인 기술이 아니라 자바 표준인 JSR-250이기 때문에 스프링이 아닌 다른 컨테이너에서도 동작합니다.

그러나 이는 외부 라이브러리에는 사용할 수 없습니다.(어쨋든 코드를 수정해야하니까)

따라서 기본적으로 이방법을 사용하고, 그러지 못한 경우에 @Bean의 기능을 사용하는 것을 권장합니다.

profile
좋은 사람, 좋은 개발자 (되는중.. :D)

0개의 댓글