Press / to search, Esc to close, ↑↓ to navigate

Spring Bean 라이프사이클 완전 정복

생성부터 소멸까지 — Bean이 거치는 모든 단계를 코드로 확인하기

Posted by DoYoon Kim on April 5, 2026 | 17 min read

들어가며

Spring을 사용하면 대부분의 객체 생명주기를 컨테이너가 관리한다. new로 직접 생성하지 않고 @Component@Bean으로 등록하면, Spring이 알아서 객체를 만들고, 의존성을 주입하고, 초기화하고, 소멸시킨다. 이 과정의 기반이 되는 JVM 클래스 로딩 메커니즘을 이해하면 Spring이 어떻게 바이트코드 레벨에서 빈을 관리하는지 더 깊이 파악할 수 있다.

하지만 “정확히 어떤 순서로?” 라는 질문에 답하려면 Bean 라이프사이클을 제대로 이해해야 한다. 특히 AOP 프록시 생성 시점이나 초기화 로직을 넣을 위치를 결정할 때 이 지식이 직접적으로 필요하다.


Bean 라이프사이클 전체 흐름

Spring Bean이 생성되고 소멸되기까지의 전체 흐름은 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
1. 빈 인스턴스화 (Instantiation)
       ↓
2. 의존성 주입 (Dependency Injection)
       ↓
3. BeanNameAware.setBeanName()
       ↓
4. BeanFactoryAware.setBeanFactory()
       ↓
5. ApplicationContextAware.setApplicationContext()
       ↓
6. BeanPostProcessor.postProcessBeforeInitialization()
       ↓
7. @PostConstruct
       ↓
8. InitializingBean.afterPropertiesSet()
       ↓
9. @Bean(initMethod = "...")
       ↓
10. BeanPostProcessor.postProcessAfterInitialization()
    ← AOP 프록시가 여기서 생성됨
       ↓
    ===== 빈 사용 =====
       ↓
11. @PreDestroy
       ↓
12. DisposableBean.destroy()
       ↓
13. @Bean(destroyMethod = "...")

단계가 많아 보이지만, 실무에서 주로 사용하는 것은 @PostConstruct / @PreDestroy, InitializingBean / DisposableBean, BeanPostProcessor 세 가지 그룹이다.


초기화 콜백: 세 가지 방법

1. @PostConstruct / @PreDestroy (권장)

Jakarta EE 표준 어노테이션으로, 가장 간결하고 가독성이 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Component
@Slf4j
public class CacheWarmer {

    private final ProductRepository productRepository;
    private Map<Long, Product> cache;

    @Autowired
    public CacheWarmer(ProductRepository productRepository) {
        this.productRepository = productRepository;
        // 이 시점에서는 아직 DI가 완료되지 않았을 수 있음
        log.info("[1] 생성자 호출 — productRepository: {}", productRepository);
    }

    @PostConstruct
    public void warmUp() {
        // DI 완료 후 호출 — 안전하게 의존 객체를 사용할 수 있다
        log.info("[2] @PostConstruct — 캐시 워밍업 시작");
        cache = productRepository.findAll().stream()
                .collect(Collectors.toMap(Product::getId, Function.identity()));
        log.info("[2] @PostConstruct — {}개 상품 캐시 완료", cache.size());
    }

    @PreDestroy
    public void clearCache() {
        log.info("[3] @PreDestroy — 캐시 정리");
        cache.clear();
    }
}

왜 생성자가 아닌 @PostConstruct에서 초기화하는가? 생성자 호출 시점에는 필드 주입(@Autowired 필드)이 아직 완료되지 않았을 수 있다. @PostConstruct는 모든 의존성 주입이 끝난 후 호출되므로 안전하다. 위 예시처럼 생성자 주입을 사용하면 생성자에서도 의존 객체에 접근할 수 있지만, 다른 빈의 @PostConstruct가 아직 실행되지 않았을 수 있으므로 초기화 로직은 @PostConstruct에 두는 것이 원칙이다.

2. InitializingBean / DisposableBean

Spring 전용 인터페이스를 구현하는 방식이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Component
@Slf4j
public class DatabaseHealthChecker implements InitializingBean, DisposableBean {

    private final DataSource dataSource;
    private ScheduledExecutorService scheduler;

    public DatabaseHealthChecker(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        log.info("InitializingBean.afterPropertiesSet() — DB 헬스체크 스케줄러 시작");
        scheduler = Executors.newSingleThreadScheduledExecutor();
        scheduler.scheduleAtFixedRate(this::checkHealth, 0, 30, TimeUnit.SECONDS);
    }

    @Override
    public void destroy() throws Exception {
        log.info("DisposableBean.destroy() — 스케줄러 종료");
        scheduler.shutdown();
    }

    private void checkHealth() {
        try (Connection conn = dataSource.getConnection()) {
            conn.isValid(3);
        } catch (SQLException e) {
            log.error("DB 헬스체크 실패", e);
        }
    }
}

이 방식은 Spring 프레임워크에 직접 의존하게 되므로, 프레임워크 독립적인 코드가 필요할 때는 @PostConstruct/@PreDestroy를 사용하는 것이 좋다.

3. @Bean(initMethod, destroyMethod)

외부 라이브러리 클래스처럼 소스를 수정할 수 없는 경우에 유용하다.

1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {

    @Bean(initMethod = "init", destroyMethod = "close")
    public ExternalConnectionPool connectionPool() {
        return new ExternalConnectionPool("jdbc:mysql://localhost/mydb");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 외부 라이브러리 클래스 — 어노테이션을 붙일 수 없음
public class ExternalConnectionPool {

    public ExternalConnectionPool(String url) { /* ... */ }

    public void init() {
        // 커넥션 풀 초기화
    }

    public void close() {
        // 커넥션 풀 정리
    }
}

실행 순서 확인: 전체 라이프사이클 로그

세 가지 방식을 모두 적용한 빈을 만들어 실행 순서를 확인해 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Component
@Slf4j
public class LifecycleDemo implements InitializingBean, DisposableBean,
        BeanNameAware, ApplicationContextAware {

    private String beanName;

    public LifecycleDemo() {
        log.info("[1] 생성자 호출");
    }

    @Override
    public void setBeanName(String name) {
        this.beanName = name;
        log.info("[2] BeanNameAware.setBeanName() — {}", name);
    }

    @Override
    public void setApplicationContext(ApplicationContext ctx) {
        log.info("[3] ApplicationContextAware.setApplicationContext()");
    }

    @PostConstruct
    public void postConstruct() {
        log.info("[4] @PostConstruct");
    }

    @Override
    public void afterPropertiesSet() {
        log.info("[5] InitializingBean.afterPropertiesSet()");
    }

    @PreDestroy
    public void preDestroy() {
        log.info("[6] @PreDestroy");
    }

    @Override
    public void destroy() {
        log.info("[7] DisposableBean.destroy()");
    }
}

실행 결과:

1
2
3
4
5
6
7
8
[1] 생성자 호출
[2] BeanNameAware.setBeanName() — lifecycleDemo
[3] ApplicationContextAware.setApplicationContext()
[4] @PostConstruct
[5] InitializingBean.afterPropertiesSet()
===== 애플리케이션 실행 =====
[6] @PreDestroy
[7] DisposableBean.destroy()

@PostConstructInitializingBean.afterPropertiesSet()보다 먼저 호출된다는 점을 기억하자. 이는 @PostConstructBeanPostProcessorpostProcessBeforeInitialization 단계에서 처리되기 때문이다.


BeanPostProcessor 활용

BeanPostProcessor모든 빈의 초기화 전후에 개입할 수 있는 강력한 확장 포인트다. Spring 내부적으로도 AOP 프록시 생성, @Autowired 처리, @PostConstruct 처리 등이 모두 BeanPostProcessor를 통해 이루어진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
@Slf4j
public class CustomBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
            throws BeansException {
        if (bean.getClass().isAnnotationPresent(MonitoredComponent.class)) {
            log.info("[BPP Before] {} — 모니터링 대상 빈 감지", beanName);
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName)
            throws BeansException {
        if (bean.getClass().isAnnotationPresent(MonitoredComponent.class)) {
            log.info("[BPP After] {} — 초기화 완료, 모니터링 등록", beanName);
            MonitoringRegistry.register(beanName, bean);
        }
        return bean;
    }
}
1
2
3
4
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MonitoredComponent {
}
1
2
3
4
5
@Component
@MonitoredComponent
public class PaymentGateway {
    // 이 빈이 생성될 때 CustomBeanPostProcessor가 개입한다
}

AOP 프록시와 BeanPostProcessor

Spring AOP 글에서 설명했듯이, AOP 프록시는 BeanPostProcessorpostProcessAfterInitialization 단계에서 생성된다. 즉, @PostConstruct가 실행되는 시점에는 아직 프록시가 아닌 원본 객체다.

1
2
3
4
5
@PostConstruct        → 원본 객체에서 실행
  ↓
postProcessAfterInit  → 여기서 프록시로 교체
  ↓
빈 등록               → 프록시 객체가 빈으로 등록됨

이 때문에 @PostConstruct에서 this를 출력하면 프록시가 아닌 원본 클래스명이 나온다.


스코프별 라이프사이클 차이

스코프 생성 시점 소멸 시점
singleton (기본) 컨테이너 시작 시 컨테이너 종료 시
prototype 요청할 때마다 컨테이너가 관리하지 않음
request HTTP 요청 시 HTTP 응답 완료 시
session 세션 생성 시 세션 종료 시

prototype 스코프 주의: Spring은 prototype 빈의 소멸을 관리하지 않는다. @PreDestroy가 호출되지 않으므로, 리소스 정리가 필요하다면 직접 처리해야 한다.

1
2
3
4
5
6
7
8
9
@Component
@Scope("prototype")
public class PrototypeBean {

    @PreDestroy
    public void cleanup() {
        // 이 메서드는 호출되지 않는다!
    }
}

실전 주의사항

  1. 생성자에서 무거운 작업을 하지 마라 — 생성자는 빈 인스턴스화 단계이므로, 아직 다른 빈이 초기화되지 않았을 수 있다. 무거운 초기화는 @PostConstruct에서 수행하자.

  2. @PostConstruct에서 예외가 발생하면 애플리케이션이 기동되지 않는다 — 초기화 실패 시 빈 등록이 취소되고, 의존하는 다른 빈도 연쇄적으로 실패한다. 외부 시스템 연결 같은 불안정한 작업은 @PostConstruct 대신 별도의 헬스체크 메커니즘을 고려하자.

  3. 순환 참조와 라이프사이클 — A → B → A 순환 참조가 있으면 Spring은 아직 완전히 초기화되지 않은 빈의 참조를 주입할 수 있다. 생성자 주입을 사용하면 순환 참조를 컴파일 타임에 감지할 수 있다.

  4. @Lazy와 초기화 시점@Lazy가 붙은 빈은 처음 사용될 때까지 초기화가 지연된다. 애플리케이션 기동 시간을 줄이는 데 유용하지만, 런타임에 초기화 에러가 발생할 수 있다는 단점이 있다.


마무리

Spring Bean 라이프사이클의 핵심은 “의존성 주입 완료 후 초기화” 라는 원칙이다. 생성자는 객체 생성만, 비즈니스 초기화 로직은 @PostConstruct에서 처리하는 패턴을 기본으로 삼자. BeanPostProcessor는 프레임워크 확장이나 커스텀 어노테이션 처리에 강력한 도구이며, AOP 프록시가 여기서 생성된다는 점은 Spring 내부를 이해하는 데 중요한 열쇠다.


관련 포스트

Share


CATALOG