목표
- Spring Container에 대한 이해
- 객체는 어떻게 관리되는가
Bean을 담는 그릇, Container
Spring application에서는 Spring container에서 객체가 태어나고, 자라고, 소멸한다. Spring Container는 객체 생성, wiring, 그리고 이들의 전체 생명주기를 관리한다. (어떤 객체를 생성, 구성, 서로 엮어줘야 하는지 알 수 있게 설정하는 방법은 2장) 즉, Spring Container는 객체들의 삶의 터전이다.
Spring container는 Spring framework의 핵심부에 위치한다. Spring Container의 역할은 아래와 같다.
- DI(종속 객체 주입)을 이용해 application을 구성하는 컴포넌트 관리
- 협력 컴포넌트 간 연관관계의 형성 (wiring)
이러한 짐들을 container에 덜어버린(=역할을 위임했다고 이해했다.) 객체들은 더 명확하고 이해하기 쉬우며, 재사용을 촉진하고, 단위 테스트가 용이해진다. 그렇다면 Container에는 어떤 것들이 있을까?
1. Spring Containers
Spring Container는 여러 가지가 있다. 여러 컨테이너 구현체가 존재하며, 크게 두 가지로 분류된다.
-
빈팩토리 Bean Factory (org.springframework.beans.factory.BeanFactory) interface에 의해 정의된다. DI에 대한 기본적인 지원을 제공하는 가장 단순한 Container다.
-
어플리케이션 컨텍스트 ApplicationContext (org.springframework.context.ApplicationContext) interface에 의해 정의된다. 빈팩토리를 확장해서 property file에 텍스트 메시지를 읽고, 해당 이벤트 리스너에 대한 애플리케이션 이벤트 발행 같은 애플리케이션 프레임워크 서비스를 제공하는 컨테이너다.
Spring으로 작업할 때 빈팩토리나 어플리케이션 컨텍스트 중 아무거나 사용해도 상관없지만 빈팩토리는 지나치게 저수준의 기능을 제공하므로 일반적으로 어플리케이션 컨텍스트를 더 선호한다.
1-1. Application Context
Spring에는 다양한 종류의 application context가 존재한다. 가장 많이 접하게 될 세 가지는 아래와 같다. (2021년 기준으로 다섯 가지라 다섯 가지를 소개한다.)
-
AnnotationConfigApplicationContext Spring 3.0에서 도입.
@Configuration
,@Component
로 정의된 내용을 가져올 수 있다. -
AnnotationConfigWebApplicationContext
AnnotationConfigApplicationContext
의 웹 기반. 이 클래스는 Spring의ContextLoaderListener
servlet listener 혹은web.xml
파일의 Spring MVCDispatcherServlet
을 구성할 때 사용할 수 있다. Spring 3.0 부터는WebApplicationInitializer
interface를 구현해 programming 방식으로도 구현할 수 있다. -
XmlWebApplicationContext 웹 어플리케이션에서 xml 기반 configuration을 사용할 경우 사용 가능하다. 웹 어플리케이션에서 포함된 XML 파일에서 context 정의 내용을 load한다.
-
FileSystemXmlApplicationContext 파일 시스템에서, 즉 파일 경로로 지정된 xml파일에서 context 정의 내용을 load한다.
ApplicationContext context = new FileSystemXmlApplicationContext("c:/foo.xml");
- ClassPathXmlApplicationContext ClassPath에 위치한 xml 파일에서 context 정의 내용을 load한다. 이 경우 클래스패스에 포함된 모든 경로(JAR 파일 포함)에서 찾으려 한다.
ApplicationContext context = new ClassPathXmlApplicationContext("foo.xml");
application context를 얻은 다음 context의 getBean()
메소드를 호출해 Spring container에서 bean을 조회할 수 있다.
1-2. Bean Life Cycle
일반적인 Java application에서 bean의 생명주기는 매우 단순하다. new keyword를 이용하거나 역직렬화(deserialization)를 통해 bean을 인스턴스화하고, 이를 바로 사용한다. 빈이 더 이상 사용되지 않으면 가비지 컬렉션 후보가 되어 언젠가 메모리 덩어리가 됐다가 사라지게 될 것이다.
반면 Spring Container 내에서 빈의 생명주기는 좀 더 정교하다. 앞서 얘기한 것처럼 Bean life cycle은 Spring container에 의해 관리되며, container는 프로그램 실행 시 시작된다. container는 요청에 따라 bean의 인스턴스를 생성하고, 종속 객체를 주입한다. 이후 container가 닫히면 bean은 파괴된다.
도식화된 모습을 살펴보자.
bean instance는 Java 또는 XML bean 정의를 기반으로 인스턴스화 된다. 이를 사용 가능한 상태로 만들기 위해 일부 전후 초기화 단계를 수행해야 할 수도 있고, 더 이상 bean이 필요하지 않게 되면 IoC Container에서 제거되는데 시스템 리소스를 해제하기 위해 전후 destory 단계를 수행해야 할 수도 있다.
bean이 생성될 때 Spring이 제공하는 커스터마이징 기회(life cycle callback method)를 이용하려면 Life Cycle을 이해해두는 것이 좋다.
상세히 보기
2. Spring이 값, bean reference를 bean의 property에 주입한다.
3. 빈이 `BeanNameAware`를 구현하면, Spring이 Bean의 ID를 `setBeanName()` 메소드에 넘긴다.
4. 빈이 `BeanFactoryAware`를 구현하면, `setBeanFactory()` 메소드를 호출하여 빈팩토리 자체를 넘긴다.
5. 빈이 `ApplicationContextAware`를 구현하면, Spring이 `setApplicationContext()` 메소드를 호출하고, 둘러싼(enclosing) 어플리케이션 컨텍스트에 대한 참조를 넘긴다.
6. 빈이 BeanPostProcessor 인터페이스를 구현하면, Spring은 postProcessBeforeInitialization() 메소드를 호출한다.
7. 빈이 InitializingBean 인터페이스를 구현하면, 스프링은 afterPropertiesSet() 메소드를 호출한다. custom init method가 존재한다면 해당 메소드가 호출된다.
8. 빈이 BeanPostProcessor를 구현하면, 스프링은 postProcessAfterInitialization() 메소드를 호출한다.
9. 이 상태가 되면 빈은 어플리케이션에서 사용할 준비가 된 것이며, 어플리케이션 컨텍스트가 소멸될 때까지 어플리케이션 컨텍스트에 남아 있다.
10. 빈이 `DisposableBean` 인터페이스를 구현하면, Spring은 `destroy()` 메소드를 호출한다. custom destroy method가 존재한다면 해당 메소드가 호출된다.
Spring이 제공하는 Bean의 Life cycle callback 구현 방법은 아래와 같다.
- Annotation
- 자바 표준 annotation (JSR-250)
- 최신 Spring application에서 권장: bean이 Spring의 특정 interface에 연결되지 않기 때문, JSR-250 annotation을 사용하지 않고 coupling을 제거하려면 xml의 init-method, destory-method 정의 방법 고려
- 프로그래밍
InitializingBean
,DisposableBean
interface 사용 -> Spring과 불필요하게 연결되기 때문에 사용하지 않는 것을 권장함
- XML
init-method
,destory-method
attribute 사용
3가지 방법 중 Annotation(JSR-250)을 이용해 간단하게 구현해보았다.
- HelloWorld.java
...
@Component
public class HelloWorld {
@PostConstruct
public void init() {
System.out.println(
"HelloWorld Bean has been instantiated "
+ "and I am the init() method!");
}
@PreDestroy
public void destroy() {
System.out.println(
"HelloWorld Bean has been closed "
+ "and I am the destroy() method!");
}
}
- HelloWorldTests.java
@SpringBootTest
public class HelloWorldTests {
@Autowired
ApplicationContext context;
@Test
public void test_that_successfully_created_bean() {
HelloWorld helloWorld = context.getBean(HelloWorld.class);
assert (Objects.nonNull(helloWorld));
}
}
Component annotation으로 선언하였으니 당연히 해당 bean은 정상적으로 등록됨을 확인할 수 있다. 여기서 사용된 application context는 AnnotationConfigApplicationContext
로, 해당 container를 사용하도록 Spring boot에 설정되어있다. (관련해서 소스코드를 찾아본다면 좋을 것 같다.) 그리고 bean 생명주기에 따라 init, destroy 메소드가 정상적으로 실행됨을 확인하였다. 이외에도 DI 방법에 따라 (생성자, setter, field) 종속객체 주입 시점이 달라진다.
Spring container가 어떻게 생성되고 로드되는지는 확인하였으나 아직 container에 아무것도 넣지 않았기 때문에 쓸모가 없다. Spring DI를 활용하려면, container에 어플리케이션 객체를 넣고 wiring 해주어야 한다.
글을 끝내면서…
이해를 위해 도식화된 그림을 2가지 정도로 그려보았었는데, 하나는 요약 하나는 상세 버전이다. 요약 버전은 아래와 같다.
여기서 궁금한 점이 있었는데, Properties 할당부터 afterPropertiesSet() 전까지를 DI를 볼 수 있을까였다. 아마 아직 종속객체 주입 과정을 잘 이해하지 못한 부분이 있는 것 같고, 해당 process가 잘 흡수되진 않았기 때문에 그런 듯하다. wiring까지 세부적으로 더 공부한 뒤에 다시 한 번 살펴본다면 좋을 것이라 생각한다.
References
- Craig Walls, Spring in action thrid edition
- https://www.baeldung.com/spring-application-context
- https://howtodoinjava.com/spring-core/spring-bean-life-cycle/
- https://www.geeksforgeeks.org/bean-life-cycle-in-java-spring/
- https://devlog-wjdrbs96.tistory.com/321
- https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-nature
- https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.spring-application.web-environment