[TIL] _ 260316 _ 빈의 생명주기를 배우다

호두·2026년 3월 17일

✔️ Java / Spring

목록 보기
4/25

✅ 빈 생명주기 콜백

우리가 프로그램을 서버와 데이터베이스를 연결해서 서비스를 시작할 때 미리 연결을 해둬야하는 것들이 있다. 데이터베이스 커넥션 풀, 네트워크 소켓 처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 이 연결을 종료하는 작업이 필요하다. 이걸 객체의 초기화 콜백, 소면전 콜백 이라고 한다.


자바 코드 + 서버(스프링) + DB


서비스를 딱 시작할 때 서비스에 필요한 것들을 준비하면 안된다. 필요한 것들을 다 준비한 후에 서비스를 시작해야 오류가 발생하지 않고 제대로 사용할 수 있다.

스프링 빈 클래스

package hello.core.lifecycle;


import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

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"); // Setter 로 url 주입해서 생성시에 null
            return networkClient;
        }
    }
}

테스트 진행 과정

: 스프링 빈을 생성하고 setter 로 url 의 값을 넣어주고는 스프링 컨테이너에 보관한다. 해당 스프링 빈을 생명주기 제어기능도 가진 ConfigurableApplicationContext 를 사용해서 서비스 종료까지 테스트 했다.


➡️ 설정파일(LifeCycleConfig) 빈에서 도메인을 생성자 호출하면 생성자 안의 코드가 실행된다. 생성자 호출 시 url 값, 서비스 연결에 필요한 값 준비 시작, 초기화 완료 알람 을 실행한다.

➡️ 생성자 안의 url은 null 로 출력이 된다. 왜냐하면 LifeCycleConfig 에서 생성 후에 setter 로 주입해주기 때문이다. 또 connect() 도 아직 url 주입 전이라 null 이 출력된다. 즉 연결 준비가 안된 상태에서 서비스를 시작하게 된다. (🚨 예외 발생)


스프링 빈의 라이프 사이클 🌀

스프링 빈은 2가지 라이프 사이클이 존재한다.

  • 객체 생성
  • 의존 관계 주입

중요한 건 객체가 생성이 완료된 후에 ‼️ 의존관계가 주입된다는 점이다.

위의 코드에서 의존관계로 주입 되는 값이 필요한 초기화 단계를 의존관계가 주입되기 전에 호출해서 null 이 나왔다.

그래서 우리는 객체가 생성되고 의존관계가 다 주입이 된 후에, 서비스를 배포하기 위해 필요한 값들을 준비(초기화) 해야 한다. 우리가 의존관계가 완료된 시점을 알 수 있도록 해주는 게 바로 ✔️초기화 콜백 이다.

스프링이 콜백 메서드를 통해서 다양한 기능들을 제공한다. 또 스프링 컨테이너가 종료되기 직전에 ✔️소멸 콜백을 준다. 그래서 우리는 안전하게 서비스를 종료할 수 있다.




자바 코드와 초기화 기능

객체 초기화 라는 개념이 제대로 안 잡혀 있는 상태였어서 객체 생성이랑 왜 따로 인지 이게 왜 필요한 지 이해할 수 없었다. 그래서 딱 정리를 하자면,

자바 코드는 서비스에 사용되는 비지니스 로직들을 구현하는 일을 한다. 그리고 초기화는 객체와 그 안의 값들과 관련있는 것이 아니라! 그 외에 우리가 서비스를 배포할 때 준비해둬야 하는 여러가지 설정값 (DB 연결 시, 네트워크 연결 시, 파일 열기 등) 을 세팅 해두는 것이다.

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



✅ 인터페이스 InitiallizingBean, DisposableBean


스프링에서는 쉽게 콜백 메서드를 사용할 수 있도록 인터페이스를 제공한다. 초기화 콜백에는 InitializingBean, 종료 콜백에는 DisposableBean 을 사용한다. 이 방식은 아주 예전 방식으로 지금은 잘 사용하지 않는다.

package hello.core.lifecycle;


import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
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 {
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메세지");
    }

    @Override // ✅ 추가
    public void destroy() throws Exception {
        System.out.println("NetworkClient.close");
        disconnect();
    }
}

➡️ destroy()는 초기화 콜백 메서드, afterPropertiesSet()는 종료 콜백 메서드다. 각 인터페이스가 제공하는 메서드를 오버라이딩 했다.

➡️ 이제 테스트를 돌려보면 스프링이 제공하는 두 인터페이스 덕분에 초기화 시점, 종료 시점에 자동으로 콜백 메서드가 실행된다. 이제 객체 생성하고 의존관계 주입(url) 후에 초기화가 실행되면서 connect()와 disconnect() 의 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 (initMethod = "init", destroyMethod = "close") // ✅ 메서드 이름 지정!
        public NetworkClient networkClient() {
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://hello-spring.dev"); 
            return networkClient;
        }
    }
}

    ...

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

➡️ @Bean 에 초기화, 소멸 메서드 이름을 저렇게 설정 정보로 적어주면 스프링이 이 이름을 보고 매칭해서 실행한다.


❓ 이건 스프링 의존적이지 않은가?

설정정보로 매칭하는 기능은 스프링 뿐만 아니라 대부분의 프레임워크에서 제공하는 기본적인 기능이라 스프링에 의존적이지 않다.


✅ @Bean 의 소멸 메서드 속성에는 특별한 기능이 있다. 기본적으로 inferred(추론) 으로 등록되어있어서 소멸 관련 이름인 close, shutdown 라는 이름의 메서드를 자동으로 호출해준다. 그래서 종료 메서드의 경우에는 따로 설정정보를 적어주지 않아도 잘 작동 한다.




✅ 애노테이션 @PostConstruct, @PreDestroy


이 두개의 애노테이션을 초기화, 소멸 메서드에 붙이면 가장 편리하게 초기화와 종료를 실행할 수 있다. 실무에서 사용하는 방법이다.

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

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

➡️ javax.annotation.PostConstruct 로 스프링이 아니라 자바에서 제공하는 기능이라 스프링이 아닌 다른 프레임워크에서도 동작한다.

➡️ 단점은 외부 라이브러리에는 적용하지 못한다는 점이다. 외부 라이브러리를 초기화, 종료 해야할 때는 @Bean의 기능을 사용하자.

0개의 댓글