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

실용적인 테스트 가이드: @Mock, @MockBean, @Spy, @SpyBean, @InjectMocks

테스트 시 의존성 주입(Dependency Injection)과 Mockito Spring 애플리케이션을 개발하다 보면, 테스트 코드에서 실제 빈(Bean)을 사용하지…

1주 ago

실용적인 테스트 가이드: Layered Architecture 레이어드 아키텍처

들어가며 스프링 기반 프로젝트에서 좋은 설계 구조와 테스트 전략은 소프트웨어 품질과 유지보수성에 직결됩니다. 최근 학습한…

1주 ago

읽기 좋은 코드를 작성하는 사고법: 논리, 사고의 흐름과 SOLID

들어가며 코드를 작성할 때 종종 "이 로직을 어떻게 단순하고 읽기 쉽게 표현할 수 있을까?" 고민하게…

4주 ago

읽기 좋은 코드를 작성하는 사고법: 추상과 구체

들어가며 코드를 작성하다 보면 "왜 이 코드는 한눈에 이해가 안 될까?" 하는 고민을 종종 하게…

1개월 ago

HTTP 상태코드 총정리 서버-클라이언트 간 명확한 의사소통

HTTP 상태코드란 무엇인가? HTTP 상태코드(HTTP Status Code)는 서버가 클라이언트의 요청을 처리한 결과를 수치화된 코드로 나타내는…

1개월 ago

HTTP 메서드 완벽 가이드 GET, POST, PUT, PATCH, DELETE 등 총정리

HTTP란 무엇인가? HTTP(Hypertext Transfer Protocol)는 웹에서 데이터를 주고받기 위해 사용하는 응용 계층 프로토콜입니다. 우리가 브라우저에서…

1개월 ago