Categories: Spring

Spring 스프링 빈 생명주기(Bean Lifecycle)와 콜백(Callback)

스프링 빈이란?

스프링 프레임워크에서 “스프링 빈(Bean)”이란, IoC(Inversion of Control) 컨테이너가 직접 관리하는 객체를 말합니다. @Component, @Service, @Repository, @Controller 등과 같이 컴포넌트 스캔 대상으로 등록되거나, 자바 설정(@Configuration, @Bean)을 통해 생성되어 IoC 컨테이너에 의해 관리되는 객체들이 스프링 빈이 됩니다.

스프링은 이 빈들을 생성, 의존성 주입, 초기화, 사용, 종료의 과정을 거쳐서 lifecycle을 관리합니다. 이러한 과정을 “스프링 빈 생명주기(Bean Lifecycle)”라고 부르며, 스프링은 이 생명주기에 개입할 수 있는 여러 가지 “콜백(callback) 지점”을 제공합니다.

스프링 빈 생명주기의 전체 흐름

스프링 빈의 생명주기는 크게 다음과 같은 단계를 거칩니다.

  1. 스프링 컨테이너 생성
    • ApplicationContext, AnnotationConfigApplicationContext 등의 구체적인 컨테이너가 생성됩니다.
  2. 빈(Bean) 생성(Instantiating)
    • 스프링 컨테이너는 필요한 클래스의 인스턴스를 생성합니다.
    • 예: new SomeService() 또는 CGLIB 등 동적 프록시를 사용해 생성.
  3. 의존성 주입(Dependency Injection)
    • 생성된 빈의 필드, 생성자, Setter 등에 필요한 다른 빈을 주입합니다.
    • @Autowired, @Inject, 생성자 주입 등을 통해 진행됩니다.
  4. 빈 초기화(Initialization) 콜백
    • InitializingBean 인터페이스의 afterPropertiesSet() 메서드,
    • @PostConstruct 애노테이션이 붙은 메서드,
    • initMethod 설정 등을 통한 사용자 정의 초기화 로직이 호출됩니다.
  5. 빈 후처리(Bean Post-Processing)
    • BeanPostProcessor 인터페이스를 구현한 클래스들이 빈이 생성된 직후와 초기화 직전에 개입할 수 있습니다.
    • AOP, 프록시 생성, 로깅, 트랜잭션 설정 등을 수행할 수 있는 훅(Hook)입니다.
  6. 빈 사용(Ready for Use)
    • 사용자 코드가 실제로 빈을 사용합니다.
  7. 종료(Destruction) 콜백
    • 어플리케이션 컨텍스트가 종료될 때, 빈을 마무리(소멸)하는 단계에서 호출됩니다.
    • DisposableBean 인터페이스의 destroy() 메서드,
    • @PreDestroy 애노테이션 메서드,
    • destroyMethod 설정 등을 사용할 수 있습니다.
  8. 컨테이너 종료
    • 최종적으로 스프링 컨테이너가 종료되며 모든 빈을 정리합니다.

이번 글에서는 특히 초기화(Initialization) 콜백소멸(Destruction) 콜백을 중심으로, 각 단계에서 어떤 일이 일어나는지 구체적으로 살펴보겠습니다.

생명주기 콜백을 위한 대표적인 방법

스프링은 빈의 생명주기에 다양한 지점에서 콜백 로직을 주입할 수 있도록 여러 기법을 제공합니다. 대표적으로 아래 네 가지를 주로 사용합니다.

  1. 인터페이스 상속
    • InitializingBean, DisposableBean을 구현.
  2. 애노테이션 사용
    • @PostConstruct, @PreDestroy.
  3. XML, 자바 설정에서 initMethod / destroyMethod 지정
    • @Bean(initMethod = “init”, destroyMethod = “cleanup”).
  4. 빈 후처리기(BeanPostProcessor)
    • BeanPostProcessor 인터페이스 구현.

이 네 가지 방법은 사용 목적과 편의성 면에서 차이가 있습니다. 가장 권장되는 방식은 자바 표준 애노테이션인 @PostConstruct, @PreDestroy를 활용하는 것입니다. 이유는 다음과 같습니다:

  • 표준화: JSR-250(자바 표준)에서 제공하는 애노테이션이므로 스프링 이외의 환경에서도 사용 가능.
  • 코드 간결성: 메서드 위에 애노테이션만 붙이면 되므로 가독성이 좋고, 스프링 설정에 종속되지 않음.
  • 유연성: 다른 설정을 해줄 필요 없이 간편하게 초기화와 소멸 로직을 작성 가능.

나머지 방법들도 상황에 따라 사용할 수 있으니 각각의 특성을 알아봅시다.

초기화(Initialization) 콜백

@PostConstruct 애노테이션

스프링에서 가장 권장하는 초기화 방식으로, 다음과 같이 사용할 수 있습니다.

Java
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.stereotype.Component;

@Component
public class ExampleService {

    public ExampleService() {
        System.out.println("ExampleService 생성자 호출");
    }

    @PostConstruct
    public void init() {
        System.out.println("ExampleService init() 호출 - 의존성 주입 완료 후 초기화 작업");
    }

    // @PreDestroy는 소멸 콜백에서 설명
}
Java

@PostConstruct가 붙은 메서드는 빈 생성 및 의존관계 주입이 완료된 직후 호출됩니다.

  • 초기화 시 필요한 설정, 메모리 할당, 별도 스레드 혹은 리소스 연결 등을 수행할 수 있습니다.
  • 예: DB Connection Pool 초기화, 캐시 메모리 초기화 등.

InitializingBean 인터페이스

InitializingBean 인터페이스의 afterPropertiesSet()을 구현하는 방법도 있습니다.

Java
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

@Component
public class ExampleService implements InitializingBean {

    public ExampleService() {
        System.out.println("ExampleService 생성자 호출");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("ExampleService.afterPropertiesSet() 호출");
    }
}
Java

  • afterPropertiesSet() 역시 의존성이 모두 주입된 뒤에 호출됩니다.
  • 스프링에 종속적인 인터페이스라는 단점이 있어, 최근에는 잘 쓰이지 않고 @PostConstruct가 더 자주 사용됩니다.

자바 설정에서 initMethod 설정하기

@Bean 애노테이션을 사용해 메서드를 스프링 빈으로 등록하면서, 직접 초기화 메서드를 지정할 수도 있습니다.

Java
@Configuration
public class AppConfig {

    @Bean(initMethod = "init")
    public ExampleService exampleService() {
        return new ExampleService();
    }
}
Java

ExampleService 클래스 안에서 init() 메서드를 만든 뒤 다음과 같이 작성합니다.

Java
public class ExampleService {
    public void init() {
        System.out.println("ExampleService init() 호출 - 자바 설정에서 initMethod 지정");
    }
}
Java

  • XML 설정에서도 <bean id=”exampleService” class=”ExampleService” init-method=”init” />처럼 지정할 수 있습니다.
  • 코드 자체를 스프링에 종속시키지 않고, 설정 파일로 관리할 수 있다는 장점이 있습니다.
  • 하지만 @PostConstruct가 보편화되면서 최근에는 많이 사용되지 않습니다.

소멸(Destruction) 콜백

빈이 더 이상 필요 없어지고 컨테이너가 종료되거나, 수동으로 빈을 제거해야 하는 상황에서 소멸 콜백이 호출됩니다.

@PreDestroy 애노테이션

@PostConstruct와 짝을 이루는 @PreDestroy 애노테이션입니다.

Java
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.stereotype.Component;

@Component
public class ExampleService {

    @PostConstruct
    public void init() {
        System.out.println("초기화 로직 실행");
    }

    @PreDestroy
    public void destroy() {
        System.out.println("소멸 로직 실행 - 자원 반납 등 정리");
    }
}
Java

  • 컨테이너가 종료될 때, @PreDestroy가 붙은 메서드가 자동으로 호출됩니다.
  • DB 커넥션 반납, 파일 스트림 종료, 캐시 정리, 스레드 풀 종료 등 마무리 작업을 수행하기 좋습니다.

DisposableBean 인터페이스

DisposableBean은 destroy() 메서드를 오버라이드하여 소멸 로직을 작성합니다.

Java
import org.springframework.beans.factory.DisposableBean;

public class ExampleService implements DisposableBean {
    @Override
    public void destroy() throws Exception {
        System.out.println("ExampleService.destroy() 호출");
    }
}
Java

  • InitializingBean과 마찬가지로 스프링 종속적인 방식이므로 권장도는 낮습니다.

자바 설정에서 destroyMethod 설정하기

자바 설정이나 XML 설정에서 destroyMethod를 지정할 수도 있습니다.

Java
@Configuration
public class AppConfig {

    @Bean(initMethod = "init", destroyMethod = "cleanup")
    public ExampleService exampleService() {
        return new ExampleService();
    }
}
Java

ExampleService 안에 cleanup() 메서드를 두어 자원 정리를 합니다.

Java
public class ExampleService {
    public void cleanup() {
        System.out.println("cleanup() 호출 - destroyMethod에서 지정");
    }
}
Java

BeanPostProcessor를 통한 후처리

초기화(Initialization)와 소멸(Destruction) 콜백 외에도, 스프링은 빈의 생명주기에 개입할 수 있는 후처리기(BeanPostProcessor) 개념을 제공합니다.

BeanPostProcessor를 구현하면, 스프링 빈이 생성된 직후(postProcessBeforeInitialization)와 초기화 직후(postProcessAfterInitialization) 등 여러 시점에 후처리 로직을 삽입할 수 있습니다.

대표적인 사용 예시

  • AOP(관점 지향 프로그래밍): 프록시를 생성하거나 메서드 호출을 가로채는 로직.
  • 로깅: 빈 생성 로깅, 특정 메서드 호출 모니터링.
  • 커스텀 애노테이션 처리: 특정 애노테이션이 붙은 빈을 발견해 추가 설정을 하는 경우 등.

예시 코드

Java
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {

    // 초기화(beforeInitialization) 직전 콜백
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof ExampleService) {
            System.out.println("[BeanPostProcessor] " + beanName + " 초기화 전 로직 실행");
        }
        return bean;
    }

    // 초기화(afterInitialization) 직후 콜백
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof ExampleService) {
            System.out.println("[BeanPostProcessor] " + beanName + " 초기화 후 로직 실행");
        }
        return bean;
    }
}
Java

  • postProcessBeforeInitialization → 빈 초기화 작업(@PostConstruct, initMethod, InitializingBean) 등이 실행되기 직전에 호출.
  • postProcessAfterInitialization → 빈 초기화가 완료된 직후에 호출.

BeanPostProcessor를 활용하면 특정 빈을 대상으로 동적 프록시를 생성해 AOP 처리를 하거나, 추가 검증 로직을 넣는 등 다양한 기능을 구현할 수 있습니다.

BeanFactoryPostProcessor와의 차이점

헷갈리기 쉬운 또 다른 인터페이스로 BeanFactoryPostProcessor가 있습니다. 이는 “빈 공장(BeanFactory)” 자체를 후처리하는 로직을 넣을 수 있는 방법으로, 실제 빈 인스턴스가 아닌 빈 정의(BeanDefinition) 정보를 다룹니다.

  • 빈이 생성되기 전 단계에서 작동하여, 빈 설정 메타데이터를 조정하는 역할을 합니다.
  • 예를 들어, 프로퍼티 값을 동적으로 변경하거나 특정 설정 조건에 따라 BeanDefinition 자체를 등록/변경/삭제할 수 있습니다.

반면에 BeanPostProcessor빈 인스턴스(실체 객체)가 생성된 후를 다루는 것이 차이점입니다.

정리 및 모범 사용 예

  1. 초기화와 소멸: @PostConstruct, @PreDestroy를 권장
    • 가장 간편하고 표준 애노테이션이므로 다른 환경에서도 사용하기 쉽습니다.
    • 스프링에만 종속되지 않으며, 코드가 깔끔합니다.
  2. 초기화, 소멸 콜백 메서드명은 알아보기 쉽게
    • init(), cleanup(), close(), destroy() 등 명확한 이름을 사용하세요.
    • 타 개발자와 협업 시 의도를 더 분명히 표현할 수 있습니다.
  3. 빈 후처리기(BeanPostProcessor) 사용 시 주의
    • AOP, 로깅, 프록시 생성에 유용하지만 복잡도가 증가할 수 있습니다.
    • 전체 애플리케이션의 성능이나 디버깅 난이도에 영향을 줄 수 있으므로 주의 깊게 사용하세요.
  4. BeanFactoryPostProcessor와 구분
    • BeanPostProcessor는 빈 인스턴스가 생성된 후 단계에서 작동,
    • BeanFactoryPostProcessor는 빈 정의 메타데이터를 다루는 단계에서 작동합니다.
  5. 실제 프로젝트 적용 시
    • 라이프사이클 콜백에 복잡한 로직을 너무 많이 넣는 것은 좋지 않습니다(초기화가 오래 걸리면 어플리케이션 구동 속도에 악영향).
    • 가능하면 “리소스 할당이나 외부 서비스 연결은 필요한 시점에” 지연 로딩으로 처리하는 것도 고려해 보세요.

예제 코드 전체

여기서는 @Configuration 클래스와 @PostConstruct, @PreDestroy를 함께 사용하는 예시 코드를 보여드리겠습니다.

Java
// ExampleService.java
package com.example.lifecycle;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

public class ExampleService {

    public ExampleService() {
        System.out.println("ExampleService 생성자 호출");
    }

    @PostConstruct
    public void init() {
        System.out.println("ExampleService init() 호출");
        // 초기화 시 필요한 로직 (예: DB 연결, 캐시 로딩 등)
    }

    public void doSomething() {
        System.out.println("ExampleService.doSomething() 실행");
    }

    @PreDestroy
    public void cleanup() {
        System.out.println("ExampleService cleanup() 호출");
        // 소멸 시 필요한 로직 (예: 리소스 해제, 스레드 종료 등)
    }
}
Java

Java
// AppConfig.java
package com.example.lifecycle;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public ExampleService exampleService() {
        return new ExampleService();
    }
}
Java

Java
// Main.java
package com.example.lifecycle;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        System.out.println("===> 스프링 컨테이너 생성");
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

        System.out.println("===> 스프링 컨테이너에서 ExampleService 가져오기");
        ExampleService service = context.getBean(ExampleService.class);
        service.doSomething();

        System.out.println("===> 스프링 컨테이너 종료");
        context.close();
    }
}
Java

실행 결과

Java
===> 스프링 컨테이너 생성
ExampleService 생성자 호출
ExampleService init() 호출
===> 스프링 컨테이너에서 ExampleService 가져오기
ExampleService.doSomething() 실행
===> 스프링 컨테이너 종료
ExampleService cleanup() 호출
Java

  • 생성자와 @PostConstruct가 순차적으로 호출된 뒤, 프로그램 종료 시점에 @PreDestroy가 호출됩니다.

마무리

스프링 빈 생명주기와 콜백을 잘 이해하고 사용하면, 애플리케이션 전반에 걸쳐 더 견고하고 일관성 있는 초기화와 자원 관리를 할 수 있습니다.

가장 핵심적인 포인트는 다음과 같습니다.

  1. 빈 생명주기는 “생성 → 의존성 주입 → 초기화 → 사용 → 소멸” 단계를 거친다.
  2. 초기화와 소멸에는 @PostConstruct, @PreDestroy 애노테이션을 가장 권장한다.
  3. 필요에 따라 BeanPostProcessor, BeanFactoryPostProcessor 등 후처리 기능을 통해 각종 커스텀 로직을 삽입할 수 있다.
  4. 리소스 할당 및 해제 로직을 적절히 처리해 어플리케이션 성능과 안정성을 유지해야 한다.

위 내용을 숙지하면, 스프링 빈 관리에 대해 더 깊이 있게 이해하고 체계적으로 애플리케이션을 구성할 수 있을 것입니다. 스프링은 애플리케이션 전반을 유연하게 구성해주기 때문에, Bean Lifecycle에 대한 이해는 필수적입니다. 꼭 알아두시고 다양한 프로젝트에서 활용해 보세요!

suover

Recent Posts

Network 인터넷 네트워크란? 개념과 구조 완벽 정리

인터넷 네트워크란? "인터넷(Internet)"이라는 단어는 "인터네트워크(Internetwork)"의 줄임말입니다. 즉, 여러 개의 네트워크가 상호 연결되어 전 세계적으로 하나의…

3주 ago

Spring 스프링 빈 스코프(Bean Scope) 개념 정리

스프링 빈(Spring Bean)과 IoC 컨테이너 스프링 프레임워크의 핵심 철학은 IoC(Inversion of Control) 컨테이너를 통해 객체(빈,…

4주 ago

Spring 스프링 의존성 주입(Dependency Injection)이란?

의존성 주입(Dependency Injection)이란? 프로그램을 개발하다 보면, 여러 클래스나 객체들은 서로 필요한 기능을 사용하기 위해 관계를…

1개월 ago

Spring 스프링 컴포넌트 스캔(Component Scan)이란?

컴포넌트 스캔이란? 컴포넌트 스캔(Component Scan)은 스프링 프레임워크가 특정 패키지를 탐색하면서, 스캔 대상에 해당하는 클래스를 찾아…

2개월 ago

Spring 스프링 빈(Bean)이란?

스프링 빈이란? 스프링 빈(Spring Bean)은 스프링 IoC(Inversion of Control) 컨테이너가 관리하는 자바 객체를 의미합니다. 간단히…

2개월 ago

Spring 스프링 컨테이너(Spring Container)란?

스프링 컨테이너(Spring Container)란? 스프링 컨테이너는 스프링 프레임워크에서 가장 핵심적인 부분으로, IoC(Inversion of Control) 개념을 기반으로…

2개월 ago