Spring

빈 생명 주기 콜백

dev-rootable 2023. 5. 11. 11:49

❓ 왜 빈 생명 주기를 알아야 할까

 

예를 들어, 우리가 보통 데이터베이스 또는 Java의 I/O 자원을 사용할 때, 애플리케이션 시작 시점에 초기화를 통해 자원을 받아 오고 종료 시점에 그 자원을 반납한다. 스프링도 마찬가지로 외부에서 자원을 갖고 와서 사용한다면 이러한 초기화와 종료 작업이 필요하다.

 

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

 

public class BeanLifeCycleTest {
    
    @Test
    public void lifeCycleTest() throws Exception{
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        NetworkClient client = ac.getBean(NetworkClient.class);
        ac.close(); //스프링 컨테이너를 종료, ConfigurableApplicationContext 필요
    }

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

}

 

위 코드는 생성자에서 초기화 작업을 진행하는데, url 의 값이 모두 null 이 나온다. 그 이유는 스프링 빈의 라이프사이클을 고려하지 않고 코드를 작성했기 때문이다.

 

스프링 빈은 크게 객체 생성 ➡ 의존 관계 주입 이라는 라이프사이클을 가진다. 단, 생성자 주입은 객체 생성 시점에 완료된다. 그래서 객체를 생성하고 의존 관계 주입이 끝난 다음에야 필요한 데이터를 사용할 수 있는 준비가 완료된다. 따라서 의존 관계 주입이 끝난 후에 초기화 작업을 해야 한다. 그렇다면 우리는 이 시점을 어떻게 알 수 있을까

 

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

 

출처:스프링 핵심 원리 - 기본편

스프링은 의존 관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해 초기화 콜백을 주고, 스프링 컨테이너 종료 직전에 소멸전 콜백을 준다. 이를 통해 개발자는 초기화 작업과 종료 전 수행해야 할 작업을 적절한 시점에 수행되도록 할 수 있다.

 

❓ 왜 생성자와 초기화 작업을 분리해야 할까

 

생성자는 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임을 가지는 반면 초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는 무거운 동작을 수행한다. 그래서 서로 역할이 다르기 때문에 유지 보수 관점에서 둘을 분리하는 것이 바람직하다. 단, 초기화 작업이 내부 값들을 변경하는 정도의 단순한 작업이라면 생성자에서 한번에 처리하는 것이 나을 수 있다.

 

📌 빈 생명 주기 라이프사이클에 따른 초기화 및 종료 작업

 

@PostConstruct : 초기화 콜백

@PreDestroy : 소멸전 콜백

 

특징

  • 스프링에서 가장 권장하는 방법으로 애노테이션만 붙이면 되므로 매우 편리하다.
  • 메서드 이름을 자유롭게 줄 수 있다.
  • 스프링에 종속적인 기술이 아닌 JSR-250이라는 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 동작한다.
  • 외부 라이브러리에는 적용하지 못하므로 외부 라이브러리에 적용하고 싶다면 아래 챕터의 방법을 이용하자

테스트 코드는 동일하다.

 

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 shutdown() {
        System.out.println("NetworkClient.close");
        disconnect();
    }
    
}

정상적으로 콜백 메서드가 호출된 결과

📌 빈 생명 주기 라이프사이클에 따른 초기화 및 종료 작업 - 외부 라이브러리

 

@Bean의 initMethod와 destroyMethod 를 활용한 방법

 

initMethod를 통해 초기화용 메서드를 지정하고, destroyMethod를 통해 종료 작업용 메서드를 지정하면 각각 콜백 시기에 작업이 수행된다.

 

특징

  • 메서드 이름을 자유롭게 줄 수 있다.
  • 스프링 빈이 스프링 코드에 의존하지 않는다.
  • 코드가 아닌 설정 정보를 이용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 적용할 수 있다.
@Bean의 destroyMethod 속성은 기본값이 inferred(추론)으로 되어 있는데, 이 기능은 종료 메서드 이름을 close 또는 shotdown으로 하면 속성 설정을 생략해도 자동으로 호출해준다. 이 기능을 사용하고 싶지 않다면 빈 값("")으로 설정을 명시하면 된다.

 

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 shutdown() {
        System.out.println("NetworkClient.close");
        disconnect();
    }
    
}

 

public class BeanLifeCycleTest {
    
    @Test
    public void lifeCycleTest() throws Exception{
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        NetworkClient client = ac.getBean(NetworkClient.class);
        ac.close(); //스프링 컨테이너를 종료, ConfigurableApplicationContext 필요
    }

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

}

 

 

Reference:

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com