Spring Bean의 생명주기와 초기화 순서 제어 심화 가이드

Spring Bean 생명주기의 핵심 단계

Spring IoC 컨테이너는 Bean의 생성부터 소멸까지의 전체 과정을 관리합니다. 이 과정은 크게 인스턴스화 및 의존성 주입, 초기화, 그리고 소멸 단계로 나눌 수 있습니다.

flowchart TD
    A[Bean 생명주기 시작] --> B[인스턴스화: 생성자 호출]
    B --> C[속성 주입: 의존성 설정]
    C --> D[Aware 인터페이스 콜백]
    D --> E[BeanPostProcessor 사전 처리]
    E --> F[@PostConstruct 메서드]
    F --> G[InitializingBean의 afterPropertiesSet]
    G --> H[사용자 정의 init-method]
    H --> I[BeanPostProcessor 사후 처리]
    I --> J[Bean 사용 준비 완료]
    J --> K[컨테이너 종료]
    K --> L[@PreDestroy 메서드]
    L --> M[DisposableBean의 destroy]
    M --> N[사용자 정의 destroy-method]
    N --> O[Bean 소멸 및 생명주기 종료]

1. 인스턴스화 및 의존성 주입

  • 인스턴스화: 컨테이너가 리플렉션을 통해 생성자를 호출하여 메모리에 객체를 할당합니다.
  • 속성 주입: @Autowired@Value 등을 통해 필요한 의존성을 주입합니다.

2. 초기화 단계

  • Aware 인터페이스: BeanNameAware, ApplicationContextAware 등을 구현했다면 컨테이너 관련 정보를 주입받습니다.
  • BeanPostProcessor 사전 처리: postProcessBeforeInitialization이 호출되어 AOP 프록시 생성 등의 작업이 이루어질 수 있습니다.
  • 초기화 메서드 실행: @PostConstruct -> InitializingBean.afterPropertiesSet() -> @Bean(initMethod) 순서로 실행됩니다.
  • BeanPostProcessor 사후 처리: postProcessAfterInitialization이 호출되며, 최종적으로 컨테이너에 등록될 객체(프록시 등)를 반환합니다.

3. 소멸 단계

컨테이너가 종료될 때 단일 Bean(Singleton)에 대해 소멸 콜백이 실행됩니다. @PreDestroy -> DisposableBean.destroy() -> @Bean(destroyMethod) 순서로 진행됩니다. Prototype 스코프의 경우 소멸 콜백이 호출되지 않습니다.

생명주기 인터페이스 구현 예제

public class LifecycleDemoBean implements BeanNameAware, InitializingBean, DisposableBean {

    private String configValue;

    public LifecycleDemoBean() {
        System.out.println("[1] 인스턴스화: 생성자 호출");
    }

    public void setConfigValue(String configValue) {
        this.configValue = configValue;
        System.out.println("[2] 속성 주입: setter 호출");
    }

    @Override
    public void setBeanName(String name) {
        System.out.println("[3] Aware 콜백: Bean 이름은 " + name);
    }

    @PostConstruct
    public void initAnnotation() {
        System.out.println("[4] 초기화: @PostConstruct 실행");
    }

    @Override
    public void afterPropertiesSet() {
        System.out.println("[5] 초기화: InitializingBean 실행");
    }

    public void customInit() {
        System.out.println("[6] 초기화: 사용자 정의 init-method 실행");
    }

    @PreDestroy
    public void cleanupAnnotation() {
        System.out.println("[7] 소멸: @PreDestroy 실행");
    }

    @Override
    public void destroy() {
        System.out.println("[8] 소멸: DisposableBean 실행");
    }

    public void customDestroy() {
        System.out.println("[9] 소멸: 사용자 정의 destroy-method 실행");
    }
}
@Configuration
public class LifecycleConfig {

    @Bean(initMethod = "customInit", destroyMethod = "customDestroy")
    public LifecycleDemoBean lifecycleDemoBean() {
        LifecycleDemoBean bean = new LifecycleDemoBean();
        bean.setConfigValue("demo-config");
        return bean;
    }
}

BeanPostProcessor의 동작 원리와 오해 해소

BeanPostProcessor는 특정 Bean이 아닌 컨테이너 내의 모든 Bean 생성 과정에 개입하는 전역 인터셉터입니다. 특정 Bean 클래스 내부에 정의하는 것이 아니라, 독립된 컴포넌트로 등록하여 전체 Bean의 생명주기를 가로챕니다.

실전 예제: 모든 Repository 빈에 성능 측정 프록시 적용

특정 애노테이션이 붙은 Bean에만 동적 프록시를 적용하여 메서드 실행 시간을 측정하는 후처리기를 작성해 보겠습니다.

@Component
public class PerformanceMonitorPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        // @Repository 애노테이션이 있는 클래스만 프록시로 래핑
        if (bean.getClass().isAnnotationPresent(Repository.class)) {
            return Proxy.newProxyInstance(
                bean.getClass().getClassLoader(),
                bean.getClass().getInterfaces(),
                new TimingInvocationHandler(bean)
            );
        }
        return bean;
    }
}

class TimingInvocationHandler implements InvocationHandler {
    private final Object target;

    public TimingInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long start = System.nanoTime();
        try {
            return method.invoke(target, args);
        } finally {
            long duration = (System.nanoTime() - start) / 1_000_000;
            System.out.println("[" + target.getClass().getSimpleName() + "] " + method.getName() + " 실행 시간: " + duration + "ms");
        }
    }
}

위 코드가 컨테이너에 등록되면, Spring은 UserRepository, OrderRepository 등 모든 @Repository Bean을 생성할 때 자동으로 postProcessAfterInitialization을 호출하여 프록시 객체로 교체합니다.

Bean 초기화 순서 제어 전략

특정 Bean들이 반드시 순서대로 초기화되어야 하는 경우(예: 데이터베이스 스키마 생성 후 캐시 워밍업), 다음과 같은 전략을 사용할 수 있습니다.

1. @DependsOn을 이용한 명시적 의존성 선언

가장 직관적인 방법으로, 특정 Bean이 먼저 생성되도록 강제합니다.

@Component
public class CacheWarmer {
    @PostConstruct
    public void warmUp() {
        System.out.println("캐시 워밍업 완료");
    }
}

@Component
@DependsOn("cacheWarmer") // cacheWarmer가 먼저 초기화됨을 보장
public class ApplicationStartupTask {
    @PostConstruct
    public void run() {
        System.out.println("애플리케이션 시작 작업 실행");
    }
}

2. SmartLifecycle 인터페이스 활용

phase 값을 통해 컨테이너 시작 및 종료 시의 실행 순서를 세밀하게 제어할 수 있습니다.

@Component
public class DatabaseSeeder implements SmartLifecycle {
    private boolean running = false;

    @Override
    public void start() {
        System.out.println("데이터베이스 시드 데이터 적재");
        running = true;
    }

    @Override
    public int getPhase() {
        return 10; // 숫자가 작을수록 먼저 실행
    }

    @Override
    public boolean isRunning() { return running; }
}

@Component
public class MessageBrokerStarter implements SmartLifecycle {
    private boolean running = false;

    @Override
    public void start() {
        System.out.println("메시지 브로커 연결 시작");
        running = true;
    }

    @Override
    public int getPhase() {
        return 20; // DatabaseSeeder 이후에 실행
    }

    @Override
    public boolean isRunning() { return running; }
}

3. ApplicationRunner와 @Order 결합

모든 Bean의 초기화가 완료된 직후, 애플리케이션 시작 시점에 실행되어야 하는 작업의 순서를 제어할 때 유용합니다.

@Component
@Order(1)
public class IndexBuilderRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) {
        System.out.println("검색 인덱스 빌드");
    }
}

@Component
@Order(2)
public class HealthCheckRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) {
        System.out.println("외부 시스템 헬스체크");
    }
}

4. 이벤트 리스너를 통한 느슨한 결합

초기화 완료 이벤트를 발행하고 이를 청취하여 다음 단계를 진행하면 컴포넌트 간 결합도를 낮출 수 있습니다.

public class DataReadyEvent extends ApplicationEvent {
    public DataReadyEvent(Object source) { super(source); }
}

@Component
public class DataInitializer {
    private final ApplicationEventPublisher publisher;

    public DataInitializer(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    @PostConstruct
    public void init() {
        System.out.println("데이터 초기화 완료");
        publisher.publishEvent(new DataReadyEvent(this));
    }
}

@Component
public class AnalyticsEngine {
    @EventListener
    public void onDataReady(DataReadyEvent event) {
        System.out.println("분석 엔진 가동 (데이터 준비 이벤트 수신)");
    }
}

전략별 비교 요약

전략 적용 시점 특징
@DependsOn Bean 생성 및 초기화 단계 단순한 선후 관계 명시. 복잡한 그래프에는 부적합.
SmartLifecycle 컨테이너 구동/종료 단계 Phase 기반의 정교한 순서 제어 가능.
ApplicationRunner 모든 Bean 초기화 완료 직후 순수한 시작 작업(배치, 캐시 등)에 적합. @Order로 순서 지정.
ApplicationEvent 특정 Bean 초기화 완료 후 이벤트 기반 아키텍처로 확장성 및 느슨한 결합 제공.

태그: SpringFramework BeanLifecycle BeanPostProcessor IOC DependsOn

6월 2일 20:05에 게시됨