관리 메뉴

공부 기록장 💻

[Spring] 스프링 빈 생명 주기 콜백 (@PostConstruct, @PreDestroy를 이용한 스프링 빈 생성과 초기화, 소멸) 본문

# Tech Studies/Java Spring • Boot

[Spring] 스프링 빈 생명 주기 콜백 (@PostConstruct, @PreDestroy를 이용한 스프링 빈 생성과 초기화, 소멸)

dream_for 2023. 2. 14. 10:51

인프런 - 스프링 핵심 원리 기본편 정리


데이터베이스 커넥션 풀이나, 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화와 종료 작업이 필요하다.

스프링 빈은 간단하게 1. 객체 생성 후, 2. 의존 관계 주입 을 하는 라이프사이클을 가진다.
(단, 생성자 주입의 경우는 예외이다)
스프링 빈은 객체를 생성하고, 의존 관계 주입이 다 끝난 후에야 필요한 데이터를 사용할 수 있는 준비가 완료된다.
따라서 초기화 작업(초기 작업 시작)은 의존 관계 주입이 모두 완료되고 난 후에 호출해야 한다.

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

 

싱글톤 스프링 빈의 Event LifeCycle

1. 스프링 컨테이너 생성
2. 스프링 빈 생성 (생성자 주입의 경우 이 단계에서 진행되기도 함)
3. 의존 관계 주입 (setter/field injection의 경우)
4. 초기화 콜백
5. 애플리케이션 동작 및 데이터 사용
6. 소멸 전 콜백 (빈이 소멸되기 직전)
7. 스프링 종료

 

 

스프링은 다양한 방식으로 생명 주기 콜백을 지원한다. 

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

- 객체 생성: 메모리 할당
- 객체 초기화: 별다른 객체 사용의 작업은 초기화 단계로 넘기자

 

생성자는 필수 정보(파라미터)를 받고, 메모리를 할당하여 객체를 생성하는 책임을 가진다. 

반면, 초기화는 생성된 값들을 활용해 외부 커넥션을 연결하는 등 무거운 동작을 수행한다.

유지보수 관점에서, 생성자 안에서는 값의 변경과 같은 가벼운 초기화 작업만 허용하자. 체 생성과 초기화를 명확히 분리하자!

 

 


 

스프링의 빈 생명주기 콜백 방법

 

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

1) 인터페이스 (InitializingBean, DisposableBean)

2) 설정 정보에 초기화, 종료 메서드 지정

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

아래를 통해 하나하나 자세히 살펴보자.

 

 


1. 인터페이스(InitializingBean, DisposableBean)

  • InitializingBean의 afterPropertiesSet(), DisposableBean의 destroy() 메서드를 각각 오버라이딩하여 작성하면, 각각 스프링 빈 생성과 의존 관계 이후, 빈 종료 시 각각 호출이 된다.

 

아래의 예시를 살펴보자.

NetworkClient() 생성자 호출이 되며 connect(), call() 을 통해 초기화 작업이 진행되고,

close() 메서드의 disconnect() 를 통해 소멸 전 작업이 진행된다.

 

public class NetworkClient implements  {
    public 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);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        connect();
        call("초기화 연결 메세지");
    }

    @Override
    public void destroy() throws Exception {
        disconnect();
    }

}

 

InitializingBean과 DisposableBeaen 인터페이스를 구현하여 위의 작업을 변형해보면 다음과 같다.

생성자 호출에서 초기화 작업이 진행되지 않고,

생성자 이후 afterProtiesSet() 메서드를 통해 connect()와 call() 메서드가 실행되어 초기화 작업이 진행되고,

빈 소멸 직전 destroy() 메서드를 통해 disconnect() 메서드가 실행되어 연결 작업이 종료되는 것을 알린다.

 

public class NetworkClient implements InitializingBean, DisposableBean {
    public 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();
    }

}

 

 

초기화, 소멸 인터페이스의 단점은?

 

위의 초기화, 소멸 인터페이스는 Spring 전용 인터페이스로, 코드 자체가 스프링 전용 인터페이스에 의존한다.

초기화, 소멸 메서드를 오버라이딩하여 구체화하므로 메서드 이름을 바꿀 수 없으며, 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.

그리고 위 인터페이스를 사용한 생명주기 콜백 방식은 오래된 초기의 방식이기 때문에 권장하는 방법이 아니라고 한다.

 

 

 

2. 설정 정보에 초기화, 종료 메서드 지정

 

다음 방법은 빈 초기화, 소멸 인터페이스의 afterPropertiesSet(), destroy() 메서드를 구체화하는 대신,

설정 정보에 initMethod와 destroyMethod를 명시하는 방법이다.

 

다음과 같이 init(), close() 메서드로 변경한 후,

public class NetworkClient {

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

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

 

 

@Configuration 설정 정보 부분에서 @Bean 등록 시 초기 메서드와 소멸 메서드의 이름을 init과 close로 작성해준다.

 

   @Test
    public void lifeCycleTest(){
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        NetworkClient client = ac.getBean(NetworkClient.class);
        ac.close();
    }

    @Configuration
    static class LifeCycleConfig{
        @Bean(initMethod = "init", destroyMethod = "close")
        public NetworkClient networkClient(){
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://dream-and-develop.tistory.com");
            return networkClient;
        }
    }

 

2번 방법은 메서드 이름을 자유롭게 지을 수 있고, 스프링 빈이 스프링 코드에 의존하지 않는다.

코드가 아니라 설정 정보를 사용하기 때문에, 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용 가능하다.

(이때, @Bean의 destroyMethod 속성에는 기본 값이 '(inferred)' (추론)으로 등록되어 있어, close나 shutdown이라는 이름의 종료 메서드를 자동으로 호출한다. 따라서 종료 메서드는 따로 적어주지 않아도 잘 동작한다.)

 

 


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

마지막으로는 설정 정보를 작성하는 대신, 간단하게 @PostConstruct, @PreDestroy 애노테이션을 명시하는 방법이다. (최신 스프링에서 권장하는 방법이므로, 이 방법을 사용하도록 하자..!)

 

기존의 @Bean 애노테이션의 initMethod, destroyMethod 속성을 제거하고,

다음과 같이 init과 close 메서드 위에 @PostConstruct, @PreDestroy 애노테이션을 추가해주자.

 

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

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

 

728x90
반응형
Comments