-
Bean Lifecycle CallbackJava/Spring 2025. 1. 3. 17:22
스프링의 빈 생명주기 콜백에 대해 정리하였습니다.
< 빈 생명주기 콜백 >
데이터베이스 커넥션 풀이나 네트워크 소켓처럼, 애플리케이션 시작 시점에 필요한 연결을 미리 해두고 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체(빈)의 초기화와 종료 작업이 필요합니다.
Ex)
- 외부 네트워크에 미리 연결하는 객체를 하나 생성한다고 가정.
- 실제로 네트워크에 연결되는 것은 아니고 단순히 문자열을 출력하는 코드.
- NetworkClient 객체는 애플리케이션의 시작 시점에 connect() method를 호출해서 연결을 맺고
- 애플리케이션이 종료되면, disconnect() method를 호출해서 연결 끊음.
로직 코드 :
package hello.core.lifecycle; public class NetworkClient { private String url; public NetworkClient() { System.out.println("생성자 호출, url = " + url); connect(); call("초기화 연결 메시지"); } public void setUrl(String url) { this.url = url; } //서비스 시작시 호출 public void connect() { System.out.println("connent: " + url); } public void call(String message) { System.out.println("call: " + url + ", message = " + message); } //서비스 종료시 호출 public void disconnect() { System.out.println("disconnect: " + url); } }
테스트 :
package hello.core.lifecycle; class BeanLifeCycleTest { @Test public void lifeCycleTest() { ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class); NetworkClient client = ac.getBean(NetworkClient.class); ac.close(); //스프링 컨테이너를 종료, ConfigurableApplicationContext 필요 } @Configuration static class LifeCycleConfig { @Bean public NetworkClient networkClient() { NetworkClient networkClient = new NetworkClient(); networkClient.setUrl("http://hello-spring.dec"); return networkClient; } } }
테스트 결과 : 객체 생성 시 초기에는 url이 null입니다. setUrl() 메서드를 통해 외부에서 수정자 주입을 해야만 url 값이 설정됩니다.
앞서 계속 언급한 바와 같이 스프링 빈의 라이프사이클은
- 객체 생성
- 의존관계 주입
위 두 단계를 거쳐서 일어납니다. 그렇기 때문에 객체가 생성되고, 이에 따른 의존관계가 주입된 이후에야 필요한 데이터를 사용할 수 있는 준비가 완료됩니다.
그런데, 개발자의 입장에서 의존관계가 주입 완료된 시점을 어떻게 알 수 있을까요?
이를 위해, 스프링은 의존관계 주입이 완료된 후 스프링 빈에게 콜백 메서드를 통해 초기화 시점을 알리는 기능과, 스프링 컨테이너가 종료되기 직전에 소멸 콜백 메서드을 제공하여 안전하게 종료 작업을 수행할 수 있도록 지원합니다.
(여기서 초기화란 객체의 생성이 아니라, 객체가 필요한 모든 값을 갖추고 제대로 작동할 준비가 된 상태를 의미합니다.)
[ 스프링 빈의 이벤트 라이프사이클 ]
스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸 전 콜백 → 스프링 종료
스프링 빈이 생성된 이후에 의존관계 주입이 이루어지고, 의존관계 주입까지 갖춰진 이후에 초기화 작업을 수행해야 스프링 빈이 제 역할을 할 수 있도록 만들어집니다.
- 초기화 콜백 : 빈이 생성되고, 빈의 의존관계 주입이 완료된 후에 호출됩니다.
- 소멸자 콜백 : 빈이 소멸되기 직전에 호출됩니다.
스프링은 크게 3가지 방법으로 빈 생명주기 콜백을 지원합니다.
- 인터페이스(InitializingBean, DisposableBean)
- 설정 정보에 초기화 메서드, 종료 메서드를 지정
- @PostConstruct, @PreDestroy 어노테이션 지원
< 객체의 생성과 초기화를 분리하자 >
생성자 호출 시 모든 것을 처리하는 것이 더 간단하지 않을까라는 의문이 들 수 있지만, 이는 객체 지향 설계의 원칙 중 하나인 SRP(단일 책임 원칙)와 관련이 있습니다.
생성자는 필수 정보(파라미터)를 수집하고 메모리를 할당하여 객체를 생성하는 역할만을 맡습니다. 반면, 초기화는 생성된 객체를 이용해 외부 커넥션을 설정하는 등 추가적인 무거운 작업을 수행합니다. 따라서, 생성자에 무거운 초기화 작업을 포함시키기보다는 초기화를 별도로 분리하는 것이 유지보수 측면에서 더 바람직합니다.
다만, 초기화 작업이 내부 값을 단순히 변경하는 정도라면 생성자에서 처리하는 것이 유리할 수도 있습니다.
< 인터페이스 InitializingBean, DisposableBean 사용 >
스프링 빈은 두 인터페이스, 즉 InitializingBean과 DisposableBean을 상속받고 이를 오버라이드하여 커스터마이징 함으로써 생명주기 콜백을 사용합니다.
InitializingBean은 afterPropertiesSet() method로 초기화를 지원하고, DisposableBean은 destory() method로 소멸을 지원합니다.Ex)
package hello.core.lifecycle; public class NetworkClient implements InitializingBean, DisposableBean { //... @Override public void afterPropertiesSet() throws Exception { System.out.println("NetworkClient.afterPropertiesSet"); connect(); call("초기화 연결 메시지"); } @Override public void destroy() throws Exception { System.out.println("NetworkClient.destroy"); disconnect(); } }
테스트 결과 : 출력 결과를 통해 초기화 메서드가 주입 완료 후 호출되고, 스프링 컨테이너 종료 시 소멸 메서드가 호출되는 것이 확인됩니다.
초기화, 소멸 인터페이스 사용의 단점
- 이 인터페이스는 스프링 전용 인터페이스입니다. 따라서 해당 코드가 스프링 전용 인터페이스에 의존하게 된다.
- 초기화, 소멸 메서드의 이름을 변경 불가합니다.
- 내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수가 없습니다.
→ 이 방식은 스프링 초창기에 나온 방식으로 최근에는 거의 사용하지 않는다고 합니다.
< 빈 등록 초기화, 소멸 메서드 >
설정 정보에 @Bean(initMethod = "init", destroyMethod = "close")처럼 초기화, 소멸 메서드를 지정함으로써 생명주기 콜백을 사용합니다.
Ex)
1. 클래스 내에 초기화 및 소멸 시 호출될 메서드를 직접 정의합니다.
public class NetworkClient { //... public void init() { System.out.println("NetworkClient.init"); connect(); call("초기화 연결 메시지"); } public void close() { System.out.println("NetworkClient.close"); disconnect(); } }
2. 빈으로 등록 시 초기화 및 소멸 시 호출할 메서드를 지정합니다.
package hello.core.lifecycle; class BeanLifeCycleTest { //... @Configuration static class LifeCycleConfig { @Bean(initMethod = "init", destroyMethod = "close") public NetworkClient networkClient() { NetworkClient networkClient = new NetworkClient(); networkClient.setUrl("http://hello-spring.dec"); return networkClient; } }
설정 정보 사용의 특징
- 메서드의 이름을 자유롭게 설정할 수 있습니다.
- 스프링 빈이 스프링 코드에 의존하지 않게 됩니다.
- 코드가 아니라 설정 정보를 사용하기 때문에, 코드를 고칠 수 없는 외부 라이브러리에도 적용이 가능합니다.
+. 종료 메서드의 추론
@Bean의 destroyMethod 속성에는 종료 메서드를 자동으로 호출하는 특별한 기능이 있습니다. 대부분의 라이브러리는 종료 메서드의 이름으로 close나 shutdown을 사용하며, destroyMethod의 기본값은 inferred로 설정되어 있어 이러한 이름의 메서드를 자동으로 호출합니다. 이 기능 덕분에 종료 메서드를 따로 지정하지 않아도 잘 동작하지만, 원하지 않으면 destroyMethod = ""로 설정해 비활성화할 수 있습니다.
< 어노테이션 @PostConstruct, @PreDestroy >
초기화와 소멸 과정에서 수행하고 싶은 메서드에 @PostConstruct와 @PreDestroy 애노테이션을 붙이면 생명주기 콜백을 사용할 수 있습니다.
JSR-250라는 자바 표준 기술입니다.Ex)
package hello.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(); } }
@PostConstruct, @PreDestroy 어노테이션 사용의 특징
- 최신 스프링에서 가장 권장하는 방법입니다.
- 어노테이션만 잘 붙어있으면 돼서 아주 편리합니다.
- 자바 표준 기술이기 때문에, 스프링 컨테이너가 아니어도 잘 동작합니다.
- 컴포넌트 스캔과 잘 어울립니다.
- 유일한 단점은 외부 라이브러리에는 적용하지 못한다는 점입니다. 외부 라이브러리를 초기화, 종료하는 과정을 정의하고 싶은 경우 @Bean을 섞어서 사용합시다.
지금까지 내용을 정리하자면, 기본적으로 @PostConstruct, @PreDestroy 어노테이션을 사용하되, 코드를 고칠 수 없는 외부 라이브러리를 사용하는 경우 @Bean의 initMethod, destroyMethod를 사용하는 것이 좋습니다.
해당 글에 포함된 코드나 그림은 김영한님이 제공해주신 자료를 바탕으로 작성되었습니다.
스프링 핵심 원리 - 기본편 강의 | 김영한 - 인프런
스프링 핵심 원리 - 기본편 강의 | 김영한 - 인프런
김영한 | 스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보
www.inflearn.com
'Java > Spring' 카테고리의 다른 글
Understanding Web Applications (5) 2025.01.14 Bean Scope (1) 2025.01.12 Automatic injection of dependencies (1) 2025.01.03 Component Scan (3) 2024.12.20 Singleton Container (0) 2024.12.18