스프링 빈이란?
스프링 프레임워크에서 “스프링 빈(Bean)”이란, IoC(Inversion of Control) 컨테이너가 직접 관리하는 객체를 말합니다. @Component, @Service, @Repository, @Controller 등과 같이 컴포넌트 스캔 대상으로 등록되거나, 자바 설정(@Configuration, @Bean)을 통해 생성되어 IoC 컨테이너에 의해 관리되는 객체들이 스프링 빈이 됩니다.
스프링은 이 빈들을 생성, 의존성 주입, 초기화, 사용, 종료의 과정을 거쳐서 lifecycle을 관리합니다. 이러한 과정을 “스프링 빈 생명주기(Bean Lifecycle)”라고 부르며, 스프링은 이 생명주기에 개입할 수 있는 여러 가지 “콜백(callback) 지점”을 제공합니다.
스프링 빈 생명주기의 전체 흐름
스프링 빈의 생명주기는 크게 다음과 같은 단계를 거칩니다.
- 스프링 컨테이너 생성
- ApplicationContext, AnnotationConfigApplicationContext 등의 구체적인 컨테이너가 생성됩니다.
- 빈(Bean) 생성(Instantiating)
- 스프링 컨테이너는 필요한 클래스의 인스턴스를 생성합니다.
- 예: new SomeService() 또는 CGLIB 등 동적 프록시를 사용해 생성.
- 의존성 주입(Dependency Injection)
- 생성된 빈의 필드, 생성자, Setter 등에 필요한 다른 빈을 주입합니다.
- @Autowired, @Inject, 생성자 주입 등을 통해 진행됩니다.
- 빈 초기화(Initialization) 콜백
- InitializingBean 인터페이스의 afterPropertiesSet() 메서드,
- @PostConstruct 애노테이션이 붙은 메서드,
- initMethod 설정 등을 통한 사용자 정의 초기화 로직이 호출됩니다.
- 빈 후처리(Bean Post-Processing)
- BeanPostProcessor 인터페이스를 구현한 클래스들이 빈이 생성된 직후와 초기화 직전에 개입할 수 있습니다.
- AOP, 프록시 생성, 로깅, 트랜잭션 설정 등을 수행할 수 있는 훅(Hook)입니다.
- 빈 사용(Ready for Use)
- 사용자 코드가 실제로 빈을 사용합니다.
- 종료(Destruction) 콜백
- 어플리케이션 컨텍스트가 종료될 때, 빈을 마무리(소멸)하는 단계에서 호출됩니다.
- DisposableBean 인터페이스의 destroy() 메서드,
- @PreDestroy 애노테이션 메서드,
- destroyMethod 설정 등을 사용할 수 있습니다.
- 컨테이너 종료
- 최종적으로 스프링 컨테이너가 종료되며 모든 빈을 정리합니다.
이번 글에서는 특히 초기화(Initialization) 콜백과 소멸(Destruction) 콜백을 중심으로, 각 단계에서 어떤 일이 일어나는지 구체적으로 살펴보겠습니다.
생명주기 콜백을 위한 대표적인 방법
스프링은 빈의 생명주기에 다양한 지점에서 콜백 로직을 주입할 수 있도록 여러 기법을 제공합니다. 대표적으로 아래 네 가지를 주로 사용합니다.
- 인터페이스 상속
- InitializingBean, DisposableBean을 구현.
- 애노테이션 사용
- @PostConstruct, @PreDestroy.
- XML, 자바 설정에서 initMethod / destroyMethod 지정
- @Bean(initMethod = “init”, destroyMethod = “cleanup”).
- 빈 후처리기(BeanPostProcessor)
- BeanPostProcessor 인터페이스 구현.
이 네 가지 방법은 사용 목적과 편의성 면에서 차이가 있습니다. 가장 권장되는 방식은 자바 표준 애노테이션인 @PostConstruct, @PreDestroy를 활용하는 것입니다. 이유는 다음과 같습니다:
- 표준화: JSR-250(자바 표준)에서 제공하는 애노테이션이므로 스프링 이외의 환경에서도 사용 가능.
- 코드 간결성: 메서드 위에 애노테이션만 붙이면 되므로 가독성이 좋고, 스프링 설정에 종속되지 않음.
- 유연성: 다른 설정을 해줄 필요 없이 간편하게 초기화와 소멸 로직을 작성 가능.
나머지 방법들도 상황에 따라 사용할 수 있으니 각각의 특성을 알아봅시다.
초기화(Initialization) 콜백
@PostConstruct 애노테이션
스프링에서 가장 권장하는 초기화 방식으로, 다음과 같이 사용할 수 있습니다.
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()을 구현하는 방법도 있습니다.
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 애노테이션을 사용해 메서드를 스프링 빈으로 등록하면서, 직접 초기화 메서드를 지정할 수도 있습니다.
@Configuration
public class AppConfig {
@Bean(initMethod = "init")
public ExampleService exampleService() {
return new ExampleService();
}
}
JavaExampleService 클래스 안에서 init() 메서드를 만든 뒤 다음과 같이 작성합니다.
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 애노테이션입니다.
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() 메서드를 오버라이드하여 소멸 로직을 작성합니다.
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를 지정할 수도 있습니다.
@Configuration
public class AppConfig {
@Bean(initMethod = "init", destroyMethod = "cleanup")
public ExampleService exampleService() {
return new ExampleService();
}
}
JavaExampleService 안에 cleanup() 메서드를 두어 자원 정리를 합니다.
public class ExampleService {
public void cleanup() {
System.out.println("cleanup() 호출 - destroyMethod에서 지정");
}
}
JavaBeanPostProcessor를 통한 후처리
초기화(Initialization)와 소멸(Destruction) 콜백 외에도, 스프링은 빈의 생명주기에 개입할 수 있는 후처리기(BeanPostProcessor) 개념을 제공합니다.
BeanPostProcessor를 구현하면, 스프링 빈이 생성된 직후(postProcessBeforeInitialization)와 초기화 직후(postProcessAfterInitialization) 등 여러 시점에 후처리 로직을 삽입할 수 있습니다.
대표적인 사용 예시
- AOP(관점 지향 프로그래밍): 프록시를 생성하거나 메서드 호출을 가로채는 로직.
- 로깅: 빈 생성 로깅, 특정 메서드 호출 모니터링.
- 커스텀 애노테이션 처리: 특정 애노테이션이 붙은 빈을 발견해 추가 설정을 하는 경우 등.
예시 코드
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는 빈 인스턴스(실체 객체)가 생성된 후를 다루는 것이 차이점입니다.
정리 및 모범 사용 예
- 초기화와 소멸: @PostConstruct, @PreDestroy를 권장
- 가장 간편하고 표준 애노테이션이므로 다른 환경에서도 사용하기 쉽습니다.
- 스프링에만 종속되지 않으며, 코드가 깔끔합니다.
- 초기화, 소멸 콜백 메서드명은 알아보기 쉽게
- init(), cleanup(), close(), destroy() 등 명확한 이름을 사용하세요.
- 타 개발자와 협업 시 의도를 더 분명히 표현할 수 있습니다.
- 빈 후처리기(BeanPostProcessor) 사용 시 주의
- AOP, 로깅, 프록시 생성에 유용하지만 복잡도가 증가할 수 있습니다.
- 전체 애플리케이션의 성능이나 디버깅 난이도에 영향을 줄 수 있으므로 주의 깊게 사용하세요.
- BeanFactoryPostProcessor와 구분
- BeanPostProcessor는 빈 인스턴스가 생성된 후 단계에서 작동,
- BeanFactoryPostProcessor는 빈 정의 메타데이터를 다루는 단계에서 작동합니다.
- 실제 프로젝트 적용 시
- 라이프사이클 콜백에 복잡한 로직을 너무 많이 넣는 것은 좋지 않습니다(초기화가 오래 걸리면 어플리케이션 구동 속도에 악영향).
- 가능하면 “리소스 할당이나 외부 서비스 연결은 필요한 시점에” 지연 로딩으로 처리하는 것도 고려해 보세요.
예제 코드 전체
여기서는 @Configuration 클래스와 @PostConstruct, @PreDestroy를 함께 사용하는 예시 코드를 보여드리겠습니다.
// 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// 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// 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실행 결과
===> 스프링 컨테이너 생성
ExampleService 생성자 호출
ExampleService init() 호출
===> 스프링 컨테이너에서 ExampleService 가져오기
ExampleService.doSomething() 실행
===> 스프링 컨테이너 종료
ExampleService cleanup() 호출
Java- 생성자와 @PostConstruct가 순차적으로 호출된 뒤, 프로그램 종료 시점에 @PreDestroy가 호출됩니다.
마무리
스프링 빈 생명주기와 콜백을 잘 이해하고 사용하면, 애플리케이션 전반에 걸쳐 더 견고하고 일관성 있는 초기화와 자원 관리를 할 수 있습니다.
가장 핵심적인 포인트는 다음과 같습니다.
- 빈 생명주기는 “생성 → 의존성 주입 → 초기화 → 사용 → 소멸” 단계를 거친다.
- 초기화와 소멸에는 @PostConstruct, @PreDestroy 애노테이션을 가장 권장한다.
- 필요에 따라 BeanPostProcessor, BeanFactoryPostProcessor 등 후처리 기능을 통해 각종 커스텀 로직을 삽입할 수 있다.
- 리소스 할당 및 해제 로직을 적절히 처리해 어플리케이션 성능과 안정성을 유지해야 한다.
위 내용을 숙지하면, 스프링 빈 관리에 대해 더 깊이 있게 이해하고 체계적으로 애플리케이션을 구성할 수 있을 것입니다. 스프링은 애플리케이션 전반을 유연하게 구성해주기 때문에, Bean Lifecycle에 대한 이해는 필수적입니다. 꼭 알아두시고 다양한 프로젝트에서 활용해 보세요!