본문 바로가기

Spring Study

[스프링 핵심 원리] - 기본편 #8

섹션 9 : 빈 생명주기 콜백

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

 

스프링 빈

객체 생성 -> 의존관계 주입

따라서 초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출한다. 

 

스프링 빈의 이벤트 라이프사이클

스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입  -> 초기화 콜백  -> 사용 -> 소멸 전 콜백 -> 스프링 종료

 

초기화 콜백 : 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출

소멸 전 콜백 : 빈이 소멸되기 직전에 호출

 

스프링은 의존관계 주입이 완료되면 초기화 콜백을 실행하고, 컨테이너가 종료되기 전에는 소멸 콜백을 실행한다.

=> 안전하게 종료 작업을 진행할 수 있다. 

 

객체의 생성과 초기화를 분리하는 것이 좋다. 

 

생성자 → 필수 정보(파라미터)를 받고, 메모리를 할당하는 역할

초기화 메서드 → 생성된 값들을 활용해 외부 커넥션 연결 등 무거운 작업을 수행

 

 

 

객체 생성과 초기화를 분리하면 유지보수성이 향상된다.

즉, 생성자는 객체를 만드는 역할만 하고, 초기화 메서드는 필요한 동작을 수행하는 역할을 하도록 설계하는 것이 바람직하다.

 

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

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

인터페이스 

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 disconnet(){
        System.out.println("close: " + url);
    }

    // 의존관계 주입이 끝나면 호출
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("NetworkClient.afterPropertiesSet");
        connect();
        call("초기화 연결 메시지");
    }

    // 빈이 종료될 때 호출
    // 컨테이너가 내려가서 종료하기 전에 싱글톤 빈들이 하나씩 죽음
    @Override
    public void destroy() throws Exception {
        System.out.println("NetworkClient.destroy");
        disconnet();
    }
}

 

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

 

 

BeanLifeCycleTest 코드를 실행하면

 

위의 사진과 같은 결과가 나온다. 

 

NetworkClient 객체가 생성될 때 생성자가 호출되며 "생성자 호출, url = null"이 출력된다.

이때 url이 아직 설정되지 않았기 때문에 null이 출력된다.

⬇️

스프링이 의존관계 주입을 완료한 뒤 afterPropertiesSet() 메서드를 호출하며 "NetworkClient.afterPropertiesSet"이 출력된다.

이 시점에서 setUrl("http://hello-spring.dev")이 실행되어 url 값이 설정된다.

⬇️

그다음 connect()가 실행되면서 "connect: http://hello-spring.dev"이 출력되고, call("초기화 연결 메시지")가 실행되어

"call: http://hello-spring.dev message = 초기화 연결 메시지"가 출력된다.

⬇️

마지막으로 스프링 컨테이너가 종료되면서 destroy()가 호출되고 "NetworkClient.destroy"가 출력된다.

이후 disconnect()가 실행되면서 "close: http://hello-spring.dev"이 출력된다.

 

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

  • 이 인터페이스는 스프링 전용 인터페이스라, 해당 코드가 스프링 전용 인터페이스에 의존한다.
  • 초기화, 소멸 메서드의 이름을 변경할 수 없다.
  • 외부 라이브러리에 적용할 수 없다.

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

설정 정보에 @Bean(initMethod = "init", destroyMethod = "close")처럼 초기화, 소멸 메서드를 지정할 수 있다.

 

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

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

이렇게 함수를 직접 구현한 후, 초기화 소멸 메서드를 직접 지정하는 방식도 있다. 

 

설정 정보를 사용하는 것의 장점

  • 메서드 이름을 자유롭게 줄 수 있다.
  • 스프링 빈이 스프링 코드에 의존하지 않는다.
  • 코드가 아니라 설정 정보를 사용하기 때문에 외부 라이브러리에도 최고화, 종료 메서드를 적용할 수 있다.

종료 메서드의 추론 

@Bean(initMethod = "init", destroyMethod = "close")

라이브러리는 대부분 close, shutdown 이라는 종료 메서드를 사용한다.

 

@Bean destroyMethod는 기본값이 (inffered)(추론)으로 등록되어 있다.

이 기능은 말 그대로 close, shutdown라는 이름의 종료 메서드를 추론해서 호출해준다.

따라서 직접 스프링 빈을 등록하면 종료 메서드를 따로 적어주지 않아도 잘 동작한다.

 

애노테이션 @PostConstruct, @PreDestory 

 

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

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

 

 

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

최신 스프링에서 가장 권장하는 방법이다.

 

  • 애노테이션 하나만 붙이면 되므로 매우 편리하다.
  • 패키지를 잘 보면 `javax.annotation.PostConstruct` 이다. 스프링에 종속적인 기술이 아니라 JSR-250 라는 자바 표준이다. 따라서 스프링이 아닌 다른 컨테이너에서도 동작한다.
    • 외부 라이브러리에는 적용하지 못한다. 외부 라이브러리를 초기화, 종료 해야 하면 @Bean의 기능(init 메서드를 사용해야한다.)