8장. 스프링 빈 생명주기 콜백 완전 정리

스프링을 사용하다 보면 “초기화 작업”과 “종료 작업”이 필요한 빈들이 존재한다.

대표적인 예시는 다음과 같다.

  • DB 커넥션 풀
  • 메시지 브로커(Kafka, RabbitMQ) 연결
  • 외부 API 서버 연결
  • WebSocket 서버 초기화
  • 파일 핸들, 스트림, 캐시 연결 등

이런 객체는 단순히 “new 해서 쓰는 것”으로 끝나지 않는다.

생성 이후 반드시 준비해야 할 동작이 있고, 애플리케이션 종료 시 반드시 정리해야 할 동작이 있다.

스프링은 이를 빈 생명주기 콜백(Bean Lifecycle Callback) 기능으로 제공한다.


1. 스프링 빈 생명주기는 왜 필요한가?

DB 커넥션을 예로 들어보자.

  • 애플리케이션 시작 시 DB 연결을 미리 열어두고
  • 사용 중인 객체가 모두 끝난 후 안전하게 연결을 닫아야 한다

즉, 객체는 단순히 생성만 해서는 안 되며

“생성 → 의존관계 주입 → 초기화 → 사용 → 종료”의 과정이 필요하다.

스프링은 이 전체 과정을 전문가 수준으로 관리한다.


2. 스프링 빈의 전체 생명주기 흐름

스프링 빈은 다음 순서로 동작한다.

  1. 스프링 컨테이너 생성
  2. 스프링 빈 생성 (생성자 호출)
  3. 의존관계 주입(@Autowired / setter 주입 등)
  4. 초기화 콜백 실행 (@PostConstruct 등)
  5. 빈 사용
  6. 컨테이너 종료
  7. 소멸 콜백 실행 (@PreDestroy 등)

초기화 시점은 “의존관계 주입이 끝난 후”, 소멸 시점은 “컨테이너 종료 직전”이다.


3. 생성자에서 초기화하면 왜 안 되는가?

public class NetworkClient {
    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
        connect();
    }

    public void setUrl(String url) {
        this.url = url;
    }
}

스프링 빈 생성 순서를 보면:

  1. 생성자 호출
  2. 의존관계 주입(setUrl)

즉, 생성자 내부에서 connect()를 호출하면

url이 아직 null이다.

그래서 스프링은 “초기화 콜백”을 제공한다.

의존관계 주입이 모두 끝난 후 실행되도록 하기 위함이다.


4. 스프링이 제공하는 초기화/소멸 처리 방식 3가지

스프링은 빈의 초기화·종료를 위한 3가지 기술을 제공한다.

방식특징장점단점
InitializingBean / DisposableBean인터페이스 구현코드 간단스프링에 종속
@Bean(initMethod, destroyMethod)설정 파일에 명시외부 라이브러리 적용 가능메서드 이름 강제
@PostConstruct / @PreDestroy자바 표준 어노테이션가장 깔끔, 스프링 권장외부 라이브러리에 직접 적용 불가

스프링의 공식 권장 방식은 @PostConstruct / @PreDestroy 다.


5. 기술 심층 Part ①

스프링이 “초기화/소멸”을 실제로 호출하는 내부 구조

여기부터는 기술적으로 반드시 알아야 하는 핵심 영역이다.

초기화/소멸 콜백을 실제로 실행하는 핵심 컴포넌트는 다음과 같다.

핵심 클래스: BeanPostProcessor

두 가지 대표 콜백 포인트가 있다.

  • postProcessBeforeInitialization()
  • postProcessAfterInitialization()

스프링 컨테이너는 빈 생성 이후, 초기화 콜백을 실행하기 전과 후에 BeanPostProcessor를 거치도록 설계되어 있다.

스프링은 초기화 과정에서 다음 알고리즘으로 동작한다:


초기화 전체 흐름 (실제 스프링 내부 프로세스)

다음 코드는 스프링 내부 동작을 개념적으로 표현한 것이다.

createBean() {
    // 1. 생성자 호출로 빈 생성
    Object bean = instantiation();

    // 2. 의존관계 주입
    populateBean(bean);

    // 3. BeanPostProcessor - 전처리
    applyBeanPostProcessorsBeforeInitialization(bean);

    // 4. 초기화 메서드 호출(@PostConstruct 등)
    invokeInitMethods(bean);

    // 5. BeanPostProcessor - 후처리
    applyBeanPostProcessorsAfterInitialization(bean);

    return bean;
}

특히 3번과 5번 사이에 스프링이

@PostConstruct 메서드를 호출한다.

이 과정을 가능하게 해주는 것이

CommonAnnotationBeanPostProcessor라는 BeanPostProcessor 구현체다.


소멸 전체 흐름

스프링 컨테이너 종료 시:

destroyBean(bean) {
    // DisposableBean.destroy() 호출
    // @PreDestroy 호출
    // destroyMethod 호출
}

이 소멸 호출을 담당하는 핵심 클래스는 DisposableBeanAdapter다.

이 내부에는 @PreDestroy가 있으면 우선 실행하도록 구현되어 있다.


6. 기술 심층 Part ②

@PostConstruct / @PreDestroy가 @Bean(initMethod)보다 우선되는 이유

스프링은 다음 우선순위로 초기화 메서드를 호출한다.

  1. @PostConstruct
  2. InitializingBean.afterPropertiesSet()
  3. @Bean(initMethod)

왜 그럴까?

그 이유는 다음과 같다.

1) @PostConstruct는 자바 표준(JSR-250)

→ 스프링이 아닌 다른 컨테이너(예: JavaEE)에서도 동일하게 동작

2) BeanPostProcessor 단계에서 처리 가능

@PostConstruct는 BeanPostProcessor 레벨에서 처리되기 때문에 스프링이 빈 초기화를 매우 유연하게 제어할 수 있다.

3) 외부 라이브러리와의 충돌 방지

외부 라이브러리들이 초기화 메서드를 가질 수 있기 때문에 표준 방식인 @PostConstruct를 먼저 실행한다.

이것이 “무조건 @PostConstruct를 쓰는 것이 좋은 이유”다.


7. 실제 실무에서 자주 사용하는 초기화/종료 예시

1) HikariCP(커넥션 풀)

초기화: 연결 풀 미리 세팅

종료: 모든 커넥션 안전 종료

2) Kafka Listener

초기화: consumer 시작

종료: consumer close

3) Redis Connection

초기화: TCP 연결 생성

종료: 연결 반납

4) 외부 API Client

초기화: 토큰 발급

종료: refresh token 저장

5) WebSocket Server

초기화: 소켓 포트 리스닝

종료: 모든 세션 종료

이런 객체들은 초기화/종료 작업을 반드시 구현해야 한다.


8. 소멸 콜백이 정말 중요한 이유

초기화보다 더 중요한 경우도 있다.

  • DB 커넥션 미반납 → 커넥션 누수
  • Redis 세션 미반납 → Memory leak
  • 파일 리소스 미반납 → OS File Descriptor 부족
  • Message Queue consumer 미종료 → 장애 전파

그래서 @PreDestroy는 절대 무시하면 안 된다.


9. 결론: 빈 생명주기는 스프링의 안정성과 직결되는 핵심 원리다

마지막으로 핵심만 정리하면:

  • 스프링은 빈의 생성 → 주입 → 초기화 → 종료까지 완벽하게 관리
  • 초기화/종료는 @PostConstruct / @PreDestroy가 표준
  • 내부적으로 BeanPostProcessor와 DisposableBeanAdapter가 처리
  • 외부 리소스를 다루는 빈은 반드시 생명주기 콜백 구현 필요
  • 이 개념을 이해하면 스프링의 동작 순서와 컨테이너 구조가 명확해진다

빈 생명주기 콜백은 “알아두면 좋다” 수준이 아니라 스프링 안정성의 핵심 원리라고 생각하면 된다.