[SpringBoot] 빈 라이프 사이클

나른한 개발자·2026년 1월 7일

f-lab

목록 보기
18/44

Bean

  • 빈(Bean)은 스프링 컨테이너에 의해 관리되는 재사용가능한 소프트웨어 컴포넌트를 말한다.
  • 스프링 컨테이너에 의해 관리된다. 컨테이너는 빈의 생명주기를 관리하고 의존성을 주입한다.
  • 기본적으로 빈은 싱글톤 스코프를 가진다. 동일한 빈 정의에 대해 스프링 컨테이너는 단일 인스턴스만 생성하고 애플리케이션 내에서 공유한다. 싱글톤 외에도 프로토타입, 요청, 세션 등 다양한 스코프가 있다.
  • 의존성 주입을 통해 빈 사이의 의존 관계가 설정된다.
  • XML, Java Config, 어노테이션 등 다양한 방식으로 빈을 정의할 수 있다.

빈의 생명주기 (Bean Life Cycle)

일반적인 싱글톤 타입의 빈 생명주기는 다음과 같다.

스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸 전 콜백 → 스프링 종료

1. Bean 인스턴스화

Spring IoC 컨테이너는 XML 파일 또는 Java 설정 파일에서 정의된 Bean을 인스턴스화 한다.

2. 의존성 주입

Bean 정의에 따라 의존성이 주입된다. Spring은 필요한 속성이나 의존성을 주입하여 Bean을 완전히 구성한다.

3. Aware 인터페이스 처리

Bean은 기본적으로 컨테이너에 대한 정보를 알지 못한다. 자신을 관리하고 있는 컨테이너 정보나 어떤 이름으로 관리되고 있는지 또는 환경설정에 대한 정보를 알 수 없다. 하지만 필요에 따라 이러한 정보를 알아야하는 경우 Aware 인터페이스를 구현하여 정밀한 제어를 할 수 있다.

컨테이너가 빈을 인스턴스 및 의존성 주입을 한 후 해당 빈이 Aware인터페이스를 구현했는지 확인한다. 구현했다면 관련 객체를 메서드를 통해 전달한다.

@Component
public class MyCustomBean implements ApplicationContextAware {

    private ApplicationContext context;

    // 컨테이너가 이 메서드를 호출하여 ApplicationContext를 주입함
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }

    public void checkOtherBean() {
        // 주입받은 context를 통해 다른 빈을 직접 찾거나 정보를 확인 가능
        boolean exists = context.containsBean("someOtherBean");
        System.out.println("Other bean exists: " + exists);
    }
}

위 코드는 애플리케이션 전체 컨테이너 정보를 가져와야하는 경우의 예시이다. 컨테이너가 setApplicationContext()를 호출하여 관련 객체를 넣어주고 있다.

4. BeanPostProcessor - 전처리

Bean이 초기화 되기 전에 가로채서 전처리를 해줄 수 있다. 빈이 BeanFactoryAware 인터페이스를 구현한 경우, 초기화 전에 추가적인 작업을 수행한다.

예를 들어 Spring AOP의 경우 내부적으로 BeanPostProcessor의 구현체인 AnnotationAwareAspectJAutoProxyCreator를 빈으로 자동 등록하고 있다. 그러면 이 빈 후처리기가 @Transactional 또는 Aspect 설정이 있는지 확인 후 프록시를 생성한다.

5. 초기화 메서드 호출

Bean 정의에서 init-method가 지정된 경우, 이 초기화 메서드가 호출된다. 실행되는 순서는 아래와 같다.

5-1. @PostConstruct 실행

5-2. InitializingBean 인터페이스

5-3. 커스텀 init-method

6. BeanPostProcessor - 후처리

초기화 후에 실행할 작업이 있는 경우, BeanPostProcessor의 postProcessAfterInitialization() 메서드가 호출된다.

7. 빈 소멸

Spring 컨테이너가 종료될 때, Bean이 DisposableBean 인터페이스를 구현한 경우 destroy() 메서드가 호출되거나, destroy-method로 지정된 메서드가 호출되어 Bean이 소멸된다.

7-1. @PreDestory 실행

7-2. DisposableBean 인터페이스의 destory() 실행

7-3. 커스텀 destroy-method 실행



Bean의 생명주기 콜백

이제 생명주기를 배웠으니 이 Bean 의 생명주기 중간중간에 내가 원하는 코드를 심어볼 수 있다. 생명주기 콜백은 크게 3가지가 있다.

  • 어노테이션: 스프링 권장 방식이다. @PostConstruct, @PreDestroy가 있다. 자바 표준이라 스프링이 아닌 다른 컨테이너에서도 동작한다. 외부 라이브러리에는 적용하지 못한다.
  • 인터페이스: InitializingBean, DisposableBean 인터페이스를 구현하는 방법이다. 스프링에 의존하게되므로 최근엔 거의 사용하지 않는다.
  • 커스텀 메서드 작성 후 설정 정보 지정: 외부 라이브러리처럼 코드를 수정할 수 없을 때 사용한다.

@PostConstruct

@PostConstruct는 Spring에서 빈(Bean)의 초기화 작업을 수행할 때 사용하는 애노테이션이다. 이 애노테이션이 붙은 메서드는 해당 빈이 생성/의존성 주입이 완료된 후 자동으로 호출된다. 이를 통해 빈의 초기화 과정에서 추가적인 설정이나 초기화 작업을 수행할 수 있습니다.

특히 실무에서 주로 사용하는게 key 값이다. 물론 @Value 를 통해 처음부터 설정해도 좋지만, 해당 Key 값이 DB 심지어 외부 URL을 통해서 얻어야한다면? 예를 들어 API_KEY 같은 것은 실시간으로 바뀌어야 할 수 있어야한다. 이러한 코드들은 초기화 이후에 유저가 사용하기 전에 외부에서 가져와야하는데 이때 사용하는것이 @PostConstruct 이다.

@Component
public class ExternalApiConnector {

    private final ApiClient apiClient;

    private String authToken;

    public ExternalApiConnector(ApiClient apiClient) {
        this.apiClient = apiClient;
    }

    @PostConstruct
    public void initializeConnection() {
        this.authToken = apiClient.authenticate();
        // 인증 토큰을 받아서 이후 요청에 사용
    }

    public void callExternalService() {
        apiClient.callService(authToken);
    }
}

이렇게 사용하면 Spring boot 의 Bean 이 생성된 후 초기화 콜백이 실행되며, 해당 Bean 을 사용하기 전에 authToken 에 대해서 값이 세팅된다.

@PreDestroy

@PreDestroy는 Spring에서 빈(Bean)이 소멸되기 직전에 호출되는 메서드를 지정하는 애노테이션이다. 주로 리소스를 정리하거나, 연결을 종료하거나, 애플리케이션 종료 전에 꼭 처리해야 하는 작업을 수행할 때 사용된다. @PreDestroy는 @PostConstruct와 반대로, 빈이 컨테이너에서 제거되기 전에 마지막으로 실행되어야 하는 작업을 정의한다.

아까와는 다르게 서버에서 대상에게 마지막에 전달해야하거나 저장할게 있다면 어떻게 해야할까.

예를 들어 애플리케이션이 종료되기 전에 캐시에 저장된 데이터를 파일이나 데이터베이스에 저장하는 등의 작업을 수행할 수 있다.

@Component
public class CacheService {

    private Map<String, String> cache = new HashMap<>();

    public void putInCache(String key, String value) {
        cache.put(key, value);
    }

    public String getFromCache(String key) {
        return cache.get(key);
    }

    @PreDestroy
    public void saveCacheToDatabase() {
        // 캐시 데이터를 데이터베이스에 저장하는 로직
        System.out.println("Saving cache data to database...");
        // 실제 데이터베이스 저장 로직
    }
}

초기화 로직이 따로 있는 이유

객체 생성 시점(생성자 호출) 시점에는 의존관계 주입이 완료되지 않은 상태이다. 따라서 주입받은 객체를 활용하여 로직을 수행하려면 모든 의존성이 연결된 후인 초기화 콜백 시점에서 시작해야 안전하기 때문이다. 또한 생성자로부터 비즈니스 로직을 분리하기 위함도 있다.

참고링크

profile
Start fast to fail fast

0개의 댓글