본 시리즈는 우아한형제들 개발 팀장이신
김영한
님의스프링 핵심 원리 - 기본편
강의를 들으며 개인적으로 정리한 내용을 담고 있습니다. 제가 들은 강의는 인프런에 등록되어 있습니다. 모든 다이어그램을 포함한 사진의 출처는 위 강의의 강의록임을 밝힙니다. 개인적으로 정리한 내용이기 때문에 글 내용에 오류가 있을 수 있으며 이에 대한 피드백은 댓글로 부탁드립니다.
위와 같은 작업을 할 때 초기화, 종료 작업이 필요하다. 스프링에선 이 작업을 어떻게 하는지 알아보자.
서버가 뜨기 전에 외부 네트워크에 미리 연결하는 객체를 생성한다고 상정하자.
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 msg) {
System.out.println("call: " + url + " msg = " + msg);
}
//서비스 종료 시 호출
public void disconnect() {
System.out.println("close " + url);
}
}
NetworkClient
를 테스트하는 클래스를 작성하자.
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");
return networkClient;
}
}
}
실행 결과는 다음과 같다.
생성자 호출, url = null
connect: null
call: null msg = 초기화 연결 메시지
setUrl
을 호출하기 전에 생성자를 호출했고, 생성자 안에서 connect()
가 호출될테니 url
을 세팅하기 전이라 null
이 나오는 것이다.스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메소드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공한다. 그리고 스프링은 스프링 컨테이너가 종료되기 직전 소멸 콜백을 준다. 이를 이용하면 종료 작업을 할 수 있다.
스프링 빈의 이벤트 라이프사이클
스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸 전 콜백 → 스프링 종료
- 초기화 콜백: 스프링 빈 생성, 의존관계 주입이 완료되면 호출된다.
- 소멸 전 콜백: 빈이 소멸되기 직전에 호출된다.
참고: 객체의 생성과 초기화를 구분하자
생성자는 필수 정보를 parameter로 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다. 초기화는 이렇게 생성된 값을 활용해서 외부 커넥션을 연결하는 등 무거운 동작을 수행한다.
따라서 생성자 안에서 무거운 초기화 작업을 하는 것보다는 객체를 생성하는 부분과 초기화하는 부분을 명확히 나누는 것이 유지보수 관점에서 좋다. 물론 초기화 작업이 내부 값들만 약간 변경하는 단순한 경우에는 한 번에 처리하는 것도 괜찮다.
스프링은 크게 세 가지 방법으로 빈 생명주기 콜백을 지원한다.
InitializingBean
, DisposableBean
)@PostConstruct
, @PreDestroy
애노테이션InitializingBean
, DisposableBean
public class NetworkClient implements InitializingBean, DisposableBean {
private String url;
public NetworkClient() {
System.out.println("생성자 호출, url = " + url);
}
...
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("NetworkClient.afterPropertiesSet");
connect();
call("초기화 연결 메시지");
}
@Override
public void destroy() throws Exception {
System.out.println("NetworkClient.destroy");
disconnect();
}
}
InitializingBean
을 구현하면 afterPropertiesSet()
을 통해 초기화할 내용을 지정할 수 있다.DisposableBean
을 구현하면 destroy()
를 통해 소멸되기 직전 수행할 내용을 지정할 수 있다.생성자 호출, url = null
NetworkClient.afterPropertiesSet
connect: http://hello-spring.dev
call: http://hello-spring.dev msg = 초기화 연결 메시지
01:22:25.436 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing
NetworkClient.destroy
close http://hello-spring.dev
초기화, 소멸 인터페이스의 단점은
이는 스프링 초창기에 나온 방법이고, 지금은 잘 사용하지 않는다.
설정 정보에 @Bean(initMethod="init", destroyMethod="close")
처럼 초기화, 소멸 메소드를 지정할 수 있다.
public class NetworkClient {
...
public void init() {
System.out.println("NetworkClient.afterPropertiesSet");
connect();
call("초기화 연결 메시지");
}
public void close() {
System.out.println("NetworkClient.destroy");
disconnect();
}
}
설정 정보를 다음과 같이 수정한다.
public class BeanLifeCycleTest {
...
@Configuration
static class LifeCycleConfig {
@Bean(initMethod = "init", destroyMethod = "close") // 요기에 기입한다.
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hello-spring.dev");
return networkClient;
}
}
}
종료 메소드 추론
@Bean
의 destroyMethod
속성에는 특별한(?) 기능이 있다.close
, shutdown
이라는 이름의 종료 메소드를 사용한다.destroyMethod
는 기본값으로 "(inferred)"
를 갖는데, 기능은 close
, shutdown
이라는 이름을 가진 메소드를 자동으로 호출해주는 것이다.@Bean
으로 등록했을 경우만, 자동 등록 제외) 종료 메소드는 따로 적어주지 않아도 잘 동작한다.destroyMethod=""
처럼 빈 공백을 지정하면 된다.import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
public class NetworkClient {
...
@PostConstruct
public void init() {
System.out.println("NetworkClient.afterPropertiesSet");
connect();
call("초기화 연결 메시지");
}
@PreDestroy
public void close() {
System.out.println("NetworkClient.destroy");
disconnect();
}
}
@PostConstruct
, @PreDestroy
만 붙이면 끝이다!@Bean
의 parameter들은 지워주자javax.annotation.PostContruct
이므로, 자바 표준임을 알 수 있고, 스프링이 아닌 다른 컨테이너에서도 잘 동작한다.@Bean
의 parameter를 사용하자.@PostContructor
, @PreDestroy
애노테이션을 사용하자.@Bean
의 initMethod
, destroyMethod
를 이용하자.