[Spring] 스프링 빈 생명주기 콜백
스프링 프레임워크 기반의 애플리케이션에서는 데이터베이스를 사용하거나 TCP 소켓 연결을 할때 프로그램 시작시 미리 연결을 합니다. 그리고 프로그램이 종료될때 연결된 작업들을 모두 종료하는데 이번 포스팅에서는 스프링에서 이 연결과 종료 작업을 어떻게 수행하는지 대해서 알아보겠습니다.
테스트를 위해 TCP 소켓 연결 상황을 표현할 클래스와 테스트 메서드를 만들어보겠습니다.
NetworkClient.java
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("connect: " + url);
}
public void call(String message) {
System.out.println("call: " + url + " message = " + message);
}
// 서비스 종료시 호출
public void disconnect() {
System.out.println("close: " + url);
}
}
BeanLifeCycleTest.java
public class BeanLifeCycleTest {
@Test
public void lifeCycleTest() {
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
NetworkClient client = ac.getBean(NetworkClient.class);
ac.close();
}
@Configuration
static class LifeCycleConfig {
@Bean
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://localhost");
return networkClient;
}
}
}
ConfigurableApplicationContext : ApplicationContext를 상속받는 애플리케이션 컨텍스트 클래스이며 애플리케이션 컨텍스트의 종료를 나타내는 close 메서드를 호출하기 위해 사용했습니다.
결과
생성자 호출 url = null
connect: null
call: null message = 초기화 연결 메시지
lifeCycleTest 메서드에서 NetworkClient 클래스 객체에 의존관계 주입시 스프링 빈에 등록된 networkClient 메서드에서 반환해주는 인스턴스를 받게됩니다. 이때 networkClient 메서드에서 NetworkClient 클래스의 생성자를 호출하는데 생성자 메서드를 확인해보면 url 필드에 값이 지정되지 않았기 때문에 url 이 포함된 출력문에서 모두 null이 나오는것을 확인할수 있습니다.
생성자 호출 이후에 setter를 통해 url을 지정하지만 프로그램의 목적에는 맞지않게 의존관계 주입시 연결된 소켓을 제대로 전달하지 못하는것과 같습니다.
스프링 빈 라이프사이클
객체 생성 => 의존관게 주입
스프링 빈은 객체 생성후 의존관계 주입이 다 끝나야 데이터를 사용할 준비가 완료됩니다. 따라서 TCP 소켓연결 혹은 데이터베이스 연결같은 초기화 작업은 의존관계 주입이 된 이후에 수행되야 하는데 개발자가 어떻게 해당시점을 알수있을까요??
스프링에서는 의존관계 주입이 완료되면 스프링 빈에게 초기화 콜백(Callback) 메서드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공하고 스프링 컨테이너가 종료되기 직전에는 소멸 콜백(Callback) 메서드를 호출하게 하여 안전하게 종료할수 있게 합니다.
스프링 빈 이벤트 라이프사이클
스프링 컨테이너 생성 => 스프링 빈 생성 => 의존관계 주입 => 초기화 콜백 => 사용 => 소멸 콜백 => 스프링 종료
초기화 콜백 : 스프링 빈이 생성되고 의존관계 주입이 완료된 후 호출
소멸 콜백 : 스프링 빈이 소멸되기 직전에 호출
<참고>
객체의 생성과 초기화는 분리하는것이 좋습니다. 생성자의 역할은 필수 파라미터를 받아 객체를 생성하는 작업을 수행하고 초기화는 이러한 값들을 활용하여 외부 연결과 같은 무거운 동작을 수행합니다. 따라서 생성자 메서드 내부에 초기화를 포함 하는것보다는 생성자와 초기화를 분리하는것이 유지보수에 좋습니다.
스프링 빈 생명주기 콜백 관리
스프링 빈에서는 다음과 같은 3가지 방법으로 스프링 빈 생명주기 콜백을 지원하는데 실무에서는 @PostConstruct, @PreDestroy 방식을 주로 사용하며 나머지 방법들은 참고하는 정도로만 알면 됩니다.
- 인터페이스 (InitializingBean, DisposableBean) 상속
- 설정 정보에 초기화 메서드, 종료 메서드지정
- @PostConstruct, @PreDestroy 애노테이션
1. 인터페이스 InitializingBean, DisposableBean
스프링 빈에 등록할 클래스에 인터페이스를 상속하여 destroy, afterPropertiesSet 메서드를 재정의하여 스프링 빈 생명주기를 관리하는 방법입니다.
afterPropertiesSet 메서드는 의존관계 주입이 되고나서 호출되고 destroy 메서드는 스프링이 종료되는 시점에 호출됩니다.
NetworkClient.java
public class NetworkClient implements InitializingBean, DisposableBean {
private String url;
public NetworkClient() {
System.out.println("생성자 호출 url = " + url);
}
public void setUrl(String url) {
this.url = url;
}
// 서비스 시작시 호출
public void connect() {
System.out.println("connect: " + url);
}
public void call(String message) {
System.out.println("call: " + url + " message = " + message);
}
// 서비스 종료시 호출
public void disconnect() {
System.out.println("close: " + url);
}
@Override
public void destroy() throws Exception {
System.out.println("NetworkClient.destroy");
disconnect();
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("NetworkClient.afterPropertiesSet");
connect();
call("초기화 연결 메시지");
}
}
BeanLifeCycleTest.java
public class BeanLifeCycleTest {
@Test
public void lifeCycleTest() {
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
NetworkClient client = ac.getBean(NetworkClient.class);
ac.close();
}
@Configuration
static class LifeCycleConfig {
@Bean
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://localhost");
return networkClient;
}
}
}
<결과>
생성자 호출 url = null
NetworkClient.afterPropertiesSet
connect: http://localhost
call: http://localhost message = 초기화 연결 메시지
22:12:58.478 [Test worker] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@7f36662c, started on Thu May 26 22:12:58 KST 2022
NetworkClient.destroy
close: http://localhost
테스트 코드에서 의존관계가 주입이 되고나서 afterPropertiesSet 메서드에서 NetworkClient.afterPropertiesSet 이라는 문자열을 출력하고 connect 메서드를 수행하는 결과를 확인할수 있습니다. 스프링이 종료될때는 destroy 메서드에서NetworkClient.destroy 이라는 문자열을 출력하고 close 메서드를 호출하고 있습니다.
인터페이스 사용방식은 스프링 초기의 방식이고 현재는 거의 사용하지 않는데 여러가지 단점들이 존재하기 때문입니다.
- 스프링 전용 인터페이스라서 스프링에 의존한다.
- 초기화, 소멸 메소드의 이름을 변경할수 없다.
- 외부 라이브러리에 적용할수 없다.
2. 빈 등록 초기화, 소멸 메서드 지정
스프링 빈 설정정보를 등록할때 사용자가 직접 생성한 초기화, 소멸 메서드를 지정하는 방식입니다.
NetworkClient.java
public class NetworkClient {
...
...
public void init() {
System.out.println("NetworkClient.init");
connect();
call("초기화 연결 메시지");
}
public void close() {
System.out.println("NetworkClient.close");
disconnect();
}
}
BeanLifeCycle.java
public class BeanLifeCycleTest {
@Test
public void lifeCycleTest() {
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
NetworkClient client = ac.getBean(NetworkClient.class);
ac.close();
}
@Configuration
static class LifeCycleConfig {
@Bean(initMethod = "init", destroyMethod = "close")
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://localhost");
return networkClient;
}
}
}
인터페이스 방식과 마찬가지로 결과가 나온것을 볼수있는데 구현방식은 더 간단합니다. 스프링 빈을 등록하는 메서드 Bean 애노테이션 내부에 초기화 함수인 initMethod, 소멸 함수인 destroyMethod 의 이름을 지정할수 있습니다.
메서드 지정 방식은 다음과 같은 특징이 있습니다.
- 메서드 이름을 자유롭게 줄수있다.
- 스프링에 의존하지 않는다.
- 외부 라이브러리에도 적용할수 있다.
3. @PostConstruct, @PreDestroy 애노테이션
이름 그대로 @PostConstruct는 초기화 함수에 사용하고 @PreDestroy는 소멸 함수에 사용합니다.
NetworkClient.java
public class NetworkClient {
...
...
@PostConstruct
public void init() {
System.out.println("NetworkClient.init");
connect();
call("초기화 연결 메시지");
}
@PreDestroy
public void close() {
System.out.println("NetworkClient.close");
disconnect();
}
}
BeanLifeCycleTest.java
public class BeanLifeCycleTest {
@Test
public void lifeCycleTest() {
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
NetworkClient client = ac.getBean(NetworkClient.class);
ac.close();
}
@Configuration
static class LifeCycleConfig {
@Bean
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://localhost");
return networkClient;
}
}
}
이 방식은 스프링에서 가장 권장하는 방식이며 애노테이션만 붙이면 되므로 아주 편리합니다.
- javax 패키지에 포함되므로 스프링에 의존하지 않는다.
- 컴포넌트 스캔과 잘 사용된다.
- 외부 라이브러리에는 사용하지 못한다는 단점이 있다. 외부 라이브러리 사용은 두번째 방법인 @Bean 방식을 사용한다.
테스트 소스(GitHub)
출처 : 인프런 - 우아한 형제들 기술이사 김영한의 스프링 완전 정복 (스프링 핵심원리 - 기본 편)
'WEB > Spring' 카테고리의 다른 글
[Spring] @RequestMapping HTTP Header 데이터 유형 / produces, consume 의미와 역할 (0) | 2022.07.06 |
---|---|
[Spring] 스프링 빈 웹 스코프, request 타입과 프록시 모드 - 2/2 (2) | 2022.06.19 |
[Spring] 스프링 빈 스코프(Scope) 싱글톤, 프로토 타입 - 1/2 (0) | 2022.06.19 |
[Spring] 리액티브 스트림 Operator 의 개념과 예제 (0) | 2022.02.25 |
[Spring] 리액티브 스트림 기본 개념 (Publisher, Subscriber, Subscription) (0) | 2022.02.17 |
댓글