목차
Java Spring Bean의 생명주기(Lifecycle)
Spring 프레임워크는 강력한 IoC(Inversion of Control) 컨테이너를 제공하며, 이 컨테이너에서 관리되는 핵심 컴포넌트가 바로 Bean입니다. Bean은 Spring 컨테이너에 의해 생성, 조립, 사용, 소멸되는 객체를 의미하며, Spring Bean의 생명주기를 이해하는 것은 Spring 기반 애플리케이션 개발의 필수적인 부분입니다. 본 블로그에서는 Spring Bean의 생명주기를 심층적으로 파헤쳐, 각 단계별 상세 설명, 주요 개념, 예제, 주의사항 등을 통해 독자 여러분의 이해를 돕고자 합니다.
1. 용어 상세 설명
- Bean: Spring IoC 컨테이너에 의해 관리되는 객체. Spring 설정 파일(XML, Annotation, Java Config)에 정의되어 컨테이너에 의해 생성, 주입, 관리됩니다.
- IoC (Inversion of Control): 제어의 역전. 객체의 생성 및 의존성 관리를 개발자가 직접 수행하는 것이 아니라, 컨테이너에 위임하는 디자인 패턴입니다.
- DI (Dependency Injection): 의존성 주입. 객체 간의 의존 관계를 컨테이너가 설정 파일을 기반으로 자동으로 연결해주는 방식입니다.
- BeanFactory: Spring IoC 컨테이너의 최상위 인터페이스. Bean을 생성하고 관리하는 기본적인 기능을 제공합니다.
- ApplicationContext: BeanFactory 인터페이스를 확장한 인터페이스. BeanFactory의 모든 기능을 포함하며, AOP, 메시지 처리, 이벤트 발행 등 엔터프라이즈 애플리케이션 개발에 필요한 추가 기능을 제공합니다.
- Bean Definition: Bean을 생성하기 위한 설정 정보. Bean의 클래스, 스코프, 초기화/소멸 메서드 등을 정의합니다.
- BeanPostProcessor: Bean 생성 전/후에 특정 작업을 수행할 수 있도록 하는 인터페이스. Bean의 초기화 과정에 개입하여 Bean을 변경하거나 추가적인 기능을 제공할 수 있습니다.
- FactoryBean: Bean 자체가 아닌, Bean을 생성하는 객체를 정의하는 인터페이스. 복잡한 Bean 생성 로직을 캡슐화할 때 유용합니다.
2. 주요 개념 및 특징
- Spring 컨테이너: Bean의 생명주기를 관리하는 핵심 주체. Bean을 생성, 의존성 주입, 초기화, 사용, 소멸하는 모든 과정을 담당합니다.
- Bean 스코프: Bean의 인스턴스 범위를 정의합니다. Spring은 다음과 같은 스코프를 제공합니다.
- singleton: 컨테이너당 하나의 인스턴스만 생성 (기본값)
- prototype: 매 요청마다 새로운 인스턴스 생성
- request: HTTP 요청당 하나의 인스턴스 생성 (Web 환경)
- session: HTTP 세션당 하나의 인스턴스 생성 (Web 환경)
- application: 웹 애플리케이션 컨텍스트 당 하나의 인스턴스 생성 (Web 환경)
- websocket: WebSocket 세션당 하나의 인스턴스 생성 (Web 환경)
- 콜백 메서드: Bean의 특정 시점에 실행되는 메서드. 초기화 콜백(afterPropertiesSet, @PostConstruct), 소멸 콜백(@PreDestroy, destroy)이 있습니다.
- BeanPostProcessor: Bean 생성 전후에 공통적인 작업을 수행하는 데 사용됩니다. 예를 들어, AOP 프록시를 생성하거나, Bean의 유효성을 검사하는 데 사용될 수 있습니다.
3. 생명주기 흐름 및 설명
Spring Bean의 일반적인 생명주기는 다음과 같습니다.
- Bean Definition 로딩: Spring 컨테이너는 설정 파일(XML, Annotation, Java Config)을 읽어 Bean Definition을 로딩합니다. Bean Definition은 Bean을 생성하기 위한 메타데이터를 담고 있습니다.
- Bean 인스턴스화: 컨테이너는 Bean Definition을 기반으로 Bean 인스턴스를 생성합니다. new 연산자를 사용하거나, Factory Method, FactoryBean 등을 이용하여 객체를 생성합니다.
- 의존성 주입 (Dependency Injection): 컨테이너는 Bean이 필요로 하는 의존성을 주입합니다. Constructor Injection, Setter Injection, Field Injection 등의 방식을 사용할 수 있습니다.
- BeanPostProcessor (pre-initialization): 컨테이너는 BeanPostProcessor의 postProcessBeforeInitialization 메서드를 호출합니다. 이를 통해 Bean 초기화 전에 필요한 작업을 수행할 수 있습니다. 예를 들어, AOP 프록시를 생성하거나, Bean의 값을 변경할 수 있습니다.
- InitializingBean 인터페이스 구현 또는 @PostConstruct 어노테이션 사용:
- InitializingBean: Bean 클래스가 InitializingBean 인터페이스를 구현한 경우, afterPropertiesSet() 메서드가 호출됩니다.
- @PostConstruct: Bean 클래스에 @PostConstruct 어노테이션이 붙은 메서드가 있는 경우, 해당 메서드가 호출됩니다.
이 단계에서 Bean은 필요한 초기화 작업을 수행합니다. 예를 들어, 데이터베이스 연결을 설정하거나, 캐시를 초기화할 수 있습니다.
- BeanPostProcessor (post-initialization): 컨테이너는 BeanPostProcessor의 postProcessAfterInitialization 메서드를 호출합니다. 이를 통해 Bean 초기화 후에 필요한 작업을 수행할 수 있습니다. 예를 들어, AOP 프록시를 적용하거나, Bean을 컨테이너에 등록할 수 있습니다.
- Bean 사용: Bean이 컨테이너에 의해 관리되면서 애플리케이션에서 사용됩니다.
- DisposableBean 인터페이스 구현 또는 @PreDestroy 어노테이션 사용:
- DisposableBean: Bean 클래스가 DisposableBean 인터페이스를 구현한 경우, destroy() 메서드가 호출됩니다.
- @PreDestroy: Bean 클래스에 @PreDestroy 어노테이션이 붙은 메서드가 있는 경우, 해당 메서드가 호출됩니다.
컨테이너가 종료되거나 Bean이 더 이상 필요하지 않게 되면, 소멸 단계가 시작됩니다. 이 단계에서 Bean은 사용했던 자원을 해제하거나, 필요한 정리 작업을 수행합니다.
- Bean 소멸: 컨테이너는 Bean 인스턴스를 메모리에서 제거합니다.
요약:
Bean Definition 로딩 -> Bean 인스턴스화 -> 의존성 주입
-> BeanPostProcessor (pre-initialization) -> InitializingBean/ @PostConstruct
-> BeanPostProcessor (post-initialization) -> Bean 사용 -> DisposableBean/ @PreDestroy
-> Bean 소멸
4. 예제 및 예제 상세 설명
4.1. XML 설정 방식
public class XmlBean {
private String message;
public XmlBean() {
System.out.println("XmlBean Constructor");
}
public void setMessage(String message) {
System.out.println("Setting message: " + message);
this.message = message;
}
public void init() {
System.out.println("XmlBean init method");
}
public void destroy() {
System.out.println("XmlBean destroy method");
}
public void printMessage() {
System.out.println("Message: " + message);
}
}
XML 설정:
<bean id="xmlBean" class="com.example.XmlBean" init-method="init" destroy-method="destroy">
<property name="message" value="Hello from XML!"/>
</bean>
- class: Bean으로 등록할 클래스를 지정합니다.
- init-method: Bean 초기화 시 실행할 메서드를 지정합니다. init() 메서드가 호출됩니다.
- destroy-method: Bean 소멸 시 실행할 메서드를 지정합니다. destroy() 메서드가 호출됩니다.
- <property>: Bean의 속성 값을 설정합니다. setMessage() 메서드가 호출되어 message 속성에 "Hello from XML!"이 할당됩니다.
실행 코드:
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
XmlBean xmlBean = context.getBean("xmlBean", XmlBean.class);
xmlBean.printMessage();
context.close();
출력 결과:
XmlBean Constructor
Setting message: Hello from XML!
XmlBean init method
Message: Hello from XML!
XmlBean destroy method
- Spring 컨테이너는 applicationContext.xml 파일을 읽어 XmlBean에 대한 Bean 정의를 로드합니다.
- XmlBean의 생성자가 호출되어 인스턴스가 생성됩니다.
- <property> 태그에 의해 setMessage() 메서드가 호출되어 message 속성 값이 설정됩니다.
- init-method에 지정된 init() 메서드가 호출됩니다.
- xmlBean.printMessage()를 호출하여 message 속성 값을 출력합니다.
- context.close()를 호출하면 컨테이너가 종료되면서 destroy-method에 지정된 destroy() 메서드가 호출됩니다.
4.2. Annotation 기반 설정 방식 (@PostConstruct, @PreDestroy)
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
public class AnnotationBean {
private String message;
public AnnotationBean() {
System.out.println("AnnotationBean Constructor");
}
public void setMessage(String message) {
System.out.println("Setting message: " + message);
this.message = message;
}
@PostConstruct
public void init() {
System.out.println("AnnotationBean @PostConstruct init method");
}
@PreDestroy
public void destroy() {
System.out.println("AnnotationBean @PreDestroy destroy method");
}
public void printMessage() {
System.out.println("Message: " + message);
}
}
XML 설정 (Component Scanning):
<context:component-scan base-package="com.example"/>
Java Config 설정:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public AnnotationBean annotationBean() {
AnnotationBean bean = new AnnotationBean();
bean.setMessage("Hello from Annotation!");
return bean;
}
}
- @PostConstruct: Bean이 의존성 주입을 완료한 후 초기화 작업을 수행할 메서드에 붙입니다.
- @PreDestroy: Bean이 소멸되기 전에 정리 작업을 수행할 메서드에 붙입니다.
- <context:component-scan> (XML): 지정된 패키지에서 @Component 어노테이션이 붙은 클래스를 찾아 Bean으로 등록합니다. @PostConstruct와 @PreDestroy 어노테이션을 사용하려면 <context:annotation-config/> 또는 <context:component-scan/>을 설정 파일에 추가해야 합니다.
- @Bean (Java Config): @Configuration 클래스 내에서 메서드에 @Bean 어노테이션을 붙여 Bean을 정의합니다.
실행 코드:
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
AnnotationBean annotationBean = context.getBean("annotationBean", AnnotationBean.class);
annotationBean.printMessage();
context.close();
출력 결과:
AnnotationBean Constructor
Setting message: Hello from Annotation!
AnnotationBean @PostConstruct init method
Message: Hello from Annotation!
AnnotationBean @PreDestroy destroy method
- Spring 컨테이너는 applicationContext.xml 파일을 읽고 <context:component-scan> 설정을 통해 com.example 패키지를 스캔하여 @Component 어노테이션이 붙은 클래스를 찾아 Bean으로 등록합니다. 또는 @Bean 어노테이션으로 등록합니다.
- AnnotationBean의 생성자가 호출되어 인스턴스가 생성됩니다.
- setMessage() 메서드가 호출되어 message 속성 값이 설정됩니다.
- @PostConstruct 어노테이션이 붙은 init() 메서드가 호출됩니다.
- annotationBean.printMessage()를 호출하여 message 속성 값을 출력합니다.
- context.close()를 호출하면 컨테이너가 종료되면서 @PreDestroy 어노테이션이 붙은 destroy() 메서드가 호출됩니다.
4.3. InitializingBean, DisposableBean 인터페이스 구현 방식
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
public class InterfaceBean implements InitializingBean, DisposableBean {
private String message;
public InterfaceBean() {
System.out.println("InterfaceBean Constructor");
}
public void setMessage(String message) {
System.out.println("Setting message: " + message);
this.message = message;
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("InterfaceBean InitializingBean.afterPropertiesSet() method");
}
@Override
public void destroy() throws Exception {
System.out.println("InterfaceBean DisposableBean.destroy() method");
}
public void printMessage() {
System.out.println("Message: " + message);
}
}
XML 설정 (Bean 정의):
<bean id="interfaceBean" class="com.example.InterfaceBean">
<property name="message" value="Hello from Interface!"/>
</bean>
Java Config 설정:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public InterfaceBean interfaceBean() {
InterfaceBean bean = new InterfaceBean();
bean.setMessage("Hello from Interface!");
return bean;
}
}
- InitializingBean: Bean 생성 후 속성 설정이 완료되면 afterPropertiesSet() 메서드가 자동으로 호출됩니다.
- DisposableBean: Bean이 소멸되기 전에 destroy() 메서드가 자동으로 호출됩니다.
실행 코드:
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
InterfaceBean interfaceBean = context.getBean("interfaceBean", InterfaceBean.class);
interfaceBean.printMessage();
context.close();
출력 결과:
InterfaceBean Constructor
Setting message: Hello from Interface!
InterfaceBean InitializingBean.afterPropertiesSet() method
Message: Hello from Interface!
InterfaceBean DisposableBean.destroy() method
- Spring 컨테이너는 applicationContext.xml 파일을 읽어 InterfaceBean에 대한 Bean 정의를 로드합니다.
- InterfaceBean의 생성자가 호출되어 인스턴스가 생성됩니다.
- <property> 태그에 의해 setMessage() 메서드가 호출되어 message 속성 값이 설정됩니다.
- InitializingBean 인터페이스를 구현했으므로 afterPropertiesSet() 메서드가 호출됩니다.
- interfaceBean.printMessage()를 호출하여 message 속성 값을 출력합니다.
- context.close()를 호출하면 컨테이너가 종료되면서 DisposableBean 인터페이스를 구현했으므로 destroy() 메서드가 호출됩니다.
4.4. BeanPostProcessor 활용
예제 코드 (MyBeanPostProcessor):
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("BeanPostProcessor - Before Initialization: " + beanName);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("BeanPostProcessor - After Initialization: " + beanName);
return bean;
}
}
적용 대상 Bean (예시):
import org.springframework.stereotype.Component;
@Component
public class TargetBean {
public TargetBean() {
System.out.println("TargetBean Constructor");
}
}
XML 설정 (Component Scanning):
<context:component-scan base-package="com.example"/>
또는 Java Config:
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan("com.example")
public class AppConfig {
}
- BeanPostProcessor 인터페이스를 구현하여 Bean 생성 전후에 공통 작업을 수행합니다.
- postProcessBeforeInitialization(): Bean 초기화 전(InitializingBean#afterPropertiesSet() 또는 @PostConstruct 호출 전)에 호출됩니다.
- postProcessAfterInitialization(): Bean 초기화 후(InitializingBean#afterPropertiesSet() 또는 @PostConstruct 호출 후)에 호출됩니다.
- 모든 Bean에 적용되므로, beanName을 사용하여 특정 Bean에만 적용하도록 조건을 설정할 수 있습니다.
실행 코드:
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
TargetBean targetBean = context.getBean("targetBean", TargetBean.class);
context.close();
출력 결과:
TargetBean Constructor
BeanPostProcessor - Before Initialization: targetBean
BeanPostProcessor - After Initialization: targetBean
- Spring 컨테이너는 applicationContext.xml 파일을 읽고 <context:component-scan> 설정을 통해 com.example 패키지를 스캔하여 @Component 어노테이션이 붙은 클래스를 찾아 Bean으로 등록합니다.
- MyBeanPostProcessor는 컨테이너 내의 모든 Bean에 적용됩니다.
- TargetBean의 생성자가 호출되어 인스턴스가 생성됩니다.
- BeanPostProcessor의 postProcessBeforeInitialization() 메서드가 호출됩니다.
- BeanPostProcessor의 postProcessAfterInitialization() 메서드가 호출됩니다.
- context.close()를 호출하면 컨테이너가 종료됩니다. (TargetBean은 DisposableBean 구현 또는 @PreDestroy 어노테이션이 없으므로 별도의 소멸 단계는 실행되지 않습니다.)
5. 주의사항
- 초기화 및 소멸 콜백 메서드 중복 정의: @PostConstruct, InitializingBean, init-method를 Bean에 모두 정의하면, 모든 메서드가 순서대로 호출됩니다. 필요에 따라 적절한 방법을 선택하여 사용해야 합니다.
- Bean 스코프: Bean 스코프를 잘못 설정하면 예상치 못한 문제가 발생할 수 있습니다. 예를 들어, prototype 스코프의 Bean에 의존성을 주입하면 매번 새로운 인스턴스가 생성되므로, 싱글톤 Bean에서 상태를 유지하는 데 문제가 발생할 수 있습니다.
- 순환 참조: Bean 간에 순환 참조가 발생하면 Spring 컨테이너가 Bean을 생성하는 데 실패할 수 있습니다. Constructor Injection보다는 Setter Injection을 사용하여 순환 참조 문제를 해결할 수 있습니다.
- BeanPostProcessor: BeanPostProcessor는 컨테이너의 모든 Bean에 영향을 미치므로, 신중하게 사용해야 합니다. 불필요한 작업을 수행하면 컨테이너의 성능을 저하시킬 수 있습니다.
- @Autowired 시점: @Autowired를 사용하는 경우, 의존성 주입이 Bean 생성 시점에 이루어지므로, @PostConstruct 또는 afterPropertiesSet 메서드에서 주입된 의존성을 사용하는 것이 안전합니다. 생성자 주입을 사용하는 경우, Bean 생성 시점에 의존성이 주입되므로 바로 사용할 수 있습니다.
Spring Bean의 생명주기를 깊이 있게 이해하는 것은 Spring 기반 애플리케이션을 효과적으로 개발하고 유지보수하는 데 필수적입니다. 본 블로그에서 다룬 내용을 통해 독자 여러분이 Spring Bean의 생명주기를 완벽하게 이해하고, 실제 개발 현장에서 이를 활용할 수 있기를 바랍니다. Spring Bean의 생명주기를 숙지하고 적절하게 활용하면, 더욱 견고하고 효율적인 애플리케이션을 구축할 수 있습니다.
'이직&취업 > Java 기초 상식' 카테고리의 다른 글
JAVA Call by Value vs Call by Reference 무엇인가? (15) | 2025.03.25 |
---|---|
불변성(Immutable)과 가변성(Mutable)란 무엇인가? (23) | 2025.03.24 |
Java Collection Framework란? (8) | 2025.03.23 |
Checked Exception 와 Unchecked Exception (16) | 2025.03.23 |
Transaction 란? (14) | 2025.03.22 |