전체 목록
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 { }

초기화 방법 비교

방법애노테이션/인터페이스권장 여부특징
@PostConstructJSR-250 표준권장코드가 가장 간결, Spring 비종속
@PreDestroyJSR-250 표준권장코드가 가장 간결, Spring 비종속
InitializingBeanSpring 인터페이스보통Spring에 종속, 인터페이스 구현 필요
DisposableBeanSpring 인터페이스보통Spring에 종속, 인터페이스 구현 필요
@Bean(initMethod)Spring 애노테이션외부 라이브러리코드 수정 없이 적용, XML 방식과 동일
@Bean(destroyMethod)Spring 애노테이션외부 라이브러리코드 수정 없이 적용

생명주기 단계 요약

순서단계설명
1컨테이너 시작ApplicationContext 초기화
2빈 인스턴스 생성생성자 호출
3의존성 주입@Autowired, 생성자/setter 주입
4Aware 콜백BeanNameAware, ApplicationContextAware
5postProcessBeforeInitializationBeanPostProcessor (AOP 등)
6초기화 콜백@PostConstructafterPropertiesSet()init-method
7postProcessAfterInitializationBeanPostProcessor (AOP 프록시 생성 등)
8빈 사용애플리케이션 로직 실행
9소멸 콜백@PreDestroydestroy()destroy-method
10컨테이너 종료ApplicationContext.close()