※본 글은 김영한님의 '자바 스프링 완전정복 시리즈' 강의를 바탕으로 작성한 글입니다.
이번에는 스프링 빈이 싱글톤 빈이 관리되는 라이프사이클에 대해 정리해보겠습니다.
기본적으로 싱글톤 빈은 빈의 생성부터 소멸까지 스프링에서 계속해서 관리를 하게 됩니다.
관리되는 라이프 사이클은 다음과 같습니다.
① 스프링 컨테이너 생성
가장 먼저 스프링 컨테이너 자체가 생성됩니다.
② 스프링 빈 생성
스프링 컨테이너에 빈을 등록하는 단계입니다.
③ 의존관계 주입
생성된 빈들의 의존 관계를 조립하는 단계입니다.
다만, 생성자 주입의 경우 생성 단계에서 주입이 이뤄지기 때문에 ②단계에서 수행이 됩니다.
④ 초기화 콜백
초기화 메서드를 호출합니다.
⑤ 빈 사용
빈을 사용하는 단계입니다.
⑥ 소멸전 콜백
빈이 소멸하기 이전 소멸 메서드를 호출합니다.
⑦ 스프링 종료
스프링 종료.
싱글톤 빈은 소멸까지 컨테이너가 관리한다는 것과, 빈이 생성되고 의존관계 주입이 이뤄진다는 내용은 이전에도 많이 다루고, 쉽게 알 수 있는 내용입니다.
그렇다면 여기서 초기화 콜백과, 소멸전 콜백은 어떠한 단계일까요?
먼저 생성자에서 처리하는 내용에 대해서 다시 생각해보겠습니다.
생성자에서는 파라미터로 필수 정보를 받고, 초기화하는 등의 역할을 합니다.
외부 커넥션이 필요한 클래스를 가정해봅시다.
외부 커넥션 관련 정보를 생성자로 전달받는다면, 실제 연결하는 작업도 생성자에서 처리해주어는 것이 좋을까요?
유지보수 측면에서,
생성자는 객체 자체의 생성에만 집중하고, 앞의 예시에서 실제 연결과 같은 그 외의 초기화 작업은 별도의 메서드로 생성하는 것이 좋다고 합니다.
그러한 메서드가 초기화 메서드입니다.
소멸 메서드도 쉽게 이해할 수 있습니다. 객체가 소멸되기 이전 연결을 끊는 작업 등을 수행해야 하는데, 이러한 경우처럼 소멸되기 이전에 호출해야할 메서드가 소멸 메서드입니다.
초기화 메서드 콜백에 대해서 다음가 같이 정리할 수 있습니다.
1. 생성자와 초기화 메서드를 분리해주는 것이 유지보수 측면에서 좋다.
2. 초기화 메서드도 빈의 생성단계에서 호출해주어야 하는데, 그 단계가 초기화 콜백 단계이다.
package hello.core;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.junit.jupiter.api.Assertions.*;
class callbackTest {
@Test
void lifeCycleTest(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycle.class);
ac.close();
}
static class LifeCycle{
String url;
public LifeCycle(){
this.url = "https://~~";
System.out.println("생성자 호출");
}
void connect(){
System.out.println("connect " + url);
}
void disconnect(){
System.out.println("disconnect");
}
}
}
앞서 예시로든 커넥션이 필요한 클래스를 가정해봤습니다.
생성자에서 url을 설정하고, connect 메서드를 통해 연결을 수행합니다.
그렇다면 스프링 컨테이너는 connect 메서드가 초기화 메서드(혹은 소멸 메서드)인 것을 어떻게 알까요?
현재 상태로는 테스트 코드를 돌리면 connect와 disconnect가 호출되지 않습니다.
3가지 방법이 있습니다.
InitializingBean, DisposableBean 인터페이스를 상속하는 방법입니다.
(org.springframework.beans.factory.InitializingBean)
package hello.core;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.junit.jupiter.api.Assertions.*;
class callbackTest {
@Test
void lifeCycleTest(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycle.class);
ac.close();
}
static class LifeCycle implements InitializingBean, DisposableBean {
String url;
public LifeCycle(){
this.url = "https://~~";
System.out.println("생성자 호출");
}
void connect(){
System.out.println("connect " + url);
}
void disconnect(){
System.out.println("disconnect");
}
@Override
public void afterPropertiesSet() throws Exception {
connect();
}
@Override
public void destroy() throws Exception {
disconnect();
}
}
}
인터페이스를 상속한 후 afterPropertiesSet, destory 메서드를 오바라이드해주면 해당 메서드를 스프링이 콜백해주게 됩니다.
다만 아래와 같은 단점 때문에 이 방식은 사실상 사용하지 않습니다.
package hello.core;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.junit.jupiter.api.Assertions.*;
class callbackTest {
@Test
void lifeCycleTest(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
ac.close();
}
@Configuration
static class Config{
@Bean(initMethod = "connect", destroyMethod = "disconnect")
public LifeCycle lifeCycle(){
return new LifeCycle();
}
}
static class LifeCycle{
String url;
public LifeCycle(){
this.url = "https://~~";
System.out.println("생성자 호출");
}
void connect(){
System.out.println("connect " + url);
}
void disconnect(){
System.out.println("disconnect");
}
}
}
코드를 보면 쉽게 이해할 수 있을 것입니다.
@Bean으로 등록할 시에 initMethod와 destroyMethod의 이름을 알려주기만 하면 됩니다.
-> 외부 라이브러리에도 적용할 수 있다는 장점이 큽니다.
초기화 메서드 위에는 @PostConstruct, 소멸 메서드 위에는 @PreDestory 를 추가해주기만 하면 되는 방법입니다.
단순하기 때문에 코드는 생략하겠습니다.
단순하고 최근에 가장 권장하는 방법이지만, 마찬가지로 외부 라이브러리에는 적용할 수 없다는 단점이 있습니다.
결론적으로 다음과 같은 가이드 라인을 따르면 됩니다.