스프링을 사용하다 보면 “초기화 작업”과 “종료 작업”이 필요한 빈들이 존재한다.
대표적인 예시는 다음과 같다.
- DB 커넥션 풀
- 메시지 브로커(Kafka, RabbitMQ) 연결
- 외부 API 서버 연결
- WebSocket 서버 초기화
- 파일 핸들, 스트림, 캐시 연결 등
이런 객체는 단순히 “new 해서 쓰는 것”으로 끝나지 않는다.
생성 이후 반드시 준비해야 할 동작이 있고, 애플리케이션 종료 시 반드시 정리해야 할 동작이 있다.
스프링은 이를 빈 생명주기 콜백(Bean Lifecycle Callback) 기능으로 제공한다.
1. 스프링 빈 생명주기는 왜 필요한가?
DB 커넥션을 예로 들어보자.
- 애플리케이션 시작 시 DB 연결을 미리 열어두고
- 사용 중인 객체가 모두 끝난 후 안전하게 연결을 닫아야 한다
즉, 객체는 단순히 생성만 해서는 안 되며
“생성 → 의존관계 주입 → 초기화 → 사용 → 종료”의 과정이 필요하다.
스프링은 이 전체 과정을 전문가 수준으로 관리한다.
2. 스프링 빈의 전체 생명주기 흐름
스프링 빈은 다음 순서로 동작한다.
- 스프링 컨테이너 생성
- 스프링 빈 생성 (생성자 호출)
- 의존관계 주입(@Autowired / setter 주입 등)
- 초기화 콜백 실행 (@PostConstruct 등)
- 빈 사용
- 컨테이너 종료
- 소멸 콜백 실행 (@PreDestroy 등)
초기화 시점은 “의존관계 주입이 끝난 후”, 소멸 시점은 “컨테이너 종료 직전”이다.
3. 생성자에서 초기화하면 왜 안 되는가?
public class NetworkClient {
private String url;
public NetworkClient() {
System.out.println("생성자 호출, url = " + url);
connect();
}
public void setUrl(String url) {
this.url = url;
}
}
스프링 빈 생성 순서를 보면:
- 생성자 호출
- 의존관계 주입(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)보다 우선되는 이유
스프링은 다음 우선순위로 초기화 메서드를 호출한다.
- @PostConstruct
- InitializingBean.afterPropertiesSet()
- @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가 처리
- 외부 리소스를 다루는 빈은 반드시 생명주기 콜백 구현 필요
- 이 개념을 이해하면 스프링의 동작 순서와 컨테이너 구조가 명확해진다
빈 생명주기 콜백은 “알아두면 좋다” 수준이 아니라 스프링 안정성의 핵심 원리라고 생각하면 된다.