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)");
    }
}

실행 순서: 생성자 → 의존성 주입 → @PostConstructafterPropertiesSet() → (사용) → @PreDestroydestroy(). (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)

해결 — 설계 재검토

순환 의존성은 설계 문제의 신호다. 해결 방법:

  1. 중간 객체 추출 — 공통 로직을 세 번째 클래스로 분리
  2. 인터페이스 도입 — 직접 참조 대신 이벤트/콜백 패턴
  3. @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 — 단계적 시작/종료

SmartLifecyclestart()/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 콜백 → @PostConstructafterPropertiesSetBeanPostProcessor 후처리 → 사용 → @PreDestroydestroy().
  • @PostConstruct에서 초기화 로직을 수행한다. 생성자에서는 의존성 주입이 완료되지 않았을 수 있으므로, 의존 객체를 안전하게 사용하려면 @PostConstruct를 쓴다.
  • 순환 의존성은 설계 문제다. 생성자 주입에서 즉시 감지되며, 세터 주입에서는 Spring의 3단계 캐시로 해결할 수 있지만 Spring Boot 2.6+부터는 기본적으로 금지된다.
  • BeanPostProcessor가 AOP의 기반이다. postProcessAfterInitialization에서 프록시 객체를 반환하여 부가 기능을 끼워넣는다.

생각해 볼 문제

  1. ApplicationContext가 bean을 eager로 초기화하는 이유는 무엇인가? lazy의 단점은?
  2. @PostConstruct 메서드에서 다른 bean을 ApplicationContext.getBean()으로 가져오면 어떤 일이 일어나는가?
  3. BeanPostProcessor가 원본 bean 대신 완전히 다른 객체를 반환하면 어떻게 되는가?
  4. initMethod(@Bean(initMethod="...")), @PostConstruct, InitializingBean의 실행 순서는?
  5. prototype 스코프 bean의 생명주기는 singleton과 어떻게 다른가? @PreDestroy가 호출되는가?

참고

'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