데이터베이스 커넥션 풀이나, 네트워크 소켓 연결 등 스프링 프로젝트가 시작될 때 필요한 연결을 미리 해두고, 종료 시점에 연결을 모두 종료하는 작업을 진행하려면 객체의 초기화와 종료 작업이 필요하다.
// ex) 커넥션 풀의 connect, disconnect
// 테스트 케이스 작성 시 테스트 픽스처 준비를 하는 경우에도 초기화 및 종료 작업을 해야한다.
외부 네트워크에 미리 연결하는 객체를 하나 생성한다고 가정하고, 단순히 문자만 출력하는 NetworkClient
예제 코드를 작성하며 정리해보자.
connect()
: 연결 맺기disconnect()
: 애플리케이션 종료 시 연결 끊기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("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 {
@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.dev");
return networkClient;
}
}
}
// 테스트 코드 상의 ConfigurableApplicationContext
와 관련된 라인에 대해 좀 더 궁금하다면 다음 링크를 참고해보자.
// 스프링 부트 애플리케이션에서 초기화 코드를 넣는 3가지 방법
테스트 결과는 다음과 같다.
생성자 호출, url = null
connect: null
call: null message = 초기화 연결 메시지
실행 결과를 보면 url이 모두 null이다.
객체를 생성하는 단계에는 url이 입력되지 않아 값이 없는 상태이고, 객체를 생성한 다음에 외부에서 수정자 주입을 통해 setUrl()
이 호출되어야 url이 존재하게 된다.
스프링 빈의 라이프사이클을 간단하게 생각하면 객체 생성 -> 의존관계 주입 이다.
스프링 빈은 객체를 생성 후 의존관계를 주입한 뒤에야 필요한 데이터를 사용할 준비가 완료된다. 해당 빈에서 초기화 작업들을 해주고싶다면, 이런 의존관계가 모두 주입된 다음에 호출해야 한다. 개발자 입장에서 의존관계가 모두 주입이 완료된 시점을 알고자 할 땐 어떤 방법을 써야할까?
스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해 초기화 시점을 알려주는 다양한 기능을 제공한다. 추가로 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다. 이 때문에 안전하게 종료 작업을 진행할 수 있다.
1. 스프링 컨테이너 생성
2. 스프링 빈 생성
3. 의존관계 주입
4. 초기화 콜백
5. 사용
6. 소멸전 콜백
7. 스프링 종료
📌 객체의 생성과 초기화를 분리하자.
생성자는 필수 정보(파라미터)를 받고, 메모리 할당 후 객체를 생성하는 책임을 가진다. 반면 초기화는 이렇게 생성된 값들을 활용해 기타 무거운 작업들을 수행한다. 따라서 생성자 안에서 무거운 초기화 작업을 함께 하는 것 보다는 객체 생성 부분과 초기화 부분을 명확하게 나누는 것이 유지보수 관점 및 객체지향 관점에서도 좋다.(SRP: 단일 책임 원칙)
📌 싱글톤 빈들은 스프링 컨테이너가 종료될 때 싱글톤 빈들도 함께 종료되기 때문에 스프링 컨테이너가 종료되기 직전에 소멸전 콜백이 일어난다. 생명주기가 짧은 빈들도 있는데 이 빈들은 컨테이너와 무관하게 해당 빈이 종료되기 직전에 소멸전 콜백이 일어난다.
스프링은 크게 다음 3가지 방법으로 빈 생명주기 콜백을 지원한다.
InitializingBean
, DisposableBean
)@PostConstruct
, @PreDestroy
애노테이션 지원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 {
connect();
call("초기화 연결 메시지");
}
@Override
public void destroy() throws Exception {
disconnect();
}
}
InitializingBean
: afterPropertiesSet()
메서드로 초기화를 지원DisposableBean
: destroy()
메서드로 소멸을 지원위 코드로 리팩토링 후 앞서 만들어 두었던 BeanLifeCycleTest
테스트 코드를 실행 시 출력 결과를 보면 다음과 같이 정상적인 결과가 나오게 된다.
생성자 호출, url = null
NetworkClient.afterPropertiesSet
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 초기화 연결 메시지
13:24:49.043 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing NetworkClient.destroy
close + http://hello-spring.dev
disconnect
가 정상적으로 호출되었다.다만, 초기화, 소멸 인터페이스의 단점은 다음과 같다.
이 방법은 스프링 초창기에 나온 방법들이고, 이를 대체하는 더 좋은 방법들이 있기 때문에 거의 사용되지 않는다.
public class NetworkClient {
...
// 인터페이스에서 @Override된 afterPropertiesSet(), destroy()를 제거하고
// 기존 코드에 추가
public void init() {
System.out.println("NetworkClient.init");
connect();
call("초기화 연결 메시지");
}
public void close() {
System.out.println("NetworkClient.close");
disconnect();
}
}
설정 정보에 초기화 소멸 메서드 지정
// BeanLifeCycleTest.java
@Configuration
static class LifeCycleConfig {
@Bean(initMethod = "init", destroyMethod = "close")
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hello-spring.dev");
return networkClient;
}
}
실행 결과는 다음과 같다.
생성자 호출, url = null
NetworkClient.init
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 초기화 연결 메시지
13:33:10.029 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing NetworkClient.close
close + http://hello-spring.dev
@Bean
의 destroyMethod
속성에는 추론기능이라는 특별한 기능이 있다.
라이브러리에서 대부분 close
, shutdown
이라는 이름의 종료 메서드를 사용하기 때문에 @Bean
의 해당 속성에는 default 값으로 (inferred)
(추론)으로 등록되어 있다. 이 때문에 close
, shutdown
라는 이름의 메서드를 자동으로 호출해준다.
즉, 직접 스프링 빈으로 등록하면 종료 메서드는 따로 지정해주지 않아도 잘 동작하게 된다.
만약, 추론 기능을 사용하기 싫다면 destroyMethod=""
처럼 빈 공백을 지정하면 된다.
@PostConstruct
, @PreDestroy
// NetworkClient.java
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();
}
}
// BeanLifeCycleTest.java
@Configuration
static class LifeCycleConfig {
@Bean
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hello-spring.dev");
return networkClient;
}
}
테스트 결과는 다음과 같다.
생성자 호출, url = null
NetworkClient.init
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 초기화 연결 메시지
19:40:50.269 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing NetworkClient.close
close + http://hello-spring.dev
이 방식의 특징을 정리하자면,
javax.annotation.PostConstruct
이다. 스프링에 종속적인 기술이 아닌 JSR-250이라는 자바 표준이다. 스프링이 아닌 다른 컨테이너에서도 동작한다.@Bean
의 기능을 사용해야 한다.@PostConstruct
, @PreDestroy
애노테이션을 사용하자.@Bean
의 initMethod
, destroyMethod
를 사용하자.