Spring Bean Lifecycle이란?
Spring IoC 컨테이너는 Bean의 생성부터 소멸까지 전체 생명주기를 관리한다. 단순히 객체를 만들고 주입하는 것이 아니라, 초기화 콜백, 후처리, 소멸 콜백 등 정교한 단계를 거친다. 이 생명주기를 이해하면 커넥션 풀 초기화, 리소스 정리, 커스텀 프록시 생성 등 고급 패턴을 구현할 수 있다.
이 글에서는 Bean 생명주기의 전체 단계, 초기화·소멸 콜백 3가지 방식, BeanPostProcessor, Scope별 동작 차이까지 실전 수준으로 다룬다.
Bean 생명주기 전체 흐름
Spring Bean은 다음 순서로 생명주기가 진행된다:
1. 인스턴스화 (Instantiation)
└─ 생성자 호출
2. 의존성 주입 (Dependency Injection)
└─ @Autowired, 생성자 주입, setter 주입
3. Aware 인터페이스 콜백
├─ BeanNameAware.setBeanName()
├─ BeanFactoryAware.setBeanFactory()
└─ ApplicationContextAware.setApplicationContext()
4. BeanPostProcessor.postProcessBeforeInitialization()
5. 초기화 콜백
├─ @PostConstruct
├─ InitializingBean.afterPropertiesSet()
└─ @Bean(initMethod = "init")
6. BeanPostProcessor.postProcessAfterInitialization()
└─ AOP 프록시 생성 시점
7. ═══ Bean 사용 (애플리케이션 실행 중) ═══
8. 소멸 콜백
├─ @PreDestroy
├─ DisposableBean.destroy()
└─ @Bean(destroyMethod = "cleanup")
초기화 콜백: 3가지 방식 비교
| 방식 | 장점 | 단점 | 실행 순서 |
|---|---|---|---|
@PostConstruct |
표준 JSR-250, 간결 | Spring 의존 없음 (Jakarta) | 1번째 |
InitializingBean |
Spring 프레임워크 표준 | Spring에 강결합 | 2번째 |
@Bean(initMethod) |
외부 라이브러리에 적용 가능 | 설정 클래스에서만 사용 | 3번째 |
@Component
@Slf4j
public class CacheWarmer implements InitializingBean {
private final ProductRepository productRepository;
private final CacheManager cacheManager;
// 1. 생성자 주입 (인스턴스화 + DI)
public CacheWarmer(ProductRepository productRepository, CacheManager cacheManager) {
this.productRepository = productRepository;
this.cacheManager = cacheManager;
log.info("1️⃣ 생성자 호출 — 의존성 주입 완료");
}
// 2. @PostConstruct — 가장 먼저 실행
@PostConstruct
public void postConstruct() {
log.info("2️⃣ @PostConstruct — 초기 검증");
Objects.requireNonNull(cacheManager, "CacheManager가 null입니다");
}
// 3. InitializingBean — 두 번째 실행
@Override
public void afterPropertiesSet() {
log.info("3️⃣ afterPropertiesSet — 캐시 워밍");
List<Product> products = productRepository.findPopularProducts(100);
Cache cache = cacheManager.getCache("products");
products.forEach(p -> cache.put(p.getId(), p));
log.info("캐시 워밍 완료: {}건", products.size());
}
// 4. 소멸 콜백
@PreDestroy
public void cleanup() {
log.info("4️⃣ @PreDestroy — 캐시 정리");
cacheManager.getCache("products").clear();
}
}
Aware 인터페이스: 컨테이너 메타데이터 접근
Aware 인터페이스를 구현하면 Bean이 자신의 메타데이터나 컨테이너 객체에 접근할 수 있다.
@Component
@Slf4j
public class SelfAwareService implements
BeanNameAware,
ApplicationContextAware,
EnvironmentAware {
private String beanName;
private ApplicationContext context;
private Environment environment;
@Override
public void setBeanName(String name) {
this.beanName = name;
log.info("Bean 이름: {}", name);
}
@Override
public void setApplicationContext(ApplicationContext ctx) {
this.context = ctx;
log.info("활성 프로파일: {}",
Arrays.toString(ctx.getEnvironment().getActiveProfiles()));
}
@Override
public void setEnvironment(Environment env) {
this.environment = env;
}
// 런타임에 다른 Bean을 동적으로 조회
public <T> T getBean(Class<T> type) {
return context.getBean(type);
}
// 환경별 분기
public boolean isProduction() {
return environment.acceptsProfiles(Profiles.of("prod"));
}
}
BeanPostProcessor: Bean 후처리의 핵심
BeanPostProcessor는 모든 Bean의 초기화 전후에 개입하는 확장 포인트다. Spring AOP, @Async, @Scheduled 등이 내부적으로 이 메커니즘을 사용한다.
@Component
@Slf4j
public class ExecutionTimePostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
// 초기화 전 — 원본 Bean에 접근 가능
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
// 초기화 후 — 프록시로 교체 가능
Class<?> beanClass = bean.getClass();
// @Timed 어노테이션이 있는 Bean만 프록시 생성
boolean hasTimed = Arrays.stream(beanClass.getMethods())
.anyMatch(m -> m.isAnnotationPresent(Timed.class));
if (hasTimed) {
return createTimingProxy(bean, beanClass);
}
return bean;
}
private Object createTimingProxy(Object bean, Class<?> beanClass) {
return Proxy.newProxyInstance(
beanClass.getClassLoader(),
beanClass.getInterfaces(),
(proxy, method, args) -> {
if (method.isAnnotationPresent(Timed.class)) {
long start = System.nanoTime();
try {
return method.invoke(bean, args);
} finally {
long elapsed = System.nanoTime() - start;
log.info("{}.{} 실행 시간: {}ms",
beanClass.getSimpleName(), method.getName(),
elapsed / 1_000_000);
}
}
return method.invoke(bean, args);
}
);
}
}
Bean Scope와 생명주기 차이
| Scope | 생성 시점 | 소멸 콜백 | 사용 사례 |
|---|---|---|---|
singleton (기본) |
컨테이너 시작 시 | 호출됨 | 대부분의 서비스 |
prototype |
요청 시마다 새로 생성 | 호출 안 됨 ⚠️ | 상태 보유 객체 |
request |
HTTP 요청마다 | 요청 종료 시 | 요청별 컨텍스트 |
session |
세션마다 | 세션 종료 시 | 사용자 세션 데이터 |
주의: Prototype Bean은 Spring이 소멸 콜백을 호출하지 않는다. 리소스 정리가 필요하면 직접 관리해야 한다.
@Configuration
public class PrototypeConfig {
@Bean
@Scope("prototype")
public ExpensiveResource expensiveResource() {
return new ExpensiveResource();
}
// Singleton에서 Prototype 주입 시 ObjectProvider 사용
@Bean
public ResourceManager resourceManager(
ObjectProvider<ExpensiveResource> resourceProvider) {
return new ResourceManager(resourceProvider);
}
}
@Component
@RequiredArgsConstructor
public class ResourceManager {
private final ObjectProvider<ExpensiveResource> resourceProvider;
public void doWork() {
// 매번 새 인스턴스 획득
ExpensiveResource resource = resourceProvider.getObject();
try {
resource.process();
} finally {
resource.close(); // Prototype은 직접 정리
}
}
}
SmartLifecycle: 시작·종료 순서 제어
여러 Bean의 시작·종료 순서를 세밀하게 제어해야 할 때 SmartLifecycle을 사용한다.
@Component
@Slf4j
public class MessageConsumer implements SmartLifecycle {
private volatile boolean running = false;
private final KafkaConsumer<String, String> consumer;
@Override
public void start() {
log.info("메시지 소비자 시작");
running = true;
// 메시지 소비 시작
consumer.subscribe(List.of("orders"));
}
@Override
public void stop() {
log.info("메시지 소비자 중지 — Graceful Shutdown");
running = false;
consumer.wakeup();
}
@Override
public boolean isRunning() {
return running;
}
@Override
public int getPhase() {
// 낮은 값이 먼저 시작, 나중에 종료
// DB(phase=0) → Cache(phase=10) → Consumer(phase=20)
return 20;
}
@Override
public boolean isAutoStartup() {
return true;
}
@Override
public void stop(Runnable callback) {
stop();
callback.run(); // 비동기 종료 완료 알림
}
}
실전: @EventListener로 생명주기 이벤트 활용
@Component
@Slf4j
public class AppLifecycleListener {
@EventListener(ApplicationReadyEvent.class)
public void onReady() {
log.info("✅ 애플리케이션 완전 준비됨 — 트래픽 수신 가능");
// 헬스체크 활성화, 서비스 레지스트리 등록 등
}
@EventListener(ContextRefreshedEvent.class)
public void onRefreshed() {
log.info("🔄 컨텍스트 리프레시 완료");
}
@EventListener(ContextClosedEvent.class)
public void onClosed() {
log.info("🛑 컨텍스트 종료 — 리소스 정리 시작");
// 서비스 레지스트리 해제, 외부 연결 종료 등
}
}
안티패턴과 주의점
| 안티패턴 | 문제점 | 해결책 |
|---|---|---|
| 생성자에서 무거운 작업 | DI 실패 시 디버깅 어려움 | @PostConstruct로 분리 |
| Prototype @PreDestroy 기대 | 호출 안 됨, 리소스 누수 | try-finally로 직접 정리 |
| Singleton에 Prototype 직접 주입 | 항상 같은 인스턴스 사용됨 | ObjectProvider 또는 @Lookup |
| BeanPostProcessor에 @Autowired | 아직 처리되지 않은 Bean 접근 | 생성자 주입만 사용 |
마무리
Spring Bean Lifecycle은 프레임워크의 근본 메커니즘이다. 초기화 콜백으로 캐시를 워밍하고, BeanPostProcessor로 커스텀 프록시를 만들며, SmartLifecycle로 시작·종료 순서를 제어한다. Spring Boot 초기화 전략과 함께 애플리케이션 시작 과정을 최적화하고, Actuator 커스텀 엔드포인트에서 Bean 상태를 실시간 모니터링할 수 있다.