우리가 프로그램을 서버와 데이터베이스를 연결해서 서비스를 시작할 때 미리 연결을 해둬야하는 것들이 있다. 데이터베이스 커넥션 풀, 네트워크 소켓 처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 이 연결을 종료하는 작업이 필요하다. 이걸 객체의 초기화 콜백, 소면전 콜백 이라고 한다.
자바 코드 + 서버(스프링) + DB
서비스를 딱 시작할 때 서비스에 필요한 것들을 준비하면 안된다. 필요한 것들을 다 준비한 후에 서비스를 시작해야 오류가 발생하지 않고 제대로 사용할 수 있다.
package hello.core.lifecycle;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
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);
}
}
➡️ 생성자에서 연결과 초기화(준비) 알람을 호출한다. 객체를 생성하면서 동시에 연결도 하고 초기화 딱 해둘 준비하라고 알림까지 해준다.
package hello.core.lifecycle;
import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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://hello-spring.dev"); // Setter 로 url 주입해서 생성시에 null
return networkClient;
}
}
}
테스트 진행 과정
: 스프링 빈을 생성하고 setter 로 url 의 값을 넣어주고는 스프링 컨테이너에 보관한다. 해당 스프링 빈을 생명주기 제어기능도 가진 ConfigurableApplicationContext 를 사용해서 서비스 종료까지 테스트 했다.
➡️ 설정파일(LifeCycleConfig) 빈에서 도메인을 생성자 호출하면 생성자 안의 코드가 실행된다. 생성자 호출 시 url 값, 서비스 연결에 필요한 값 준비 시작, 초기화 완료 알람 을 실행한다.
➡️ 생성자 안의 url은 null 로 출력이 된다. 왜냐하면 LifeCycleConfig 에서 생성 후에 setter 로 주입해주기 때문이다. 또 connect() 도 아직 url 주입 전이라 null 이 출력된다. 즉 연결 준비가 안된 상태에서 서비스를 시작하게 된다. (🚨 예외 발생)
스프링 빈은 2가지 라이프 사이클이 존재한다.
객체 생성의존 관계 주입중요한 건 객체가 생성이 완료된 후에 ‼️ 의존관계가 주입된다는 점이다.
위의 코드에서 의존관계로 주입 되는 값이 필요한 초기화 단계를 의존관계가 주입되기 전에 호출해서 null 이 나왔다.
그래서 우리는 객체가 생성되고 의존관계가 다 주입이 된 후에, 서비스를 배포하기 위해 필요한 값들을 준비(초기화) 해야 한다. 우리가 의존관계가 완료된 시점을 알 수 있도록 해주는 게 바로 ✔️초기화 콜백 이다.
스프링이 콜백 메서드를 통해서 다양한 기능들을 제공한다. 또 스프링 컨테이너가 종료되기 직전에 ✔️소멸 콜백을 준다. 그래서 우리는 안전하게 서비스를 종료할 수 있다.
자바 코드와 초기화 기능
객체 초기화 라는 개념이 제대로 안 잡혀 있는 상태였어서 객체 생성이랑 왜 따로 인지 이게 왜 필요한 지 이해할 수 없었다. 그래서 딱 정리를 하자면,
자바 코드는 서비스에 사용되는 비지니스 로직들을 구현하는 일을 한다. 그리고 초기화는 객체와 그 안의 값들과 관련있는 것이 아니라! 그 외에 우리가 서비스를 배포할 때 준비해둬야 하는 여러가지 설정값 (DB 연결 시, 네트워크 연결 시, 파일 열기 등) 을 세팅 해두는 것이다.
스프링 띄우기-스프링 컨테이너 생성-스프링 빈 생성-의존관계 주입-초기화 콜백-사용-소멸전 콜백-스프링 종료
스프링에서는 쉽게 콜백 메서드를 사용할 수 있도록 인터페이스를 제공한다. 초기화 콜백에는 InitializingBean, 종료 콜백에는 DisposableBean 을 사용한다. 이 방식은 아주 예전 방식으로 지금은 잘 사용하지 않는다.
package hello.core.lifecycle;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
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.init");
connect();
call("초기화 연결 메세지");
}
@Override // ✅ 추가
public void destroy() throws Exception {
System.out.println("NetworkClient.close");
disconnect();
}
}
➡️ destroy()는 초기화 콜백 메서드, afterPropertiesSet()는 종료 콜백 메서드다. 각 인터페이스가 제공하는 메서드를 오버라이딩 했다.
➡️ 이제 테스트를 돌려보면 스프링이 제공하는 두 인터페이스 덕분에 초기화 시점, 종료 시점에 자동으로 콜백 메서드가 실행된다. 이제 객체 생성하고 의존관계 주입(url) 후에 초기화가 실행되면서 connect()와 disconnect() 의 url 에는 해당 주소가 출력된다.
이 로직의 단점🚨 은 스프링이라는 프레임워크에 의존한다는 것이다. 다른 프레임워크로 변경할 수 없다.
package hello.core.lifecycle;
import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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://hello-spring.dev");
return networkClient;
}
}
}
...
public void init() {
System.out.println("NetworkClient.init");
connect();
call("초기화 연결 메세지");
}
public void close() {
System.out.println("NetworkClient.close");
disconnect();
}
➡️ @Bean 에 초기화, 소멸 메서드 이름을 저렇게 설정 정보로 적어주면 스프링이 이 이름을 보고 매칭해서 실행한다.
❓ 이건 스프링 의존적이지 않은가?
설정정보로 매칭하는 기능은 스프링 뿐만 아니라 대부분의 프레임워크에서 제공하는 기본적인 기능이라 스프링에 의존적이지 않다.
✅ @Bean 의 소멸 메서드 속성에는 특별한 기능이 있다. 기본적으로 inferred(추론) 으로 등록되어있어서 소멸 관련 이름인 close, shutdown 라는 이름의 메서드를 자동으로 호출해준다. 그래서 종료 메서드의 경우에는 따로 설정정보를 적어주지 않아도 잘 작동 한다.
이 두개의 애노테이션을 초기화, 소멸 메서드에 붙이면 가장 편리하게 초기화와 종료를 실행할 수 있다. 실무에서 사용하는 방법이다.
@PostConstruct
public void init() {
System.out.println("NetworkClient.init");
connect();
call("초기화 연결 메세지");
}
@PreDestroy
public void close() {
System.out.println("NetworkClient.close");
disconnect();
}
➡️ javax.annotation.PostConstruct 로 스프링이 아니라 자바에서 제공하는 기능이라 스프링이 아닌 다른 프레임워크에서도 동작한다.
➡️ 단점은 외부 라이브러리에는 적용하지 못한다는 점이다. 외부 라이브러리를 초기화, 종료 해야할 때는 @Bean의 기능을 사용하자.