자바 백엔드 입문 21편. Bean이 태어나서 죽을 때까지 5단계 생명주기와 @PostConstruct·@PreDestroy 콜백을 입사부터 퇴사까지 비유로 풀어쓴 Phase 2 IoC 컨테이너 마지막 글.
이 글은 자바 백엔드 입문 시리즈 59편 중 21편이에요. Phase 2 IoC 컨테이너의 마지막 글입니다. 20편에서 "Bean이 몇 개 만들어지는가" 를 잡았다면, 이번 21편은 "Bean이 태어나서 죽을 때까지 어떤 단계를 거치는가" 를 깊이 들여다봅니다.
Bean 생명주기가 헷갈리는 이유
처음 "Bean 생명주기" 단어를 들으면 "객체가 생명 같은 게 있나?" 가 어색해요. 또 @PostConstruct·@PreDestroy·InitializingBean·DisposableBean 같이 비슷한 이름의 콜백이 여러 개 있는 게 헷갈리고요.
이 글에서는 입사부터 퇴사까지 비유로 풀어요. Bean = 회사 직원. 인스턴스화 = 채용 합격, DI = 부서 배치 + 장비 지급, 초기화 = 입사 교육, 사용 = 근무, 소멸 = 퇴사 처리. 끝까지 따라오시면 5단계 + 4가지 콜백이 한 그림에 들어와요.
Bean 생명주기 5단계
Spring Bean 한 개는 다음 5단계를 거쳐요.
[1] 인스턴스화 (new로 객체 생성 — 채용 합격)
↓
[2] 의존성 주입 (다른 Bean 끼워 들어감 — 부서 배치)
↓
[3] 초기화 콜백 (@PostConstruct 실행 — 입사 교육)
↓
[4] 사용 (애플리케이션 동작 중 — 근무)
↓
[5] 소멸 콜백 (@PreDestroy 실행 — 퇴사 인수인계)
Spring이 이 5단계를 다 자동 처리해요. 우리가 하는 일은 3단계와 5단계에 우리 코드를 끼워 넣는 것뿐. 그게 @PostConstruct 와 @PreDestroy 의 역할이에요.
@PostConstruct — 입사 교육
@PostConstruct 는 "이 메서드를 객체 생성 + 의존성 주입 끝난 직후 한 번만 호출해 줘" 메모예요.
@Service
public class OrderService {
private final PaymentGateway paymentGateway;
public OrderService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
@PostConstruct
public void init() {
// 의존성 다 주입된 후 한 번만 실행
System.out.println("OrderService 준비 완료, paymentGateway = " + paymentGateway);
paymentGateway.warmUpConnection(); // 미리 커넥션 풀 채우기 같은 초기 작업
}
}
비유로 풀면 — 신입 직원이 채용 합격(1단계) + 부서 배치 + 사번·노트북 지급(2단계) 끝난 직후, 입사 교육 한 번 받는 거예요. 본격적인 업무 시작 전 "이 일을 잘하려면 미리 준비해둘 것" 을 처리하는 단계.
자주 쓰이는 시나리오: - 초기 데이터 로딩 — DB에서 설정값 미리 읽어 메모리에 캐싱 - 외부 연결 워밍업 — Redis·Kafka 커넥션 미리 채워두기 - 검증 — 필수 설정이 정상적인지 확인, 비정상이면 즉시 예외 던져 시작 차단
@PreDestroy — 퇴사 인수인계
@PreDestroy 는 "이 메서드를 컨테이너 종료 직전 한 번만 호출해 줘" 메모예요.
@Service
public class OrderService {
@PreDestroy
public void cleanup() {
System.out.println("OrderService 정리 중...");
// 자원 정리·연결 종료·로깅·캐시 비우기 등
}
}
비유 — 직원이 퇴사하기 직전 인수인계 + 사물 정리 시간. 본인이 만들어둔 자원(파일·연결·캐시)을 다른 사람이 이어받거나 정리할 수 있도록 마무리하는 단계.
자주 쓰이는 시나리오: - 외부 자원 정리 — DB 커넥션·Redis 연결·파일 핸들 닫기 - 버퍼 flush — 메모리에 있는 로그·메트릭을 디스크로 내보내기 - 종료 알림 — "우리 서비스가 종료 중" 을 모니터링 시스템에 통지
여기서 시험 함정 자주 나와요. "@PreDestroy 는 Singleton·Prototype 둘 다 자동 호출된다" 가 보기로 나오면 X. 20편에서 짚었듯 Prototype Bean은 @PreDestroy 자동 호출 X. Singleton만 컨테이너가 끝까지 관리하니까 종료 시 콜백을 부르는 거예요.
4가지 콜백 방식 — 같은 일, 다른 방법
Spring은 초기화·소멸 콜백을 박는 방법을 4가지 지원해요. 같은 일을 다른 방식으로.
| 방식 | 초기화 | 소멸 | 비고 |
|---|---|---|---|
| 1. 어노테이션 | @PostConstruct | @PreDestroy | 가장 권장. 표준 자바 어노테이션 |
| 2. 인터페이스 | InitializingBean.afterPropertiesSet() | DisposableBean.destroy() | Spring에 강하게 결합 — 비권장 |
| 3. @Bean 옵션 | @Bean(initMethod="...") | @Bean(destroyMethod="...") | 외부 라이브러리 클래스에 유용 |
| 4. XML 설정 | <bean init-method="..."> | <bean destroy-method="..."> | 레거시 XML 환경 |
현대 Spring 표준 = 어노테이션 (@PostConstruct·@PreDestroy). 자바 표준이라 Spring 외 환경에서도 통용. 외부 라이브러리 클래스(우리 코드 아닌 것)는 3번 @Bean(destroyMethod = "close") 패턴.
우선순위 — 같은 Bean에 여러 방식 박혀 있으면?
한 Bean에 어노테이션 + 인터페이스 + @Bean 옵션이 다 박혀 있으면? Spring이 다음 순서대로 호출해요.
초기화 시 — @PostConstruct → InitializingBean.afterPropertiesSet() → @Bean initMethod
소멸 시 — @PreDestroy → DisposableBean.destroy() → @Bean destroyMethod
여러 방식 동시에 박는 건 권장 X. 코드 읽기 어려워지고 호출 순서 혼란. 한 방식만 골라 일관되게 사용.
@PostConstruct 다음에 일어나는 일
@Component
public class OrderService {
@Autowired private PaymentGateway gateway;
public OrderService() {
System.out.println("1. 생성자 — gateway = " + gateway); // gateway = null (아직 주입 전)
}
@PostConstruct
public void init() {
System.out.println("2. @PostConstruct — gateway = " + gateway); // gateway = 주입됨
}
}
핵심 사실 — 생성자 안에서는 @Autowired 필드가 아직 null. 의존성 주입은 생성자 호출 이후 일어나거든요. 그래서 "의존성을 활용한 초기 작업" 은 반드시 @PostConstruct 에 박아요. 생성자에 박으면 NPE 폭발.
다만 생성자 주입(매개변수) 으로 받은 의존성은 생성자 안에서 활용 가능. 그래서 생성자 주입이 권장되는 또 다른 이유 — "초기화 코드를 깔끔하게 짤 수 있다".
실전 패턴 — 가장 많이 쓰는 한 가지
실무에서 가장 자주 보는 패턴은 이거예요.
@Service
@RequiredArgsConstructor
public class CacheService {
private final RedisTemplate<String, String> redisTemplate;
@PostConstruct
public void warmUp() {
// 시작 시 캐시 미리 채우기
log.info("CacheService 준비 — 초기 데이터 로딩");
loadInitialData();
}
@PreDestroy
public void shutdown() {
// 종료 시 버퍼 flush
log.info("CacheService 종료 — 잔여 캐시 저장");
flushPendingWrites();
}
}
@PostConstruct + @PreDestroy 두 어노테이션 한 번에. 입사 교육과 퇴사 인수인계가 동시에 정의된 직원.
9~15편 IoC 컨테이너 7편으로 Spring의 심장을 다 다뤘어요. 의존성 주입 발상(9편) → Bean 정체(10편) → 컨테이너 본체(11편) → 어노테이션 등록(12편) → Java Config(13편) → Scope(14편) → 생명주기(15편). 이 7편이 머리에 박혀 있으면 16편부터 어떤 Spring 코드를 봐도 "이게 무슨 일을 하는지" 가 자연스럽게 잡혀요.
한 줄 정리 — Bean 생명주기 = 인스턴스화 → DI → @PostConstruct → 사용 → @PreDestroy. 초기 자원 준비는 @PostConstruct, 종료 정리는 @PreDestroy. Prototype Bean은 @PreDestroy 자동 호출 X.
시험 직전 한 번 더 — Bean 생명주기 입문자가 매번 헷갈리는 것
- Bean 생명주기 5단계 = 인스턴스화 → DI → 초기화 콜백 → 사용 → 소멸 콜백
@PostConstruct= 의존성 주입 끝난 직후 한 번 호출@PreDestroy= 컨테이너 종료 직전 한 번 호출- 자바 표준 어노테이션 (
javax.annotation.*)이라 Spring 외에도 통용 - 콜백 4가지 방식 = 어노테이션 / 인터페이스 /
@Bean옵션 / XML - 권장 = 어노테이션 (
@PostConstruct·@PreDestroy) - 외부 라이브러리 클래스 =
@Bean(initMethod = "...", destroyMethod = "...") - 인터페이스 방식 (
InitializingBean·DisposableBean) — Spring 결합 강해 비권장 - 호출 우선순위 = 어노테이션 → 인터페이스 →
@Bean옵션 - 한 Bean에 여러 방식 박는 건 권장 X
- 생성자 안에서는
@Autowired필드 null — 의존성 활용은@PostConstruct에서 - 생성자 주입 매개변수는 생성자 안에서 활용 가능
- Prototype Bean은
@PreDestroy자동 호출 X (시험 함정) - Singleton Bean만 컨테이너가 끝까지 관리하니 소멸 콜백 보장
- 자주 쓰이는 시나리오 = 초기 데이터 로딩 / 외부 연결 워밍업 / 종료 시 자원 정리
- AutoCloseable 구현 클래스는
destroyMethod = "(inferred)"자동 처리 @Lazy어노테이션 = Bean 생성을 첫 호출까지 지연 (선택)- Spring Boot 시작 로그에서 "Started ApplicationName in N seconds" 시점이 모든
@PostConstruct완료 직후 - 종료 시 SIGTERM →
@PreDestroy호출 → JVM 종료 흐름 - 길게 도는
@PreDestroy는 컨테이너 종료를 지연시키니 적당히 짧게
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 16편 — Bean이란 일반 객체와의 차이
- 17편 — ApplicationContext 컨테이너 본체
- 18편 — @Component @Autowired 한 번에
- 19편 — @Configuration @Bean Java Config
- 20편 — Bean Scope Singleton과 Prototype
다음 글: