스프링 빈에는 생명주기 Life Cycle라는 것이 존재하는데, 생명주기란 말 그대로 스프링 빈이 생성되고 종료되는 일련의 과정을 말한다. 이 과정 동안 초기화 로직이나 마무리 로직을 개발자가 임의로 삽입할 수 있는데, 이것을 빈 생명주기 콜백이라고 한다.
빈의 생명주기는 아래와 같이 진행된다.
1. 스프링 컨테이너 생성 2. 스프링 빈 생성 3. 의존 관계 주입 4. 초기화 콜백(Init Callback) 5. 빈 사용 6. 소멸 콜백(Destroy Callback) 7. 스프링 종료
여기서 우리가 주목할 것은 초기화 콜백과 소멸 콜백이다.
의존 관계 주입이 완료되고 빈을 사용하기 전에 어떤 메소드를 호출하여 객체를 초기화 한다거나, 빈이 소멸하기 전에 무언가를 해주고 싶은 것이다.
예를 들어, 외부 네트워크에 객체를 연결하는 상황을 가정해보자.
의존 관계의 주입이 끝나고 이 빈을 사용하기 전에, 어떤 외부 네트워크에 연결할지 미리 설정해두고 싶다. 이럴 때, 초기화 콜백에 자동으로 이 기능을 하는 메소드를 호출한다.
또, 이 빈의 사용이 끝나고 소멸하기 직전, 보안 상의 이유 때문에 네트워크 연결을 안전하게 끊고 싶다. 이때 소멸 콜백을 활용하여 안전하게 네트워크 연결을 끊도록 설정하는 것이다.
이제 어느 정도는 왜 빈 생명주기 콜백이 필요한지, 어떤 때에 쓰는지 알았을 테니, 스프링이 빈 생명주기 콜백을 어떻게 지원하는지 알아보자.
스프링은 크게 3가지 방법으로 빈 생명주기 콜백을 지원한다.
initializingBean, DisposableBean 인터페이스
설정 정보에 초기화 메서드, 종료 메서드 지정
@PostConstruct, @PreDestroy 애노테이션
initializingBean, DisposableBean 인터페이스
스프링 초창기에 나온 방법으로 최근에는 사용하지 않는다.
말 그대로 initializingBean*과 *DisposableBean 인터페이스를 구현implements한다.
public class NetworkClient {
...
@Override
public void afterPropertiesSet() throws Exception {
connect();
}
@Override
public void destroy() throws Exception {
disConnect();
}
}
initializingBean 인터페이스는 afterPropertiesSet()로 초기화를, DisposableBean 인터페이스는 destroy()로 소멸을 지원한다.
단점
인터페이스가 스프링 전용 인터페이스이므로, 이를 적용한 코드는 스프링에 의존적이게 된다.
초기화, 소멸 메서드의 이름을 변경할 수 없다.
수정할 수 없는 외부 라이브러리에는 사용하지 못 한다.
💡 외부 라이브러리에는 사용하지 못 한다?
이게 정확히 무슨 말인지 와닿지 않을 수 있다. 예시 코드를 하나 보자.
// 외부 라이브러리 코드 (수정 불가)
public class ExternalLib {
public void startConnection() {
System.out.println("외부 라이브러리: 연결 시작");
// ... 초기화 로직 ...
}
public void stopConnection() {
System.out.println("외부 라이브러리: 연결 종료");
// ... 자원 정리 로직 ...
}
}
이렇게 내가 수정할 수 없는 외부 라이브러리를 스프링 빈에 등록하여 사용한다고 하자. 하지만 이 라이브러리는 기본적으로 라이브러리 내부에 정의된 초기화 메서드와 종료 메서드를 사용해야 한다.
하지만 인터페이스를 사용하는 경우에는 startConnection()와 stopConnection()을 콜백 메서드로 등록해줄 방법이 없다. 결국, 내가 수동으로 호출해주어야 하는 불상사가 생긴다.
빈 등록 초기화, 소멸 메서드 지정
설정 정보에 @Bean(initMethod = "init", destroyMethod = "close") 이런 식으로 지정을 해줄 수 있다.
public class NetworkClient {
...
public void init() {
connect();
}
public void close() {
disConnect();
}
}
이렇게 메서드를 정의한 후,
@Configuration
static class AppConfig {
@Bean(initMethod = "init", destroyMethod = "close")
public NetworkClient networkClient() {
return networkClient();
}
}
설정 정보에 이렇게 수동으로 등록해줄 수 있다.
장점
메서드 이름을 자유롭게 지정할 수 있다.
스프링 빈이 스프링 코드에 의존하지 않아도 된다.
스프링 빈 코드(여기서는 NetworkClient) 자체에 스프링에 의존적인 코드는 존재하지 않는다.
외부 라이브러리에도 적용할 수 있다.
사용자가 메서드를 정의한 후, 설정 정보에만 호출되게끔 등록하면 되므로 외부 라이브러리에도 적용이 가능하다.
💡외부 라이브러리에도 적용할 수 있다?
위의 코드를 다시 한 번 가져오자.
// 외부 라이브러리 코드 (수정 불가)
public class ExternalLib {
public void startConnection() {
System.out.println("외부 라이브러리: 연결 시작");
// ... 초기화 로직 ...
}
public void stopConnection() {
System.out.println("외부 라이브러리: 연결 종료");
// ... 자원 정리 로직 ...
}
}
인터페이스를 사용하는 경우에는, 위 라이브러리를 수정할 수 없으므로 콜백을 사용하지 못했지만, 이 방법의 경우에는 이렇게 설정 파일에 메서드 이름만 등록하면 사용할 수 있다.
@Configuration
public class AppConfig {
@Bean(initMethod = "startConnection", destroyMethod = "stopConnection")
public ExternalLib externalLibBean() {
return new ExternalLib();
}
}
종료 메서드 추론
소멸 콜백에 사용되는 종료 메서드는 일반적으로 clsoe(), shutdown()이라는 이름으로 많이 사용한다. 그렇기에 이런 이름의 메서드는 스프링이 자동으로 소멸 콜백으로 등록한다.
destroyMethod의 기본값이 (inferred)로 등록되어 있기 때문이다.
즉, 위의 예제와 같은 경우에는
@Configuration
static class AppConfig {
@Bean(initMethod = "init")
public NetworkClient networkClient() {
return networkClient();
}
}
이렇게 해주어도, 종료 메서드가 작동한다.
@PostConstruct, @PreDestroy 애노테이션
이 방법은 초기화 메서드, 종료 메서드로 사용하고자 하는 메서드 위에 이 애노테이션을 붙여주기만 하면 된다.
public class NetworkClient {
...
@PostConstruct
public void init() {
connect();
}
@PreDestroy
public void close() {
disConnect();
}
}
이렇게만 하면 매우 간단하게 초기화와 종료를 실행할 수 있다.
장점
javax.annotation.PostConstruct이라는 패키지에 등록된 자바 표준 기술이다. 즉, 스프링에 종속적인 기술이 아니다.
컴포넌트 스캔과 같이 간편하게 쓰기 좋다.
단점
외부 라이브러리에는 적용할 수 없다.
이유는 인터페이스를 사용할 때와 동일하다. 외부 라이브러리의 코드를 수정할 수 없으므로, 애노테이션을 붙일 수 없는 것은 당연하다.
이 경우 @Bean(initMethod = "...", destroyMethod = "...") 방식을 사용하자.