INFLEARN

[스프링 핵심 원리 - 기본편] 16. 빈 생명주기 콜백

ch010104 2026. 3. 24. 15:47

1. 빈 생명주기 콜백의 필요성

데이터베이스 연결이나 네트워크 소켓처럼 시작 시 연결하고 종료 시 끊어야 하는 객체는 초기화와 종료 작업이 필수입니다.

문제 상황: 생성자에서 초기화 시도

package com.example.spring_study.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);
    }

    // connect가 된 상태에서 call을 부를 수 있다고 가정
    public void call(String message){
        System.out.println("call: " + url + ", message: " + message);
    }

    // 서비스 종료시 호출하는 메서드
    public void disconnect() {
        System.out.println("close: " + url);
    }
}
  • 결과: url이 null인 상태로 connect()가 호출됨. (생성자 호출 시점에는 의존관계 주입이 안 되었기 때문)
  • 해결: 객체 생성과 초기화를 분리하고, 의존관계 주입이 완료된 후 호출되는 콜백을 사용해야 함.

2. 방법 1: 인터페이스 (InitializingBean, DisposableBean)

스프링 초창기에 사용하던 방법으로, 인터페이스의 메서드를 오버라이딩합니다.

[전체 코드: NetworkClient.java]

package com.example.spring_study.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);
    }

    // connect가 된 상태에서 call을 부를 수 있다고 가정
    public void call(String message){
        System.out.println("call: " + url + ", message: " + message);
    }

    // 서비스 종료시 호출하는 메서드
    public void disconnect() {
        System.out.println("close: " + url);
    }

    @Override
    // InitializingBean 부모에서 상속
    public void afterPropertiesSet() throws Exception { // 의존관계 주입이 끝나면 호출해 주겠다는 뜻
        // 생성자에서는 진짜 생성만 하고, 초기화하는 과정을 분리함
        System.out.println("NetworkClient.afterPropertiesSet");
        connect();
        call("초기화 생성 메시지");
    }

    @Override
    // DisposableBean 부모에서 상속
    public void destroy() throws Exception { // Bean이 종료될 때 호출
        System.out.println("NetworkClient.destroy");
        disconnect();
    }
}
  • 특징: 스프링 전용 인터페이스에 의존함. 메서드 이름 변경 불가. 외부 라이브러리 적용 불가.

3. 방법 2: 빈 등록 시 초기화, 소멸 메서드 지정

설정 정보(@Bean)에 직접 메서드 이름을 지정하는 방식입니다.

[전체 코드: NetworkClient.java & Config]

package com.example.spring_study.lifecycle;

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

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

    // connect가 된 상태에서 call을 부를 수 있다고 가정
    public void call(String message){
        System.out.println("call: " + url + ", message: " + message);
    }

    // 서비스 종료시 호출하는 메서드
    public void disconnect() {
        System.out.println("close: " + url);
    }

    public void init() throws Exception { // 의존관계 주입이 끝나면 호출해 주겠다는 뜻
        // 생성자에서는 진짜 생성만 하고, 초기화하는 과정을 분리함
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 생성 메시지");
    }

    public void close() throws Exception { // Bean이 종료될 때 호출
        System.out.println("NetworkClient.close");
        disconnect();
    }
}
package com.example.spring_study.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(); // 컨테이너가 종료되면서, close()가 호출됨
    }

    @Configuration
    static class LifeCycleConfig {

        @Bean(initMethod = "init", destroyMethod = "close") // NetWorkClient의 메소드 중에서 초기화와 종료 메소드를 지정
        // -> 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있음
        // Bean이 완전히 등록된 후에 "init()이 호출됨

        // destroyMethod는 (inferred) 추론으로 등록되어 있어서, 종료 메서드는 따로 적어주지 않아도 @Bean으로 등록할 시에는 'close`, 'shutdown' 라는 이름의 메서드를 자동으로 호출해줌
        // -> 즉, 이름 그대로 종료 메서드를 추론해서 호출(일반적으로 외부라이브러리의 경우, close, shudown을 종료 메서드 이름임)
        // -> 이 추론 기능을 사용하기 싫으면 destroyMethod = ""로 지정하면 됨
        public NetworkClient networkClient() {
            // 객체의 생성과 초기화는 분리하는 것이 좋다
            // 생성자는 필수 정보(파라미터)를 받고, 메모를 할당해서 객체를 생성하는 책임을 가짐
            // 반면, 초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는 무거운 동작을 수행
            // -> 이러한 무거운 동작을 생성자에서 하는 것보다 이를 분리하는 것이 유지보수 관점에서 좋음
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("<http://hello-spring.dev>");
            return networkClient;
        }
    }
}
  • 특징: 스프링 코드에 의존 안 함. 코드를 고칠 수 없는 외부 라이브러리에 사용 가능.
  • 종료 메서드 추론: destroyMethod를 생략해도 close, shutdown 이름의 메서드를 자동으로 찾아서 호출해줌.

4. 방법 3: @PostConstruct, @PreDestroy (권장)

가장 편리하고 최신 스프링에서 강력하게 권장하는 표준 방식입니다.

[전체 코드: NetworkClient.java]

package com.example.spring_study.lifecycle;

import jakarta.annotation.PostConstruct;
import jakarta.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);
    }

    // connect가 된 상태에서 call을 부를 수 있다고 가정
    public void call(String message){
        System.out.println("call: " + url + ", message: " + message);
    }

    // 서비스 종료시 호출하는 메서드
    public void disconnect() {
        System.out.println("close: " + url);
    }

    // 이 방법은 외부 라이브러리에는 적용하지 못함
    @PostConstruct // 생성자 이후에 호출한다는 뜻
    public void init() throws Exception { // 의존관계 주입이 끝나면 호출해 주겠다는 뜻
        // 생성자에서는 진짜 생성만 하고, 초기화하는 과정을 분리함
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 생성 메시지");
    }

    @PreDestroy // 종료 이전에 호출한다는 뜻
    public void close() throws Exception { // Bean이 종료될 때 호출
        System.out.println("NetworkClient.close");
        disconnect();
    }
}
  • 특징: 자바 표준(JSR-250)이라 스프링이 아닌 곳에서도 동작. 컴포넌트 스캔과 잘 어울림.
  • 유일한 단점: 외부 라이브러리에는 적용 못 함 (코드 수정을 못 하니까).

5. 결론: 무엇을 써야 할까?

  1. 기본적으로 @PostConstruct, @PreDestroy를 사용하자.
  2. 코드를 고칠 수 없는 외부 라이브러리를 종료해야 하면 @Bean(initMethod, destroyMethod)를 사용