스프링 빈도 객체이기 때문에 초기화가 필요할 것이다.
그리고 우리는 이 초기화를 보통 생성자에서 처리하게 된다.
필드같은 간단한 초기화는 괜찮지만, 만약 데이터베이스의 커넥션풀을 미리 셋팅하는 것과 같은 무거운 작업들은 ?
소켓 통신을 위해 소켓을 미리 열어두는 초기화 작업들은 ?
객체의 생성과 초기화는 분리해야한다.
객체의 생성과 객체의 초기화는 다른 책임이다. SRP 의 원칙에 따라 이 두 로직은 분리되는 것이 좋다. 물론 간단한 필드 한두개를 초기화하는 데에 생성자를 쓰지 않고 따로 하는 것은 비효율적일 수 있지만, 위의 예인 커넥션풀처럼 무거운 초기화 작업들은 분리하는 것이 좋다.
오늘은 이 초기화라는 작업을 스프링에서 어떻게 분리하여 처리하게 도와주는지 알아본다.
스프링 컨테이너를 사용해 빈을 등록해 사용할 때에는 다음과 같은 과정이 일어난다.
- 스프링 컨테이너가 생성된다.
- 스프링 빈이 등록된다.
- @Configuration 의 @Bean 에너테이션을 이용한 수동 등록
- @ComponentScan 을 이용한 자동 등록
- 스프링 빈이 모두 등록되면 빈들의 의존관계를 주입한다.
- 의존관계 주입이 끝나면 스프링은 콜백을 주고, 그때 빈들의 초기화를 한다.
- 셋팅된 빈을 가지고 에플리케이션을 돌린다.
- 스프링 컨테이너는 스프링 컨테이너가 종료되기 전 소멸 콜백을 준다.
- 스프링 컨테이너가 정지된다.
물론 생성자 주입이나 수동 주입같은 경우에는 빈의 등록과 의존 관계 주입이 같이 일어날 수 있다.
의존 관계의 빈을 초기화 하는 것은 빈 등록과 의존 관계 주입이 다 끝난 후 일어나야 하는 일이다.
의존 관계가 다 끝나지 않은 채로 그 의존 빈의 초기화작업을 진행하려 하면 당연히 null 을 초기화하는 행위와 같다.
따라서 스프링은 이 빈 등록과 의존성 주입이 모두 끝나고 초기화를 하는 콜백 기능을 제공한다.
콜백
- 초기화 콜백 : 빈이 생성되고, 의존 관계 주입이 끝났나 초기화를 진행해도 될 때 호출되는 콜백
- 소멸전 콜백 : 스프링 컨테이너가 종료되어 소멸되거나, 생명주기가 끝나는 빈들이 소멸되기 직전에 호출되는 콜백
스프링에서 제공하는 생명주기 콜백 방법은 3가지가 있다.
InitializingBean, DisposableBean
이 인터페이스들을 까보자.
public interface InitializingBean {
void afterPropertiesSet() throws Exception;
}
public interface DisposableBean {
void destroy() throws Exception;
}
유겐 휠러라는 사람이 2003 년에 만든 메서드라 적혀있다. (굉장히 오래되었다. 사실 3가지 방법중 아래로 내려갈수록 요즘 더 많이 쓰는 좋은(?) 방법이다.)
초기화 콜백 메서드와 소멸전 콜백 메서드가 있으며, 사용하고 싶은 클래스에 이 인터페이스를 구현하면 된다.
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("url = " + url + " message = " + message);
}
public void disconnect() {
System.out.println("close = " + url);
}
@Override
public void afterPropertiesSet() throws Exception {
connect();
}
@Override
public void destroy() throws Exception {
disconnect();
}
}
예시 코드이다.
객체를 생성하고, 커넥션을 초기화하여 만드는 connect() 를 초기화 콜백 메서드(afterPropertiesSet)에 등록시켰다.
객체가 소멸하기전, 소멸전 콜백 메서드(destroy)에 disconnect() 를 등록시켜 연결을 끊게 한다.
@DisplayName("빈 생명주기 확인")
@Test
void lifeCycle() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleBean.class);
NetworkClient ne = ac.getBean(NetworkClient.class);
ne.call("호출");
ac.close();
}
@Configuration
static class LifeCycleBean {
@Bean
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://springSite.com");
return networkClient;
}
}
// 위 테스트의 결과
생성자 호출, url = null
connect = http://springSite.com
url = http://springSite.com message = 호출
close = http://springSite.com
코드를 보면 알겠지만, 빈을 등록하는 과정에서 따로 connect() 나 disconnect() 를 호출하지 않았다.
그런데 afterPropertiesSet 에 등록된 메서드가 빈 생성 후 자동으로 실행된다. (url 값이 null 이었다가 들어가 있는 것을 보면 빈 등록 후임을 알 수 있다.)
또, close() 로 스프링 컨테이너가 소멸되어 빈도 소멸시킬때 destroy() 에 등록된 disconnect() 가 호출된 것을 볼 수 있다.
오래된 만큼 단점이 몇가지 존재한다.
- 스프링 전용 인터페이스다. 코드가 자바가 아닌 스프링에 의존하게 된다.
- 초기화, 소멸 메서드의 이름을 변경할 수 없다.
- 외부 라이브러리를 사용할 때, 그 라이브러리의 초기화화 소멸 콜백 메서드를 제공할 방법이 없다. (외부 라이브러리에 이 인터페이스를 구현할 수 없다.)
그래서 요즘에는 거의 쓰지 않는다.
@Bean 등록시 옵션주는 방법
@Bean 에너테이션에는 initMethod, destroyMethod 라는 옵션을 줄 수 있다.
값으로 각각 초기화 메서드 이름, 소멸시 메서드 이름을 지정해주면 된다.
@DisplayName("빈 생명주기 확인")
@Test
void lifeCycle() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleBean.class);
NetworkClient ne = ac.getBean(NetworkClient.class);
ne.call("호출");
ac.close();
}
@Configuration
static class LifeCycleBean {
@Bean(initMethod = "connect", destroyMethod = "disconnect")
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://springSite.com");
return networkClient;
}
}
이 방법의 장점은 아래와 같다.
- 메서드의 이름을 자유롭게 지정할 수 있다. (보통 init 처럼 관례적으로 쓰이는 단어들이 있긴하다.)
- 스프링 빈이 스프링 코드를 의존하지 않는다.
- 비즈니스 코드를 만지지 않고 설정정보를 사용하기 때문에 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다.
종료 메서드 추론
여기서 특이한 점이 있는데, destroyMethod 옵션의 기본값을 보자.
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean {
...
String destroyMethod() default AbstractBeanDefinition.INFER_METHOD;
}
public static final String INFER_METHOD = "(inferred)";
이런 값이 초기화되어 있다.
라이브러리들은 종료하는 메서드를 보통 close(), shutdown() 등의 이름을 관례적으로 사용하는데,
이 inferred 는 추론이란 뜻으로 말 그대로 이 close, shutdown 같은 이름을 추론하는 것이다.
종료할 메서드의 이름을 close, shutdown 으로 만들고, 굳이 destroyMethod 옵션을 주지 않아도 알아서 추론하여 실행해준다.
추론 기능을 사용하기 싫다면 초기화 값을 명시적으로 destroyMethod="" 이렇게 하면 된다.
public class NetworkClient {
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("url = " + url + " message = " + message);
}
public void close() { // 이름을 close 로 바꿈
System.out.println("close = " + url);
}
}
@DisplayName("빈 생명주기 확인")
@Test
void lifeCycle() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleBean.class);
NetworkClient ne = ac.getBean(NetworkClient.class);
ne.call("호출");
ac.close();
}
@Configuration
static class LifeCycleBean {
@Bean(initMethod = "connect") // destroyMethod 는 생략하여 기본값 사용.
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://springSite.com");
return networkClient;
}
}
// 위 테스트의 결과
생성자 호출, url = null
connect = http://springSite.com
url = http://springSite.com message = 호출
close = http://springSite.com
@PostConstruct, @PreDestory
세번째 방법이다. 가장 최근에 나왔으며 가장 많이 쓰인다.
초기화, 종료 메서드 위에 붙여주기만 하면 된다.
public class NetworkClient {
private String url;
public NetworkClient() {
System.out.println("생성자 호출, url = " + url);
}
public void setUrl(String url) {
this.url = url;
}
@PostConstruct
public void connect() {
System.out.println("connect = " + url);
}
public void call(String message) {
System.out.println("url = " + url + " message = " + message);
}
@PreDestroy
public void close() {
System.out.println("close = " + url);
}
}
이 방법은 스프링에서도 권장하므로 이 방법을 쓰자.
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
위에서 보듯 javax 패키지에 있는 자바 표준 기술이다.
따라서 스프링에 종속적이지 않고, 스프링이 아닌 다른 컨테이너에서도 동작한다.
하지만 유일한 단점으로, 코드에 작성하는 것이다 보니 외부 라이브러리에는 적용하지 못한다는 점이다.
결론
- 평소에는 @PostConstruct, @PreDestroy 를 쓰자.
- 외부 라이브러리처럼 코드를 고칠 수 없는 상황에는 @Bean 에 initMethod, destroyMethod 옵션을 쓰자.
'spring' 카테고리의 다른 글
spring interceptor (0) | 2021.10.30 |
---|---|
빈 스코프 (0) | 2021.03.21 |
자동 주입시 빈이 2개 이상일 때 문제 해결 (0) | 2021.03.19 |
생성자 자동 주입의 장점, @RequiredArgsConstructor (0) | 2021.03.19 |
@Autowired (0) | 2021.03.18 |