빈 생명주기, 빈 스코프 1편
오늘 포스트는 빈의 생존능력에 대해 알아 보올 거시다! 게다가 빈도 이 생존능력에 따라 두 가지 버전이 있다느은 거시다! 생존능력은 사실 빈이 얼마나 잘 사는 지가 아니라아 스프링 컨테이너가 얼마나 오래 가지고 있어주냐는 거시다! 거두절미하고 빈 생명주기를 시작으로 오늘 포스트를 시작할 거시올시다!
1. 빈 생명주기
애플리케이션이 시작하고 종료할 때 스프링 빈들 또한 생성되고 초기화하고 사라져야야 한다. 이 과정들에 대해 다루어 보자.
스프링 빈은 저번에도 주구장창 이야기 하였는데, 빈의 생성 단계는 객체를 생성하고 의존관계를 주입하는 두 단계의 라이프 사이클을 가진다. 당연히 이 두 단계를 지나야지만이 초기화 작업들을 할 수 있다. 그렇다면 우린 초기화 작업을 해야하는데 할 수 있는 지 없는 지 아는 방법 없나요~?
이때 바로 콜백 메서드라는 것이 있다. 이 메서드를 통해 초기화 시점을 알려준다. 뿐만 아니라 스프링 컨테이너가 종료 되기 직전에 소멸 전 콜백 또한 존재한다. 게다가 이 콜백 함수들은 여러 개가 존재하는데 2가지만 다룰 것이여. 아무튼 그 전에 이 스프링 빈의 전체적 라이프 사이클을 정리하자면,
스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 스프링 빈 지지고 볶고 놀고 겁나게 갖고 놀기
-> 소멸 전 콜백 -> 스프링 빈 소멸 -> 스프링 컨테이너 셧더 내리기
이제 두 개의 초기화 콜백 & 소멸 전 콜백 함수들을 배울 것이다. 사실 하나가 더 있는데 인터페이스인 InitializingBean 과 DisposableBean이라는 두 인터페이스를 상속을 받아서 그 안에 있는 함수들을 override해서 사용 할 수 있지만(즉, 생명 주기를 알고 싶은 객체의 코드를 수정), 스프링 전용 인터페이스이고, 함수 이름이 선정 되어 버리기도 하고, 가장 큰 것은 외부 라이브러리들에는 적용 할 수 없다. 당연하지, 외부 라이브러리 코드를 우리가 수정할 수 는 없잖소?! 그래서 안쓴다고 한다. 아무튼 그럼 나머지 두 방식을 정리하겠다!
1.1 빈 등록 초기화, 소멸 메서드 지정
@Bean 어노테이션의 옵션을 이용한다. 아래 코드와 같이 NetworkClient()라는 객체가 있고 이 객체를 사용하는 LifeCycleConfig가 있다. NetworkClient코드부터 보겠다.
public class NetworkClient{
...
public void init(){
System.out.println("NetworkClient.init");
connect();
call("초기화 연결 메시지");
}//의존관계 주입 후
public void close(){
System.out.println("NetworkClient.close");
disconnect();
}
...
}
오해하면 안된다. connect와 disconnect이런건 특별한 코드가 아니라 저 클래스 내에 정의되어있는 함수일 뿐이고 그냥 생략 했다. 이제 봐야할 것은 두 메서드 이름이다. init과 close. 이걸 눈에 담아두게 되면~
@Configuration
static class LifeCycleConfig{
@Bean(initMethod = "init", destroyMethod = "close")
public NetworkClient networkClient(){
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hello-spring.dev");
return networkClient;
}
}
이제 @Bean 어노테이션에 그 반가운 두 이름을 볼 수 있다 init과 close. 저 이름으로 찾는 것이당. 직관적이게도 initMethod가 초기화 후 콜백, destroyMethod는 소멸 전 콜백이다.
이렇게 되면 메서드 이름을 다르게 정할 수도 있고 코드가 아닌 설정정보(@Bean)을 이용하기에 외부 라이브러리의 초기화, 종료 메스드의 이름만 알면 초기화 콜백, 소멸 전 콜백으로 등록 할 수 있다는 장점이 있다.
destroyMethod의 경우 신기하게도 굳이 이름을 정해주지 않아도 (inferred)라고 디폴트 값이 설정되어 있는데 이는 대게 외부 라이브러리의 종료 매서드는 close나 shutdown같은 이름을 사용하기에 굳이 따로 적어주지 않아도 잘 찾아서 종료 메서드를 연결한다.
1.2 @PostConstruct, @PreDestroy
이번에는 @Bean어노테이션의 설정 정보를 이용했다면 이제 새로운 두 어노테이션으로 어떤 함수를 초기화 콜백, 소멸 전 콜백으로 등록 할 수가 있다.
package hihi.core.lifeCycle;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
public class NetworkClient{
@PostConstruct
public void init(){
System.out.println("NetworkClient.init");
connect();
call("초기화 연결 메시지");
}//의존관계 주입 후
@PreDestroy
public void close(){
System.out.println("NetworkClient.close");
disconnect();
}
}
간단하다. 진짜 저렇게 어노테이션만 등록하면 된다. 그럼 아까 그 LifeCycleConfig 클래스의 @Bean(뭐시기 뭐시기 부분)을 @Bean으로 바꾸어 주면 된다.
정말 편한 방법이기도 하고 위에 import 패키지를 보게 되면 javax로 시작한다. 즉, 스프링에 종속적인 어노테이션이 아니라 자바 표준에 등록 되어 있기에 스프링이 아닌 다른 컨테이너에서도 이를 이용 할 수 있다는 것이다. 하지만 당연히 두 어노테이션을 붙이기 위해 코드를 수정해야 하기에 외부 라이브러리에는 적용하지 못하니, 외부라이브러리의 경우 1.1의 방법을 쓰면 좋을 것이야
2. 빈 스코프
스코프란? 빈이 존재할 수 있는 범위?, 시간? 뭐 이런 느낌으로 생각하면 된다. {}안에 지역 변수는 {}안에만 살아있는 거랑 뭐 비슷하다.
이 스코프에 따라 스프링 빈은 다음과 같이 나뉠 수 있다.
- 싱글톤 : 기본 스코프이다. 지금까지 계속 써온 방식으로서 스프링 컨테이너의 시작과 종료까지 계속 유지되는 가장 넓은 범위의 스코프
- 프로토타입 : 컨테이너는 요 고얀 녀석을 생성과 의존관계 주입 까지만 하고 더 이상 신경 쓰지 않는다. 클라이언트에 던져주는 것 같은 방식으로 컨테이너에서 드러내 버린다고 생각하면 될 거 같다.
- 웹 관련 스코프 : request / session / application 세 가가 있다. 각각은 이름에 맞게 웹 요청 동안 / 웹 세션 동안 / 웹의 서블릿 컨텍스트 동안 살아 있다.
2.1 프로토타입
싱글톤은 뭐 많이 다뤘잖아? 넘어가겠다.
중요한 것은 프로토타입 스코프인 스프링 빈을 컨테이너에서 조회하면 그 때 마다 스프링 컨테이너는 새로운 인스턴스를 생성해서 반환한다. 참고로 기존 싱글톤 스코프의 스프링 빈은 계속 살아있고 유일하기에 clientA가 부르고 clientB가 부르면 같은 객체를 반환하기에 client가 함부로 그 값에 접근 하지 못하게 해야 한다고 이야기 한 적이 있다. 하지만 프로토타입 스코프의 스프링 빈은 상관 없겠죠?!
등록 법은 간단하다. 자동 등록인 @Component 또는 수동 등록인 @Bean 어노테이션 과 함께 @Scope("prototype") 이라고 어노테이션을 넣어주면 된다. 지금까지는 저 값이 없으면 @Scope("singleton") 이 었던 것이었던 것이다.
아무튼 위에서 말한 이야기를 조금 더 정리하자면 세 명의 클라이언트가 프로토타입 스코프인 스프링 빈을 요청하면 컨테이너는 각 세명에게 그 스프링 빈을 그제서야 생성하고 필요한 의존관계를 주입하고 클라이언트에 반환한다.
아니 잠시만, 그렇다면! 맞아 그렇다. 그런 것이다. 알았다면 넘어가겠다. 즉, 위에서 다룬 소멸 전 콜백이 등장 할 일이 없다는 것이다. 왜냐하면 이제 그 스프링 빈은 클라이언트 손에 쥐어졌고 클라이언트가 필요하면 직접 그 함수들을 불러내면 된다.
2.2 코드로서 비교
먼저 싱글톤의 경우는~~
@Test
void singletonBeanFine(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
System.out.println("singletonBean1 = " + singletonBean1);
System.out.println("singletonBean2 = " + singletonBean2);
assertThat(singletonBean1).isSameAs(singletonBean2);
ac.close();
}
@Scope("singleton")
static class SingletonBean{
@PostConstruct
public void init(){
System.out.println("SingletonBean.init");
}
@PreDestroy
public void destroy(){
System.out.println("SingletonBean.destroy");
}
}
위 테스트 코드를 실행하게 되면 singletonBean1과 singletonBean2의 경우 같은 값을 출력하게 되어있다. 실행 결과는 아래 사진에..
사진을 보기전에 어 뭐야 @Scope어노테이션 근처에 @Component 없는데요? 어떻게 스캔 하나요? 라고 묻는다면 사실 AnnotationConfigApplicationContext의 인자에 SingletonBean.class를 넘겨주게 되면 컴포넌트 스캔 대상이 되어버린다.
싱글톤 답게 init은 한 번만 일어나서 컨테이너에 등록되고 singletonBean1과 2는 같은 값을 출력한다. 게다가 destroy까지 소멸전 함수까지 호출하는 전형적인 싱글톤의 모습을 보여주고 있다.
그렇다면, 프로토타입 스코프의 경우는? 코드와 사진을 보여주자면~
public class PrototypeTest {
@Test
void prototypeBeanFine(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
System.out.println("find prototypeBean1");
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
System.out.println("find prototypeBean2");
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
System.out.println("prototypeBean1 = " + prototypeBean1);
System.out.println("prototypeBean2 = " + prototypeBean2);
assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
ac.close();
}
@Scope("prototype")
static class PrototypeBean{
@PostConstruct
public void init(){
System.out.println("PrototypeBean.init");
}
@PreDestroy
public void destroy(){
System.out.println("PrototypeBean.destroy");
}
}
}
예상한 결과이길 바란다.
저 find prototypeBean1을 왜 만들었냐면, 싱글톤은 스프링 컨테이너 생성과 동시에 만들어 지지만 프로토타입은 조회 요청이 들어와야지만 생성한다고 언급했다. 그래서 보면 find prototypeBean1 을 출력하고 요청이 들어왔으니 그제서야 init함수가 호출 된 것을 볼 수 있다.
게다가 요청이 들어올 때마다 초기화 한다고 하였으므로 2번째 프로토타입을 만들 때에도 init이 한번 더 생성 된다. 어머어머 거기다가 지금 prototypeBean1,2의 값이 다르다. 어머머멈머 게다가 소멸 전 콜백 함수도 호출이 안되었다.
이제 기존 설명들의 복선이 따악! 하고 떨어져 맞는다. 프로토타입은 스프링 컨테이너 생성 시점이 아닌 요청이 들어와야 그제서야 컨테이너가 빈을 만들고 의존관계 주입 후(이건 아직 안보이지만 아무튼) 컨테이너에서 드러내 버려서 관리 하지 않기에 종료 또한 신경 쓰지 않는다. 소오르음
휴, 오늘 글은 여기까지. 빈 스코프 더 다룰 려고 했는데 글이 너무 길어져서 여기서 끊어야 겠다.
다음 글은 프로토타입 스코프의 빈과 싱글톤 빈이 의존관계로 묶여있다면?(으악) 문제가 생겨? 그럼 해결법은? 아니면 괜찮은 거야? 를 다룰 거고 웹 스코프에 대해서도 다루겠다. 기대하시라!