프레임워크를 통한 앱 개발을 하다보면 정말 많이 듣는 단어인 Life Cycle (생명주기) 이 Bean 에도 역시 있습니다.
사실 이 Bean 의 생명주기보다 Spring App 자체의 생명주기 뼈대를 우선적으로 이해하는 것이 중요하지만, 이번
포스트에서는 Bean 의 생명주기에 대해서 살펴보겠습니다. 생명주기를 제어하기 위해서는 앱이 실행되면서 필요한
연결들을 미리 해두고 (이 경우 Bean 등록), 앱이 종료 시점에 모든 연결을 해제하고 메모리를 비워낼 수 있는 작업
을 진행해야 하며, 이를 중점으로 살펴볼 예정입니다.
우선 이번 포스트에서 Bean 생명주기 이해를 도와줄 Network Client 객체부터 살펴봅시다. NetworkClient 는 특
정 서버와 연결을 하고 데이터 통신을 해주는 객체라고 가정하겠습니다.
public class NetworkClient{
// 간단한 의존관계
private String url;
public NetworkClient() {
// url 을 알고 있어야 함
System.out.println("생성자 호출, url = " + url);
// 초기화 작업
connect();
call("초기화 연결 메시지");
}
// 시버스 시작시 호출
public void connect() {
System.out.println("Connecting to :: " + url);
}
public void call(String msg) {
System.out.println("Call:: " + url + " Do Message:: " + msg);
}
// 종료시 연결 해제 예정
public void disconnect() {
System.out.println("close session: " + url);
}
public void setUrl(String url) {
this.url = url;
}
}
우선 지난 포스트에서 생성자 주입을 제외하고 (생성자는 동시) Bean (객체) 생성 + 의존관계 주입이 일어나게 된
다는걸 알았습니다. 그리고 이 과정이 끝나야 Bean 들을 사용할 수 있는 것임을 알 수 있습니다. 따라서, 만약 특정
객체에 초기화 과정 (initializing) 이 필요하다면, 생성과 의존관계 주입이 모두 완료된 후에 호출해야 합니다.
가령 위에 함수처럼, 생성이 된 후에 url 을 알고 있어야 한다고 해보면, 생성자는 [생성] 하는 부분, url 을 세팅해주
는 부분을 [의존관계 주입] 으로 볼 수 있습니다. 그리고 초기화 작업은 url 과 연결해 놓고 (connect 함수), url 에
test message 를 보내는 작업 (call 함수)이라고 해봅시다. 일반적으로는 다음과 같이 만들어볼 수 있을 것입니다.
@Bean
public NetworkClient networkClient() {
NetworkClient nc = new NetworkClient();
nc.setUrl("http://springday.com"); // 생성한 뒤에 의존관계를 주입함
return nc;
}
이렇게 된다면, 당연히 초기화 작업에 실패하게 됩니다. 생성자에서 DI 전에 초기화를 호출함으로, url 이 모두 null
이기 때문입니다. 의존관계가 주입되기 전에 초기화를 진행한 결과입니다. 물론 생성자 안에 url 을 전달해줘서
setting 을 해줄 수 있지만, 요점은 그게 아닙니다. 생성자, 의존관계 주입, 초기화 작업들이 명백하게 분리되어야
한다는 점이 중요한 점입니다. 즉, 의존관계 주입이 끝났음을 개발자가 명확히 알아야, 초기화 작업을 진행할 때 오
류가 발생하지 않는다는 것입니다.
이해가 안되신 분들을 위해, 위 예시가 하고자 하는 말을 간략히 정리해보겠습니다.
- 위 예시에서 생성과 DI를 같이 하냐 따로하냐가 중요한게 아니라, 초기화를 DI 전에 했는지 후에 했는지가 중요한 부분입니다.
- 초기화는 Bean 사용을 시작하는 것이기 때문에, DI가 완료되어야 하기 때문입니다.
- 그래서 개발자들이 안전하게 Bean 사용을 시작하려면, DI 완료 시점을 알아야 합니다. (물론 급하게 사용할 리는 없겠지만, 필수적으로 알아야 하는 대표적인 예시로 초기화가 있는 것입니다)
참고사항
참고로, "초기화든 DI든 다 생성자에서 순서대로 해버리면 되는거 아닌가요?" 할 수도 있습니다. 그러면 위와 같은 문제
가 발생할 이유가 없기 때문입니다. 하지만 한 함수가 수행하는 역할이 많아지면 안좋은 것처럼, 객체의 생성과 초기화는
분리되는 것이 좋습니다.
생성자는 객체 생성하는거에 집중을 해야 하기 때문입니다. 여기서 생성자의 역할이란, 필수 Params 들을 받고, 메모리를 할당해서 객체를 생성하는 책임을 가집니다. 반면에 초기화는 이렇게 생성된 값들을 활용해서 내부적으로 동작할 준비를 하는 등의 동작을 수행합니다.
이렇게 역할이 다른 두 부분을 명확하게 나누는 것이 유지 보수 관점에서 좋습니다. (물론 초기화가 정말 간단하면 생성자로 해도 됩니다)
(여기까지 이해가 안되셨다면, 한번 다시 읽으면서 이해한 뒤에 넘어가 주세요 ㅠ.ㅠ)
어쨌든.... 따라서 Spring 은 이를 위해서 DI 가 끝났음을 알려주기 위한 콜벡 메소드가 있고, 초기화 시점을 알려주
게 되는데, 이 콜백은 다양한 기능을 제공합니다. 또한 Spring 은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을
하게 됩니다. 즉, 다음과 같은 Life Cycle 을 가진다고 보면 됩니다.
Spring container 생성(App 실행) → 필요 Spring Bean 생성 → DI 진행 → 완료 Callback (DI 종료 인폼) → App 내 Bean 사용 → App 종료 전 Callback (Bean 이 사라짐을 인폼) → 스프링 App 종료
95% 가 [App 내 Bean 사용] 에서만 Bean 을 사용하게 될 것이긴 하지만, 위 상황처럼 초기화 작업이 필요한
Bean 들이 언제든 있을 수 있습니다. 따라서 Life Cycle 을 알고 언제든 활용을 하거나 발생할 수 있는 오류를 잡아
낼 수 있는 것이 중요합니다.
Spring 이 지원하는 다양한 생명주기의 콜백
1) Initializing Bean, Disposable Bean 인터페이스
public class NetworkClient implemenets InitializingBean, DisposableBean{
// 간단한 의존관계
private String url;
public NetworkClient() {
// url 을 알고 있어야 함
// System.out.println("생성자 호출, url = " + url);
// 초기화 작업
// connect();
// call("초기화 연결 메시지");
}
// 위와 동일 ...
// ...
// ...
@Override // from InitializingBean
public void afterPropertiesSet() throws Exception {
System.out.println("의존관계 주입이 끝난 이후")
// url 의존관계가 세팅이됨
connect();
call("초기화 연결 메시지");
}
@Override // from DisposableBean
public void destroy() throws Exception {
System.out.println("빈이 종료되기 직전에 호출됩니다");
// 빈이 종료 직전에 실행된다.
disconnect();
}
}
InitializingBean.class 을 상속받으면 afterPropertiesSet() 이라는 함수를, DisposableBean.class 을 상속받으면
destroy() 라는 함수를 override 하게 됩니다. 해당 함수들은 [DI 완료 Callback] 과 [Bean 파기 전 Callback] 함수
들로, 생명주기를 제어할 수 있도록 도와줍니다.
하지만해당 인터페이스들은 스프링 전용 인터페이스 입니다. 해당 코드들이 스프링에 전적으로 의존하게 됩니다.
지금까지는 Annotation 기능상 Spring 의 기술을 활용하는 것이였다면, 해당 인터페이스들은 직접 상속받아서 코
드 레벨까지 Spring 에 의존하게 되는 거라, 어느정도 부담이 되는 것이 사실입니다. (외부 라이브러리에도 적용할
수 없음)
스프링 초창기의 방식이라, 지금은 거의 사용하지 않는다고 합니다 .
2) 설정 정보에 각 메소드 지정
class LifeCycleConfig {
@Bean(initMethod = "configSetInitMethod", destroyMethod = "configSetDestroyMethod")
public NetworkClient networkClient() {
NetworkClient nc = new NetworkClient();
nc.setUrl("http://springday.com");
return nc;
}
}
Config 설정 정보 파일에 @Bean(initMethod = "메소드명", destroyMethod = "메소드명") 으로 각각 메소드를
지정해줄 수 있습니다. 해당 방식은 메소드 이름을 자유롭게 지정할 수 있고, 스프링 코드에 의존하지 않는다는 장
점이 있습니다. 또한, 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메소드
를 적용할 수 있습니다.
참고로 destroyMethod 는 default 값으 "(inferred)" 로 등록되어 있는데, 이는 자동으로 종료 메소드를 찾아서 수
행해 준다는 뜻입니다. Bean 종료 메소드가 대부분 close(), shutdown() 등의 이름을 가지고 있으니, 해당 메소드
를 자동으로 찾아서 수행해줍니다 (Bean 제어시 이 점도 알고 있어야 의도치 않은 close, shutdown 등의 함수명을
사용하지 않을 수 있습니다).
그럼 적용한 NetworkClient Bean 의 모습은 다음과 같겠죠?
public class NetworkClient implemenets InitializingBean, DisposableBean{
// 간단한 의존관계
private String url;
public NetworkClient() {
//...
}
// 위와 동일 ...
// ...
// ...
public void configSetInitMethod(){
System.out.println("Config 에 지정해놓을 Init Method");
System.out.println("생성자 이후 호출입니다");
connect();
call("초기화 연결 메시지");
}
public void configSetDestroyMethod(){
System.out.println("Config 에 지정해놓을 Destroy Method");
System.out.println("종료될 시점입니다");
}
}
3) Annotation 지원
사실 가장 편하게 이걸 쓰면 된다고 생각하시면 됩니다 (스프링에서도 권장하는 방식).
@PostContruct : 생성이 된 이후
@PreDestroy : 소멸되기 전에
에노테이션으로 지원되니 매우 편리합니다. 또한, 해당 어노테이션들은 스프링 종속 기술이 아니라 자바 표준 기술
입니다. 또한, Bean 등록을 잘 해주지 않는 ComponentScan 방식에서도 잘 어울립니다. (Bean 지정 없이 @ 를
통해서 사용할 수 있다). 하지만 해당 방식은 외부 Library 를 적용하지 못합니다. 예를 들어, 해당 Bean 이 상속받
는 외부 함수를 그대로 사용하되, 해당 함수를 @PostConstruct 로 지정하고 싶은 경우가 있습니다. 따라서 외부 라
이브러리에 적용이 필요하면 2번 방식을 적용해주시면 됩니다.
어쨌든 Annotation 을 사용하면 외부에서 지정해주거나, InitializingBean.class 같은 함수를 상속받을 필요도
없이, 다음과 같이 깔끔히 정리가 될 것입니다.
public class NetworkClient implemenets InitializingBean, DisposableBean{
// 간단한 의존관계
private String url;
public NetworkClient() {
//...
}
// 위와 동일 ...
// ...
// ...
@PostConstruct
public void configSetInitMethod(){
System.out.println("Annotation 으로 지정된 Init");
System.out.println("생성자 이후 호출입니다");
connect();
call("초기화 연결 메시지");
}
@PreDestroy
public void configSetDestroyMethod(){
System.out.println("annotation 으로 지정된 destroy");
System.out.println("종료될 시점입니다");
}
}
그래서 뭘 쓰라는거냐? 할 수 있어서 정리해드리면, 필요시 (3) 을 쓰되, 외부 라이브러리를 직접 제어해야 하면 (2)
방식을 쓰면 될 것 같습니다.
포스트 요약
- Bean 은 생명주기가 있고, Bean 이 사용되기 위해서는 해당 Bean 에 대한 생성, 의존성 주입이 완료되어야 한다.
- Bean 의 실질적 사용전에 생성 및 의존성 주입이 모두 완료되었음을 알기 위해, 생명주기 Callback 을 수행시킬 수 있는 방법을 활용해야한다.
- 그 방법에는 인터페이스 상속, @Bean 지정하는 Config 에서 지정, Bean Class 내에서 Annotation 으로 지정이 있다.
- Annotation 쓰셈 ㅇㅇ
출처
[스프링 기본]으로 엮인 모든 포스트들은 교육 사이트 인프런의 지식공유자이신 김영한님의 [스프링 핵심 원리] 강의를 기반으로 작성되었습니다. 열심히 정리하고 스스로 공부하기 위해 만든 포스트이지만, 제대로 공부하고 싶으시면 해당 강의를 꼭 들으시는 것을 추천드립니다.
스프링 핵심 원리 - 기본편 - 인프런 | 강의
스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...
www.inflearn.com
'Spring > Spring 기본' 카테고리의 다른 글
[Spring과 DB] 5-1 Spring 에서의 예외처리 지원 (0) | 2023.08.15 |
---|---|
[Spring 기본] Bean Scope (0) | 2022.10.19 |
[Spring 기본] 의존 관계 주입 전략 (DI Strategy) (0) | 2022.10.17 |
[Spring 기본] Component Scan을 통한 Bean 자동화 관리 (0) | 2022.10.17 |
[Spring 기본] Spring Container의 Singleton 전략 (0) | 2022.10.14 |