SpringMedium#67
Spring Bean의 생명주기(Lifecycle)를 설명해주세요.
#Spring#Bean#생명주기#IoC
힌트
컨테이너 시작 → 빈 생성 → 의존성 주입 → 초기화 → 사용 → 소멸 흐름입니다.
정답 및 해설
Spring Bean의 생명주기(Lifecycle)를 설명해주세요.
Spring Bean의 생명주기는 Spring 컨테이너가 빈을 생성하고, 초기화하고, 사용하고, 최종적으로 소멸시키는 전체 과정을 말합니다. 이 과정에서 개발자는 여러 시점에 커스텀 로직을 삽입할 수 있습니다. 초기화 시점에는 DB 커넥션 풀 설정이나 캐시 워밍업을, 소멸 시점에는 리소스 정리나 연결 종료 작업을 수행합니다.
Spring Bean 생명주기 전체 흐름
1. Spring 컨테이너 시작
2. 빈 정의(BeanDefinition) 로드
3. 빈 인스턴스 생성 (기본 생성자 호출)
4. 의존성 주입 (DI)
5. BeanNameAware, BeanFactoryAware 등 Aware 인터페이스 콜백
6. BeanPostProcessor.postProcessBeforeInitialization()
7. @PostConstruct 또는 InitializingBean.afterPropertiesSet() 또는 init-method
8. BeanPostProcessor.postProcessAfterInitialization()
9. 빈 사용 (애플리케이션 실행)
10. 컨테이너 종료
11. @PreDestroy 또는 DisposableBean.destroy() 또는 destroy-method
단계별 상세 설명
1단계: 빈 인스턴스 생성
Spring 컨테이너는 BeanDefinition을 기반으로 빈을 생성합니다. 기본적으로 기본(no-arg) 생성자를 사용하며, 생성자 주입인 경우 해당 생성자를 호출합니다.
@Component
public class DatabaseConnectionPool {
private final String url;
private final int maxConnections;
// 생성자 주입: 이 시점에 빈 생성 + DI가 동시에 발생
public DatabaseConnectionPool(
@Value("${db.url}") String url,
@Value("${db.maxConnections:10}") int maxConnections) {
this.url = url;
this.maxConnections = maxConnections;
System.out.println("1. 빈 생성 완료: " + url);
}
}
2단계: Aware 인터페이스 콜백
Spring 내부 객체에 접근이 필요할 때 사용합니다. (일반적으로 거의 사용하지 않음)
@Component
public class MyBean implements BeanNameAware, ApplicationContextAware {
private String beanName;
private ApplicationContext applicationContext;
@Override
public void setBeanName(String name) {
// 이 빈의 이름을 Spring이 주입
this.beanName = name;
System.out.println("2-1. BeanNameAware: " + name);
}
@Override
public void setApplicationContext(ApplicationContext ctx) {
// ApplicationContext를 Spring이 주입
this.applicationContext = ctx;
System.out.println("2-2. ApplicationContextAware");
}
}
3단계: BeanPostProcessor (초기화 전)
모든 빈의 초기화 전에 실행됩니다. Spring 내부에서 AOP 프록시 생성 등에 활용됩니다.
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("3. BeforeInit: " + beanName);
// 빈을 수정하거나 프록시로 감쌀 수 있음
return bean; // 수정된 빈 또는 원본 빈 반환
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("5. AfterInit: " + beanName);
return bean;
}
}
4단계: 초기화 콜백
방법 1: @PostConstruct (권장)
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
@Component
public class CacheService {
private Map<String, Object> cache;
@PostConstruct
public void init() {
// DI 완료 후 초기화 로직
System.out.println("4. @PostConstruct - 캐시 초기화");
cache = new HashMap<>();
loadInitialData();
}
private void loadInitialData() {
// DB에서 초기 데이터 로드
System.out.println("초기 캐시 데이터 로드 완료");
}
@PreDestroy
public void cleanup() {
System.out.println("7. @PreDestroy - 캐시 정리");
cache.clear();
}
}
방법 2: InitializingBean 인터페이스
@Component
public class SchedulerService implements InitializingBean, DisposableBean {
private ScheduledExecutorService executor;
@Override
public void afterPropertiesSet() throws Exception {
// @PostConstruct와 동일한 시점에 실행
System.out.println("4. afterPropertiesSet - 스케줄러 시작");
executor = Executors.newScheduledThreadPool(5);
}
@Override
public void destroy() throws Exception {
// @PreDestroy와 동일한 시점에 실행
System.out.println("7. destroy - 스케줄러 종료");
executor.shutdown();
}
}
방법 3: @Bean의 initMethod, destroyMethod 속성
@Configuration
public class AppConfig {
@Bean(initMethod = "start", destroyMethod = "stop")
public ConnectionPool connectionPool() {
return new ConnectionPool();
}
}
// 외부 라이브러리 클래스처럼 코드 수정이 불가능한 경우 유용
public class ConnectionPool {
public void start() {
System.out.println("4. start() - 커넥션 풀 시작");
}
public void stop() {
System.out.println("7. stop() - 커넥션 풀 종료");
}
}
완전한 생명주기 실증 예시
@Component
public class LifecycleDemoBean
implements BeanNameAware, InitializingBean, DisposableBean {
private String beanName;
public LifecycleDemoBean() {
System.out.println("① 생성자 호출");
}
@Autowired
public void setDependency(SomeDependency dep) {
System.out.println("② 의존성 주입 완료");
}
@Override
public void setBeanName(String name) {
this.beanName = name;
System.out.println("③ BeanNameAware.setBeanName: " + name);
}
// BeanPostProcessor.postProcessBeforeInitialization 은 외부 클래스에서 실행됨
// ④ BeanPostProcessor.beforeInit (외부 BeanPostProcessor가 실행)
@PostConstruct
public void postConstruct() {
System.out.println("⑤ @PostConstruct");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("⑥ InitializingBean.afterPropertiesSet");
}
// ⑦ BeanPostProcessor.afterInit (외부 BeanPostProcessor가 실행)
// ⑧ 빈 사용 (애플리케이션 로직 실행)
@PreDestroy
public void preDestroy() {
System.out.println("⑨ @PreDestroy");
}
@Override
public void destroy() throws Exception {
System.out.println("⑩ DisposableBean.destroy");
}
}
실행 결과:
① 생성자 호출
② 의존성 주입 완료
③ BeanNameAware.setBeanName: lifecycleDemoBean
④ BeanPostProcessor.beforeInit
⑤ @PostConstruct
⑥ InitializingBean.afterPropertiesSet
⑦ BeanPostProcessor.afterInit
[애플리케이션 실행...]
⑨ @PreDestroy
⑩ DisposableBean.destroy
실전 활용 사례
@PostConstruct 활용 - 초기화 작업
@Service
public class ProductCacheService {
private final ProductRepository productRepository;
private Map<Long, Product> cache = new ConcurrentHashMap<>();
public ProductCacheService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@PostConstruct
public void warmUpCache() {
// 애플리케이션 시작 시 인기 상품 캐시 워밍
List<Product> popularProducts = productRepository.findTop100ByOrderBySalesCountDesc();
popularProducts.forEach(p -> cache.put(p.getId(), p));
System.out.println("캐시 워밍 완료: " + cache.size() + "개 상품");
}
@PreDestroy
public void clearCache() {
cache.clear();
System.out.println("캐시 정리 완료");
}
public Optional<Product> getFromCache(Long id) {
return Optional.ofNullable(cache.get(id));
}
}
@PostConstruct 활용 - 설정 검증
@Component
public class ExternalApiConfig {
@Value("${api.key:}")
private String apiKey;
@Value("${api.url:}")
private String apiUrl;
@PostConstruct
public void validate() {
// 필수 설정 값 검증 (DI 완료 후 실행되므로 @Value 주입됨)
if (apiKey.isBlank()) {
throw new IllegalStateException("api.key 설정이 필요합니다.");
}
if (apiUrl.isBlank()) {
throw new IllegalStateException("api.url 설정이 필요합니다.");
}
System.out.println("외부 API 설정 검증 완료: " + apiUrl);
}
}
@PreDestroy 활용 - 리소스 정리
@Component
public class WebSocketManager {
private final List<WebSocketSession> sessions = new CopyOnWriteArrayList<>();
public void addSession(WebSocketSession session) {
sessions.add(session);
}
@PreDestroy
public void closeAllSessions() {
System.out.println("애플리케이션 종료 - WebSocket 연결 정리 중...");
sessions.forEach(session -> {
try {
session.close(CloseStatus.GOING_AWAY);
} catch (IOException e) {
// 정리 중 예외는 무시 또는 로그만
}
});
sessions.clear();
System.out.println("모든 WebSocket 연결 종료 완료");
}
}
Bean Scope와 생명주기의 관계
// singleton (기본): 컨테이너당 하나의 인스턴스, 컨테이너 생명주기와 동일
@Component
@Scope("singleton") // 기본값이므로 생략 가능
public class SingletonBean { }
// prototype: 요청마다 새 인스턴스, @PreDestroy 호출 안 됨 (Spring이 관리 안 함)
@Component
@Scope("prototype")
public class PrototypeBean {
@PostConstruct
public void init() { System.out.println("프로토타입 초기화"); } // 매 생성 시 호출
@PreDestroy
public void destroy() { } // 호출되지 않음! 직접 관리 필요
}
// request: HTTP 요청마다 새 인스턴스 (웹 스코프)
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestScopedBean { }
초기화 방법 비교
| 방법 | 애노테이션/인터페이스 | 권장 여부 | 특징 |
|---|---|---|---|
@PostConstruct | JSR-250 표준 | 권장 | 코드가 가장 간결, Spring 비종속 |
@PreDestroy | JSR-250 표준 | 권장 | 코드가 가장 간결, Spring 비종속 |
InitializingBean | Spring 인터페이스 | 보통 | Spring에 종속, 인터페이스 구현 필요 |
DisposableBean | Spring 인터페이스 | 보통 | Spring에 종속, 인터페이스 구현 필요 |
@Bean(initMethod) | Spring 애노테이션 | 외부 라이브러리 | 코드 수정 없이 적용, XML 방식과 동일 |
@Bean(destroyMethod) | Spring 애노테이션 | 외부 라이브러리 | 코드 수정 없이 적용 |
생명주기 단계 요약
| 순서 | 단계 | 설명 |
|---|---|---|
| 1 | 컨테이너 시작 | ApplicationContext 초기화 |
| 2 | 빈 인스턴스 생성 | 생성자 호출 |
| 3 | 의존성 주입 | @Autowired, 생성자/setter 주입 |
| 4 | Aware 콜백 | BeanNameAware, ApplicationContextAware 등 |
| 5 | postProcessBeforeInitialization | BeanPostProcessor (AOP 등) |
| 6 | 초기화 콜백 | @PostConstruct → afterPropertiesSet() → init-method |
| 7 | postProcessAfterInitialization | BeanPostProcessor (AOP 프록시 생성 등) |
| 8 | 빈 사용 | 애플리케이션 로직 실행 |
| 9 | 소멸 콜백 | @PreDestroy → destroy() → destroy-method |
| 10 | 컨테이너 종료 | ApplicationContext.close() |