목차
서론
애플리케이션 시작 시점에 필요한 연결을 미리 해두고 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행해야하는 경우가 있습니다. ex) 데이터베이스 커넥션 풀 또는 네트워크 소켓
이를 초기화 작업, 종료 작업이라고 합니다. 이러한 작업은 객체 생성자가 아니라 다른 메서드에서 진행되어야 합니다.
스프링은 해당 작업을 수행하는 별도의 메서드를 제공합니다.
참고 데이터베이스 커넥션 풀
애플리케이션 서버가 올라올 때 데이터베이스와 미리 연결을 맺어놓습니다. 이유는 TCP/IP Handshake를 하는데 오래 걸리기 때문에 약 10개~많으면 100개까지 미리 잡아놓습니다. 고객 요청이 들어올 때 연결해 놓은 것을 재활용하여 사용합니다.
문제 상황
- 서버가 뜰 때 외부 네트워크에 미리 연결하는 객체를 생성한다고 가정해봅시다.
- 실제 네트워크에 연결하는 코드 대신 간단하게 connect() 함수를 호출합니다.
- NetworkClient 클래스의 객체는 애플리케이션 생성 시점에 connect()를 호출해 연결을 맺어두고, 애플리케이션 종료 시점에 disconnect()를 호출해 연결을 끊어야 합니다.
- 해당 구현은 다음과 같습니다.
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);
}
}
테스트 코드
public class BeanLifeCycleTest {
@Configuration
static class LifeCyCleConfig {
@Bean
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hello-spring.dev");
return networkClient;
}
}
@Test
public void lifeCycleTest() {
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCyCleConfig.class);
ac.close(); // 스프링 컨테이너를 종료 -> ConfigurableApplicationContext 필요 (AnnotationConfigApplicationContext 대신)
}
}
- 스프링 컨테이너를 띄우며, LifeCycleConfig를 설정파일로 등록합니다. 해당 파일은 위에서 만든 NetworkClient 객체를 빈으로 등록하고 있습니다.
- 객체를 생성한 후 setUrl을 통해 url을 주입합니다.
- ac.close()를 통해 컨테이너를 종료시킵니다.
실행 결과
생성자 호출, url = null
connect: null
call: null message = 초기화 연결 메시지
- 원했던 결과는 connect에 url 정보가 담겨서 호출되고, 서버가 종료되면 disconnect() 함수가 호출되는 것입니다.
- 하지만, url 정보 없이 connect가 호출되었습니다. 생성자에서 connect() 함수를 호출하고, 객체 생성 후 setUrl()을 호출 했기 때문에 당연한 이야기입니다.
- 연결 해제 메서드인 disconnect() 함수 또한 호출되지 않았습니다.
- 객체가 생성된 후 호출되는 초기화 메서드와 객체가 소멸될 때 호출되는 소멸 메서드를 사용하여 이를 해결할 수 있습니다.
스프링 빈의 라이프 사이클
객체 생성 → 의존관계 주입
(단, 생성자 주입은 예외. 생성자 주입은 객체를 생성할 때 주입한다.)
- 스프링 빈은 객체를 생성한 후 의존관계 주입까지 다 끝난 시점이 필요한 데이터를 사용할 수 있는 준비 완료상태입니다.
- 따라서 위와 같이 생성자 안에 초기화 작업을 진행하면 의존관계 주입이 끝나지 않은 채로 초기화 작업이 진행되어 원하는 결과를 얻을 수 없습니다.
- 즉, 초기화 작업은 의존관계 주입이 모두 완료된 후에 호출되어야 합니다.
콜백 메서드
- 이를 해결하기 위해 스프링은 초기화 시점을 알려주는 콜백 메서드를 제공합니다.
- 또한, 스프링 컨테이너가 종료되기 직전에 소멸 콜백 메서드 또한 제공합니다. 따라서 안전하게 종료 작업을 진행할 수 있습니다.
스프링 빈의 이벤트 라이프 사이클
스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸전 콜백 → 스프링 종료
- 초기화 콜백: 빈이 생성되고 빈의 의존관계가 주입이 완료된 후 호출
- 소멸전 콜백: 빈이 소멸되기 직전에 호출
참고 객체의 생성과 초기화를 분리하자. (단일책임원칙)
생성자는 필수 정보(파라미터)를 받고 메모리를 할당하여 객체를 생성하는 책임을 가집니다.
반면, 초기화는 이렇게 생성된 값들을 활용해 외부 커넥션을 연결하는 등의 무거운 동작을 수행합니다.
따라서 생성자 안에서 무거운 초기화 작업을 함께 하는 것보다 객체를 생성하는 부분과 초기화하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋습니다.
물론 초기화 작업이 내부 값들만 약간 변경하는 정도로 단순한 경우에는 생성자에서 한 번에 다 처리하는 것이 더 나을 수도 있습니다.
추가 장점 : 지연 (Lazy)
객체를 생성한 후, 외부 커넥션을 맺는 일은 최초의 요청이 올 때까지 미룰 수 있습니다.
참고 빈의 소멸전 콜백 시점
싱글톤 빈들은 스프링 컨테이너가 종료될 때 싱글톤 빈들도 함께 종료되기 때문에 스프링 컨테이너가 종료되기 직전에 소멸전 콜백이 일어납니다.
싱글톤처럼 컨테이너의 시작과 종료까지 생존하는 빈도 있지만, 생명주기가 짧은 빈들도 있는데 이 빈들은 컨테이너와 무관하게 해당 빈이 종료되기 직전에 소멸전 콜백이 일어납니다.
자세한 내용은 스코프에서 설명합니다.
스프링이 제공하는 3가지 해결 방안
- 인터페이스 (InitializingBean, DisposableBean)
- 설정 정보에 초기화 메서드, 소멸 메서드 지정
- @PostConstruct, @PreDestroy 애노테이션
결론부터 말하면 @PostConstructor, @PreDestroy 애노테이션을 가장 많이 사용합니다.
인터페이스 (InitializingBean, DisposableBean)
- InitializingBean은 afterPropertiesSet() 메서드로 초기화를 지원합니다.
- DisposableBean은 destory() 메서드로 소멸을 지원합니다.
package hello.core.lifecycle;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
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 afterPropertiesSet() throws Exception {
System.out.println("NetworkClient.afterPropertiesSet");
connect();
call("초기화 연결 메시지");
}
@Override
public void destroy() throws Exception {
System.out.println("Closing NetworkClient.destory");
disConnect();
}
}
출력 결과
생성자 호출, url = null
NetworkClient.afterPropertiesSet
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 초기화 연결 메시지
13:59:23.070 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@2ea6137, started on Fri Jun 30 13:59:22 YAKT 2023
Closing NetworkClient.destory
close + http://hello-spring.dev
출력 결과를 보면 원하는 결과가 나온 것을 확인할 수 있습니다.
- 초기화 메서드가 의존관계 주입 완료 후에 호출된 것을 확인할 수 있습니다.
- 또한, 스프링 컨테이너 종료가 호출되자 소멸 메서드가 호출된 것을 확인할 수 있습니다.
초기화, 소멸 인터페이스의 단점
- 스프링 전용 인터페이스입니다.
해당 코드는 스프링 전용 인터페이스에 의존합니다. 따라서 스프링이 아닌 다른 컨테이너에서는 사용이 불가합니다. (애노테이션까진 괜찮은데 코드 레벨로 인터페이스를 가져오는 건 부담이 됩니다.) - 초기화, 소멸 메서드의 이름을 변경할 수 없습니다.
- 외부 라이브러리를 다운 받아 사용하여 코드를 수정할 수 없다면 적용할 수 없습니다.
참고 사용하지 말자!
인터페이스를 사용하는 초기화, 종료 방법은 스프링 초창기에 나온 방법입니다.
지금은 다음의 더 나은 방법들이 있어 거의 사용하지 않습니다.
설정 정보에 초기화 메서드, 소멸 메서드 지정
설정 정보에 @Bean(initMethod="init", destroyMethod="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;
}
}
package hello.core.lifecycle;
public class NetworkClient {
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);
}
public void init() {
System.out.println("NetworkClient.init");
connect();
call("초기화 연결 메시지");
}
public void close() {
System.out.println("NetworkClient.close");
disConnect();
}
}
출력 결과
생성자 호출, url = null
NetworkClient.init
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 초기화 연결 메시지
14:13:45.283 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@2ea6137, started on Fri Jun 30 14:13:44 YAKT 2023
NetworkClient.close
close + http://hello-spring.dev
설정 정보 사용 특징
- 메서드 이름을 자유롭게 지을 수 있습니다.
- 스프링 코드에 의존하지 않습니다.
- 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 라이브러리에도 설정된 초기화, 종료 메서드를 적용할 수 있습니다.⭐
@Bean(destroyMethod="")의 종료 메서드 추론
@Bean의 destroyMethod 속성에는 아주 특별한 기능이 있습니다.
라이브러리는 대부분 close, shutdown 이라는 이름의 종료 메서드 이름을 사용합니다.
@Bean의 destroyMethod는 기본 값이 (inferred) (추론)으로 등록되어 있습니다.
이 추론 기능은 close, shutdown 라는 이름의 메서드를 자동으로 호출합니다. 이름 그대로 종료 메서드를 추론하여 호출합니다.
따라서, 직접 스프링 빈으로 close 와 같은 이름의 종료 메서드를 작성하면, 빈 등록 시 따로 종료 메서드를 적어주지 않아도 잘 동작합니다.
추론 기능을 사용하고 싶지 않다면, destroyMethod="" 와 같이 빈 공간을 저장하면 됩니다.
애노테이션 @PostConstruct, @PreDestroy
package hello.core.lifecycle;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
public class NetworkClient {
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);
}
@PostConstruct
public void init() {
System.out.println("Networkclient.class");
connect();
call("초기화 연결 메시지");
}
@PreDestroy
public void close() {
System.out.println("NetworkClient.close");
disConnect();
}
}
설정 정보
@Configuration
static class LifeCyCleConfig {
@Bean
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hello-spring.dev");
return networkClient;
}
}
출력 결과
생성자 호출, url = null
Networkclient.class
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 초기화 연결 메시지
14:25:05.753 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@2ea6137, started on Fri Jun 30 14:25:04 YAKT 2023
NetworkClient.close
close + http://hello-spring.dev
@PostConstruct, @PreDestroy 애노테이션 특징
- 최신 스프링에서 가장 권장하는 방법입니다.
- 애노테이션 하나만 붙이면 되므로 매우 편리합니다.
- 패키지를 잘 보면 javax.annotation.PostConstruct입니다. 즉, 스프링에 종속적인 기술이 아니라 JSR-250이라는 자바 표준입니다. 따라서 스프링이 아닌 다른 컨테이너에서도 동작합니다.
- 컴포넌트 스캔과 잘 어울립니다.
- 유일한 단점은, 외부 라이브러리에는 적용을 하지 못합니다. 외부 라이브러리를 초기화, 종료해야 한다면 @Bean의 메서드 지정 기능을 사용합시다.
결론
- @PostConstruct, @PreDestroy 애노테이션을 사용하자.
- 코드를 고칠 수 없는 라이브러리를 초기화, 종료해야 한다면 @Bean의 initMethod, destroyMethod를 사용하자.
출처
'Backend > Spring | SpringBoot' 카테고리의 다른 글
[SpringBoot] DL (ObjectProvider, JSR-330 Provider) (0) | 2023.07.10 |
---|---|
[SpringBoot] 빈 스코프 (싱글톤과 프로토타입 스코프) (0) | 2023.07.09 |
[SpringBoot] @Autowired 매칭한 빈이 2개 이상일 때 해결방안 (0) | 2023.06.24 |
[SpringBoot] 롬복(@RequiredArgsContructor), 적용 방법(gradle) (0) | 2023.06.24 |
[SpringBoot] 자동 의존관계 주입 방법 4가지 (0) | 2023.06.24 |