Bean이 생성되고 사라지기까지 — 컨테이너와 생명주기의 모든 단계
Spring 애플리케이션이 시작됐다. 컨테이너는 수백 개의 bean을 생성하고, 의존성을 주입하고, 초기화 콜백을 부르고, 애플리케이션에 서비스를 제공한다. 종료 시에는 반대로 소멸 콜백을 부르고 bean을 제거한다. 이 전체 과정이 bean 생명주기(lifecycle)다.
이 과정을 모르면 — @PostConstruct가 언제 호출되는지, BeanPostProcessor가 어디에 끼어드는지, 순환 의존성이 왜 실패하는지 이해할 수 없다. 이 글은 bean이 인스턴스화에서 소멸까지 거치는 모든 단계를 순서대로 풀어간다. (Spring Framework Reference - Lifecycle Callbacks)
BeanFactory vs ApplicationContext — 컨테이너의 두 계층
flowchart TD
BF["BeanFactory<br/>(최소 컨테이너)"] --> AC["ApplicationContext<br/>(풀 기능 컨테이너)"]
AC --> Annotation["AnnotationConfigApplicationContext<br/>(어노테이션 기반)"]
AC --> AnnotationBoot["Spring Boot 자동 구성"]
| 컨테이너 | 특징 | 사용 시기 |
|---|---|---|
BeanFactory |
lazy initialization, 최소 기능 | 거의 사용 안 함 |
ApplicationContext |
eager singleton 초기화, 이벤트, 국제화, AOP | 표준 |
ApplicationContext는 bean을 즉시(eager) 초기화한다 — 시작 시점에 모든 singleton bean을 생성하고 검증한다.BeanFactory는 lazy라서getBean()을 부를 때야 생성한다. 실제 애플리케이션에서는 항상ApplicationContext를 쓴다.
왜 eager(즉시) 초기화가 기본인가? 시작 시 모든 bean을 만들면 — 설정 오류(잘못된 bean 이름, 순환 의존성, 누락된 프로퍼티)를 배포 직후 즉시 발견할 수 있다. lazy라면 사용자가 특정 기능을 처음 사용할 때까지 오류가 숨어 있다. "빨리 실패하기(fail fast)" 원칙이다. 대신 시작 시간이 길어진다 — Spring Boot 애플리케이션 시작이 3초 걸리는 이유 중 하나.
비유: ApplicationContext는 극장 개관 전 리허설이다 — 모든 배경, 조명, 소품(bean)을 미리 세팅하고 점검한다. 관객이 와서야 "조명이 고장났네?"가 아니라, 리허설에서 미리 발견한다. BeanFactory는 "필요할 때마다 그때그때 세팅"이다 — 무대 위에서 소품이 없는 것을 발견할 수도 있다.
Bean 생명주기 — 전체 단계
flowchart TD
I["1. 인스턴스화<br/>(생성자 호출)"] --> P["2. 프로퍼티 주입<br/>(setter / @Autowired 필드)"]
P --> BN["3. BeanNameAware<br/>setBeanName()"]
BN --> BF["4. BeanFactoryAware<br/>setBeanFactory()"]
BF --> AC2["5. ApplicationContextAware<br/>setApplicationContext()"]
AC2 --> PPB["6. BeanPostProcessor.postProcessBeforeInitialization<br/>(@PostConstruct 처리)"]
PPB --> IS["7. InitializingBean.afterPropertiesSet()<br/>또는 @Bean(initMethod=)"]
IS --> PPA["8. BeanPostProcessor.postProcessAfterInitialization<br/>(AOP 프록시 생성)"]
PPA --> READY["9. Bean 사용 준비 완료"]
READY --> DESTROY["10. 종료 시<br/>@PreDestroy / DisposableBean.destroy()"]
단계별 설명
// Spring 7 / Java 25 — 생명주기 콜백을 모두 가진 bean
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.DisposableBean;
@Component
public class LifecycleDemo implements InitializingBean, DisposableBean {
public LifecycleDemo() {
System.out.println("1. 생성자 호출 (인스턴스화)");
}
@Autowired
public void setDependency(SomeDependency dep) {
System.out.println("2. 의존성 주입 (setter)");
}
@PostConstruct
public void init() {
System.out.println("3. @PostConstruct (초기화)");
}
@Override
public void afterPropertiesSet() {
System.out.println("4. afterPropertiesSet() (InitializingBean)");
}
@PreDestroy
public void cleanup() {
System.out.println("5. @PreDestroy (소멸 전)");
}
@Override
public void destroy() {
System.out.println("6. destroy() (DisposableBean)");
}
}
실행 순서: 생성자 → 의존성 주입 →
@PostConstruct→afterPropertiesSet()→ (사용) →@PreDestroy→destroy(). (Spring Framework Reference - Lifecycle)
@PostConstruct vs 생성자 — 핵심 차이
@Component
public class GoodService {
private final DatabaseClient client;
public GoodService(DatabaseClient client) {
this.client = client;
// 여기서 client를 사용하지 마라 — 아직 완전히 초기화되지 않았을 수 있음
}
@PostConstruct
public void init() {
// 여기서는 모든 의존성이 주입된 상태 — 안전하게 client 사용 가능
client.connect();
}
}
생성자에서는 자신의 의존성은 주입됐지만, 다른 bean의 @PostConstruct가 아직 실행되지 않았을 수 있다. @PostConstruct는 모든 의존성 주입이 완료된 후 실행되므로, 의존 객체를 안전하게 사용할 수 있다.
@PostConstruct와@PreDestroy는 Jakarta EE 9+에서jakarta.annotation패키지로 이동했다 (javax.annotation이 아님). Spring 7에서는import jakarta.annotation.PostConstruct를 사용한다.
BeanPostProcessor — bean 생성 과정에 끼어들기
BeanPostProcessor는 모든 bean의 초기화 전후에 가로채기(intercept)를 수행하는 확장 지점이다.
// Spring 7 / Java 25 — 커스텀 BeanPostProcessor
@Component
public class LoggingPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
System.out.println("[BPP] 초기화 전: " + beanName);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
System.out.println("[BPP] 초기화 후: " + beanName);
return bean; // 다른 객체를 반환하면 원본 bean이 교체됨
}
}
AOP의 프록시 생성이 바로 이 단계에서 일어난다 —
postProcessAfterInitialization에서 원본 bean 대신 프록시 객체를 반환하여 부가 기능(로깅, 트랜잭션, 보안)을 끼워넣는다. (장 06-AOP 상세)
순환 의존성 — 왜 생성자 주입에서 실패하는가
// Spring 7 / Java 25 — 순환 의존성
@Component
class A {
private final B b;
public A(B b) { this.b = b; } // A는 B가 필요
}
@Component
class B {
private final A a;
public B(A a) { this.a = a; } // B는 A가 필요
}
// 애플리케이션 시작 실패: BeanCurrentlyInCreationException
생성자 주입에서 순환 참조가 있으면 Spring은 A를 만들려다 B가 필요하고, B를 만들려다 A가 필요한 상태에 빠진다 — 무한 루프를 감지하고 예외를 던진다. (Spring Reference - Circular Dependencies)
세터/필드 주입에서는 Spring이 3단계 캐싱(singletonFactories)으로 순환 의존성을 해결할 수 있었다. 단, Spring Boot 2.6+부터는 기본적으로
spring.main.allow-circular-references=false로 금지됐다. (Spring Boot Reference)
해결 — 설계 재검토
순환 의존성은 설계 문제의 신호다. 해결 방법:
- 중간 객체 추출 — 공통 로직을 세 번째 클래스로 분리
- 인터페이스 도입 — 직접 참조 대신 이벤트/콜백 패턴
@Lazy— 지연 초기화로 순환 회피 (근본 해결 아님)
bean 정의 상속 — @Bean 메서드의 프로퍼티 상속
// Spring 7 / Java 25 — @Bean으로 공통 설정 공유
@Configuration
public class DataSourceConfig {
@Bean
@Scope("prototype")
public DatabaseClient clientPrototype() {
return new DatabaseClient("jdbc:postgresql://localhost:5432/mydb");
}
}
실습 — bean 생명주기 관찰
// Spring 7 / Java 25 — LifecycleDemo.java (Spring 없이 콜백 순서 시뮬레이션)
import jakarta.annotation.*;
import java.lang.annotation.*;
import java.lang.reflect.*;
public class LifecycleDemo {
public static class MyBean {
public MyBean() { System.out.println("1. 생성자"); }
@PostConstruct public void init() { System.out.println("2. @PostConstruct"); }
@PreDestroy public void cleanup() { System.out.println("4. @PreDestroy"); }
}
public static void main(String[] args) throws Exception {
System.out.println("=== Bean 생성 ===");
var bean = new MyBean();
// @PostConstruct 수동 호출 (Spring이 자동으로 하는 일)
for (var m : bean.getClass().getMethods()) {
if (m.isAnnotationPresent(PostConstruct.class)) m.invoke(bean);
}
System.out.println("3. [Bean 사용 중]");
System.out.println("=== Bean 소멸 ===");
for (var m : bean.getClass().getMethods()) {
if (m.isAnnotationPresent(PreDestroy.class)) m.invoke(bean);
}
}
}
java LifecycleDemo.java
=== Bean 생성 ===
1. 생성자
2. @PostConstruct
3. [Bean 사용 중]
=== Bean 소멸 ===
4. @PreDestroy
확인할 것: 생성자 → @PostConstruct → 사용 → @PreDestroy 순서로 콜백이 실행된다. Spring 컨테이너가 이 과정을 자동으로 수행한다.
@DependsOn — 명시적 초기화 순서
bean 간 의존성이 @Autowired로 연결되지 않았지만, 초기화 순서가 중요한 경우가 있다. 예를 들어 database 초기화 bean이 다른 bean보다 먼저 실행되어야 할 때.
// Spring 7 / Java 25 — @DependsOn으로 순서 제어
@Component
@DependsOn("databaseInitializer")
public class CacheLoader {
@PostConstruct
public void load() {
// databaseInitializer의 @PostConstruct가 먼저 실행됨을 보장
}
}
@Component
public class DatabaseInitializer {
@PostConstruct
public void init() {
// DB 스키마 생성, 시드 데이터 삽입 등
}
}
@DependsOn은@Autowired의존성이 아닌 초기화 순서만 보장한다. 가능하면 명시적 의존성(생성자 주입)으로 설계하는 것이 더 명확하다 —@DependsOn은 필요한 경우에만 쓴다.
SmartLifecycle — 단계적 시작/종료
SmartLifecycle은 start()/stop() 콜백과 단계(phase)를 제공하여, 여러 컴포넌트의 시작/종료 순서를 제어한다:
// Spring 7 / Java 25 — SmartLifecycle
import org.springframework.context.SmartLifecycle;
@Component
public class KafkaConsumerLifecycle implements SmartLifecycle {
private volatile boolean running = false;
@Override
public void start() {
System.out.println("Kafka consumer 시작");
running = true;
}
@Override
public void stop() {
System.out.println("Kafka consumer 중단");
running = false;
}
@Override
public boolean isRunning() { return running; }
@Override
public int getPhase() { return 1; }
// phase가 낮은 것부터 시작, 높은 것부터 종료
// 기본 phase = Integer.MAX_VALUE (가장 마지막에 시작, 가장 먼저 종료)
}
SmartLifecycle은 비동기 리소스(메시지 컨슈머, 네트워크 서버, 스케줄러)의 시작/종료를 컨테이너 라이프사이클에 통합한다.phase로 종료 순서를 제어할 수 있다 — phase가 높은 컴포넌트부터 종료되어 의존성 역순으로 안전하게 해제된다.
요약 — 이 글의 결론
- ApplicationContext가 표준 컨테이너다. BeanFactory의 최소 기능보다 이벤트, AOP, 국제화를 제공한다. singleton bean을 시작 시 즉시 생성하여 설정 오류를 빠르게 발견한다.
- Bean 생명주기는 10단계다. 인스턴스화 → 의존성 주입 → Aware 콜백 →
@PostConstruct→afterPropertiesSet→BeanPostProcessor후처리 → 사용 →@PreDestroy→destroy(). @PostConstruct에서 초기화 로직을 수행한다. 생성자에서는 의존성 주입이 완료되지 않았을 수 있으므로, 의존 객체를 안전하게 사용하려면@PostConstruct를 쓴다.- 순환 의존성은 설계 문제다. 생성자 주입에서 즉시 감지되며, 세터 주입에서는 Spring의 3단계 캐시로 해결할 수 있지만 Spring Boot 2.6+부터는 기본적으로 금지된다.
- BeanPostProcessor가 AOP의 기반이다.
postProcessAfterInitialization에서 프록시 객체를 반환하여 부가 기능을 끼워넣는다.
생각해 볼 문제
ApplicationContext가 bean을 eager로 초기화하는 이유는 무엇인가? lazy의 단점은?@PostConstruct메서드에서 다른 bean을ApplicationContext.getBean()으로 가져오면 어떤 일이 일어나는가?BeanPostProcessor가 원본 bean 대신 완전히 다른 객체를 반환하면 어떻게 되는가?initMethod(@Bean(initMethod="...")),@PostConstruct,InitializingBean의 실행 순서는?- prototype 스코프 bean의 생명주기는 singleton과 어떻게 다른가?
@PreDestroy가 호출되는가?
참고
- Spring Framework 7 - Lifecycle Callbacks - 접근 2026-07-10
- Spring Framework 7 - ApplicationContext - 접근 2026-07-10
- Spring Framework 7 - Circular Dependencies - 접근 2026-07-10
- jakarta.annotation.PostConstruct API - 접근 2026-07-10
'Develop Artifacts > Spring' 카테고리의 다른 글
| Spring - 06. aop (0) | 2026.07.12 |
|---|---|
| Spring - 05. bean scope profile (0) | 2026.07.12 |
| Spring - 04. configuration component-scan (0) | 2026.07.12 |
| Spring - 03. dependency-injection (0) | 2026.07.12 |
| Spring - 01. IoC DI (0) | 2026.07.12 |
