본 글은 인프런 김영한님의 스프링 완전 정복 로드맵을 기반으로 정리했습니다.
스프링 컨테이너는 빈 객체의 라이프사이클을 관리해준다. 스프링 컨테이너와 빈은 다음과 같은 생명주기를 가진다.
스프링 컨테이너는 빈 객체를 생성하고 의존 주입을 통해 의존을 설정한다. 의존 자동 주입을 사용한다면 이 시점에 의존 주입이 수행된다.
의존주입이 모두 완료된 다음에 초기화 작업이 일어난다. 이 때 초기화 작업을 수행할 수 있도록 콜백 메서드를 등록할 수 있다. 마찬가지로 소멸 작업도 콜백 메서드를 통해 등록할 수 있다.
생성자에서도 초기화 작업을 진행할 수 있긴하다. 그러나, 생성자는 객체를 생성하고 객체의 의존관계를 설정하는 책임에 집중해야한다.
초기화 콜백에서는 이렇게 생성된 값들을 이용해 보다 복잡한 처리를 하는데 사용한다. 초기화 작업이 매우 간단한 경우에는 생성자에서 초기화 작업을 진행해도 되지만 복잡한 경우라면 메서드를 나누는 것이 유지보수 하기에 편한 경우가 많다.
초기화와 소멸 콜백을 등록하는 좋은 예는 데이터베이스 커넥션 풀이다. 초기화 과정에서 데이터베이스 커넥션들을 생성한다. 커넥션 풀이 사용되는 동안만 커넥션들을 유지하고 소멸 콜백을 통해 객체가 소멸되기 전 데이터베이스 커넥션들을 모두 끊어야 한다.
콜백을 등록하는 방법은 3가지가 있다.
빈 콜백을 등록하기 위해 인터페이스를 구현하는 방법이 있다. 두 인터페이스는 다음과 같다.
public interface InitializingBean {
void afterPropertiesSet() throws Exception;
}
public interface DisposableBean {
void destroy() throws Exception;
}
두 인터페이스를 구현한 클래스를 만들어보자.
public class NetworkClient implements InitializingBean, DisposableBean {
private String host;
public void setHost(String host) {
this.host = host;
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("Client.afterPropertiesSet() 실행");
}
public void send() {
System.out.println("Client.send() to " + host);
}
@Override
public void destroy() throws Exception {
System.out.println("Client.destroy() 실행");
}
}
클래스를 빈으로 등록한 뒤 메인 메서드를 통해 스프링 컨테이너를 띄웠다.
@Configuration
public class AppCtx {
@Bean
public NetworkClient networkClient() {
NetworkClient client = new NetworkClient();
client.setHost("host");
return client;
}
}
public class Main {
public static void main(String[] args) {
AbstractApplicationContext ctx =
new AnnotationConfigApplicationContext(AppCtx.class);
NetworkClient client = ctx.getBean(NetworkClient.class);
client.send();
ctx.close();
}
}
실행결과는 다음과 같다. 두 메서드를 직접 호출해주지 않아도 빈 객체의 의존주입이 모두 완료된 후, 컨테이너 종료 직전 각각 초기화, 소멸 콜백을 스프링이 호출해준다.
초기화, 소멸 인터페이스 구현은 다음과 같은 단점 때문에 잘 사용하지 않는다.
초기화, 소멸 메서드의 이름을 변경할 수 없다.
외부 라이브러리에 적용할 수 없다.
모든 클래스가 인터페이스를 구현할 수 있는 것은 아니다. 자신이 구현한 클래스가 아닌 외부 라이브러리의 경우 소스코드를 받아서 고치지 않는 이상 인터페이스를 구현하도록 수정할 수 없다.
이 경우 스프링 설정에서 직접 초기화, 소멸 메서드를 지정할 수 있다. 장점은 다음과 같다.
콜백 메서드 이름을 자유롭게 설정 가능
스프링 의존도 낮춤 (인터페이스 구현 안해도 됨)
외부 라이브러리에 적용 가능
코드로 알아보자.
public class NetworkClient2 {
private String host;
public void setHost(String host) {
this.host = host;
}
public void init() {
System.out.println("Client2.init() 실행");
}
public void send() {
System.out.println("Client2.send() 실행");
}
public void close() {
System.out.println("Client2.close() 실행");
}
}
@Configuration
public class AppCtx {
@Bean(initMethod = "init", destroyMethod = "close")
public NetworkClient2 networkClient() {
NetworkClient2 client = new NetworkClient2();
client.setHost("host");
return client;
}
}
빈을 등록하는 애노테이션의 속성을 통해 초기화와 소멸 콜백을 지정해줄 수 있다. 참고로, destroyMethod
속성의 경우 대부분의 라이브러리가 close, shutdown 이라는 이름의 종료 메서드를 사용하기 때문에 아무 속성도 주지 않으면 자동으로 호출해준다. 이를 추론 기능이라고 하는데 이 기능을 동작하기 않게 하려면 destroyMethod=""
와 같이 빈 공백을 지정해주면 된다.
메인 메서드에서 컨테이너를 띄운 결과는 다음과 같다. 인터페이스와 크게 다르지 않다.
public class Main {
public static void main(String[] args) {
AbstractApplicationContext ctx =
new AnnotationConfigApplicationContext(AppCtx.class);
NetworkClient2 client = ctx.getBean(NetworkClient2.class);
client.send();
ctx.close();
}
}
마지막으로 가장 많이 사용되는 애노테이션 기반 콜백 등록 방법이다. 빈 등록과 메인 메서드는 생략하고 클래스 정의 부분만 보자.
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
public class NetworkClient2 {
private String host;
public void setHost(String host) {
this.host = host;
}
@PostConstruct
public void init() {
System.out.println("Client2.init() 실행");
}
public void send() {
System.out.println("Client2.send() 실행");
}
@PreDestroy
public void close() {
System.out.println("Client2.close() 실행");
}
}
이 방법을 권장하는 이유는 다음과 같다.
편리함 (애노테이션만 달면 됨)
스프링 의존도 낮춤 (javax 표준 애노테이션 사용)
컴포넌트 스캔과 함께 사용 가능
다만, 외부 라이브러리에는 적용하지 못한다. 외부 라이브러리의 경우에는 커스텀 메서드 방법을 이용하도록 하자.