실무 개발을 하다보면 DBCP(데이터베이스 커넥션 풀)를 생성하거나, 소켓을 초기화 하는 등 에플리케이션의 시작 시점에 작업을 미리해두고, 마찬가지로 종료 시점에 해당 연결을 끊는 작업을 하는 경우가 많다. 이번 포스팅에서는 스프링이 어떤 방식으로 어플리케이션의 시작 시점과 종료 시점에 작업들을 처리하는 지에 대해 알아보자.
이 포스팅은 김영한님의 '스프링 핵심 원리 - 기본편' 강의에 의존하고 있습니다.
빈 생명주기와 콜백 메서드
public class NetworkClient {
private String url;
public void setUrl(String url) {
this.url = url;
}
public NetworkClient() {
System.out.println("create Client(url=" + url + ")");
connect();
call("init message");
}
public void connect() {
System.out.println("connect: " + url);
}
public void call(String message) {
System.out.println("call " + url + " : " + message);
}
public void disconnect() {
System.out.println("disconnect: " + url);
}
}
다음과 같이 네트워크 통신을 하는 클라이언트 객체가 있다고 하자. 클라이언트를 생성하면 클라이언트 생성 메세지를 출력한후, 연결 및 초기화 메세지를 보낸다.
만약 이 클라이언트를 생성하여 url을 설정하면 어떤 결과가 발생할까?
public class LifeCycleExample {
public static void main(String[] args) {
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
NetworkClient client = ac.getBean(NetworkClient.class);
ac.close()
}
}
// AppConfig.class
@Configuration
public class AppConfig {
@Bean
public NetworkClient networkClient() {
NetworkClient client = new NetworkClient();
client.setUrl("http://hello.dev");
return client;
}
}
main 메서드를 실행하면 다음과 같은 결과가 발생한다.
create client(url=null)
connect : null
call null : init message
url이 모두 null로 출력되게 된다. 왜냐하면 연결을 하는 초기화 작업이 url 의존관계를 주입하는 시점보다 먼저 처리되었기 때문이다.
스프링 빈의 이벤트 생명주기
빈을 생성하는 시점에는 다음과 같은 생명주기를 가진다.
- 객체 생성 → 의존관계 주입
이 시점이 끝난 이후 초기화 작업이 진행되어야 하고, 그 이후에 비로소 해당 스프링 빈을 사용할 수 있다. 그후, 소멸 직전에 소멸전 작업이 처리되고, 스프링 빈이 소멸되게 된다. 이 과정을 정리하면 다음과 같다.
- 스프링 빈 생성 → 의존관계 주입 → 초기화 작업 → 사용 → 소멸전 작업 → 스프링 빈 종료
이때 초기화 작업을 초기화 콜백이라고 하고, 소멸전 작업을 소멸전 콜백이라고 한다.
지금부터 초기화 콜백과 소멸전 콜백 구현을 하는 방법에 대해 알아보자.
방법1 : 인터페이스(InitializingBean, DisposableBean) 사용
InitializingBean은 초기화 콜백 메서드를 제공하고, DisposableBean은 소멸전 콜백 메서드를 제공한다.
public class NetworkClient implements InitializingBean, DisposableBean {
private String url;
public void setUrl(String url) {
this.url = url;
}
public NetworkClient() {
System.out.println("create Client(url=" + url + ")");
}
public void connect() {
System.out.println("connect: " + url);
}
public void call(String message) {
System.out.println("call " + url + " : " + message);
}
public void disconnect() {
System.out.println("disconnect: " + url);
}
@Override
public void afterPropertiesSet() throws Exception {
connect();
call("init message");
}
@Override
public void destroy() throws Exception {
disconnect();
}
}
afterPropertiesSet() 메서드는 객체가 생성되고 의존관계 주입이 완료된 시점에 호출되어 초기화 작업을 처리하고, destroy() 메서드는 객체가 소멸되지 전 시점에 호출되어 소멸전 작업을 처리한다.
다시 LifeCycleExample.main 메서드를 실행해 보면
create client(url=null)
connect : http://hello.dev
call http://hello.dev : init message
connect() / call() 메서드 실행 시 정상적으로 url을 출력하는 것을 볼 수 있다.
이때 사용하는 인터페이스(InitializingBean, DisposableBean)는 스프링 전용 인터페이스로, 코드가 스프링에 의존한다는 문제가 발생한다. 스프링에 의존하는 코드는 여러 문제를 일으키는데 다음과 같다.
- 초기화, 소멸 메서드의 이름을 변경할 수 없다.
- 내가 코드를 고칠 수 업슨ㄴ 외부 라이브러리에 적용할 수 없다.
빈 등록 초기화, 소멸 메서드 지정
설정 정보에 초기화, 소멸 메서드를 지정할 수 있다.
public class NetworkClient {
private String url;
public void setUrl(String url) {
this.url = url;
}
public NetworkClient() {
System.out.println("create Client(url=" + url + ")");
}
public void connect() {
System.out.println("connect: " + url);
}
public void call(String message) {
System.out.println("call " + url + " : " + message);
}
public void disconnect() {
System.out.println("disconnect: " + url);
}
public void init() throws Exception {
connect();
call("init message");
}
public void destroy() throws Exception {
disconnect();
}
}
public class LifeCycleExample {
public static void main(String[] args) {
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
NetworkClient client = ac.getBean(NetworkClient.class);
ac.close()
}
}
// AppConfig.class
@Configuration
public class AppConfig {
@Bean(initMethod = "init", destroyMethod = "close")
public NetworkClient networkClient() {
NetworkClient client = new NetworkClient();
client.setUrl("http://hello.dev");
return client;
}
}
다음과 같이 @Bean(initMethod = "init", destroyMethod = "destroy") 애노테이션에 초기화, 소멸 메서드를 지정하면 된다.
설정 정보에 초기화, 소멸 메서드를 지정하는 방법은 메서드의 이름을 자유롭게 지정할 수 있고, 스프링 빈이 스프링 코드에 의존하지 않는다. 또한 코드가 아니라 설정 정보를 사용함으로 외부라이러리에도 초기화, 종료 메서드를 지정할 수 있다.
(종료 메서드 추론)
@Bean 애노테이션의 destroyMethod 속성은 종료 메서드 추론이라는 매우 특별한 기능을 제공한다. destroyMethod의 기본값은 (inferred) 로 등록되어 있는데 이는 close, shutdown이 이름인 메서드를 자동으로 호출하게 된다. 즉, 스프링 빈의 종료 메서드의 이름이 close 또는 shutdown이면 destroyMethod 속성을 지정해주지 않아도, 자동으로 찾아서 호출되게 된다.
방법3 : @PostConstruct, @PreDestroy
실제로 가장 많이 사용하는 방법으로 초기화, 종료메서드에 @PostConstruct, @PreDestroy 애노테이션을 지정함으로써 초기화, 종료 메서드를 설정하는 방법이다. 코드로 보면 다음과 같다.
public class NetworkClient {
private String url;
public void setUrl(String url) {
this.url = url;
}
public NetworkClient() {
System.out.println("create Client(url=" + url + ")");
}
public void connect() {
System.out.println("connect: " + url);
}
public void call(String message) {
System.out.println("call " + url + " : " + message);
}
public void disconnect() {
System.out.println("disconnect: " + url);
}
@PostConstruct
public void init() throws Exception {
connect();
call("init message");
}
@PreDestroy
public void destroy() throws Exception {
disconnect();
}
}
@PostConstruct, @PreDestroy 애노테이션은 자바 표준으로 스프링에 의존하지 않는다. 또한 애노테이션을 지정하는 방식으로 간편하고, 컴포넌트 스캔 방식과도 잘 어울린다. 단, 외부 라이브러리를 초기화 종료 하는 경우에는 사용할 수 없다.
최신 스프링은 이 방식을 가장 권장하고 있다.
'WEB' 카테고리의 다른 글
JWT(Json Web Token)의 구조 (0) | 2023.05.18 |
---|---|
[WEB] 빈 스코프 1 - 프로토타입 스코프 (Prototype Scope) (0) | 2022.05.20 |
[WEB] 스프링 의존관계 자동 주입 (0) | 2022.05.03 |
[WEB] 스프링 컴포넌트 스캔 (0) | 2022.04.28 |
[WEB] 스프링 컨테이너와 싱글톤 패턴 (0) | 2022.04.22 |