콜백 메서드가 필요한 이유
- 애플리케이션 시작할 때 외부 네트워크와 연결을 하고, 종료 시점에 연결을 모두 종료하는 상황
- 애플리케이션 시작 시점에 NetworkClient를 생성하면서 connect를 통해 연결을 함
- 종료 시점에는 disconnect를 통해 연결을 끊기
빠른 결론
처음 이 부분을 공부했을 때, 머릿 속으로 도저히 이해가 되질 않았다.
그래서 이 글을 본 분들이나 미래의 나에게 빠른 이해를 돕고 싶다.
유치할 수도 있지만 빠르게 이해하기 위해서 각 코드 사이마다 숫자를 넣어보았다.
실행 순서를 알기 위함이지 의미 있는 숫자들은 아님!!
BeanLifeCycleTest.class
public class BeanLifeCycleTest {
@Test
void lifeCycleTest() throws Exception {
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
System.out.println("0");
NetworkClient client = ac.getBean(NetworkClient.class);
System.out.println("3");
ac.close();
}
@Configuration
static class LifeCycleConfig{
@Bean
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
System.out.println("1");
networkClient.setUrl("hi");
System.out.println("2");
return networkClient;
}
}
}
NetworkClient.class
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("NetworkClient.connect");
System.out.println("connect: " + url);
}
public void call(String message) {
System.out.println("call: " + url + " message = " + message);
}
public void disconnect() {
System.out.println("NetworkClient.disconnect");
System.out.println("close: " + url);
}
}
/**
생성자 호출, url = null
NetworkClient.connect
connect: null
call: null message = 초기화 연결 메시지
1
2
0
3
*/
순서를 생각해보자
- 스프링 컨테이너를 생성하면서 LifeCycleConfig과 NetworkClient 빈을 등록함
- NetworkClient 빈을 등록하기 위해 생성자 호출
- 생성자 호출, connect, call 을 실행시킴
- 1을 출력
- setUrl 실행
- 2를 출력
- 스프링 컨테이너 생성 및 빈 등록이 완료되어 0을 출력
- NetworkClient 빈을 조회하고 3을 출력
이 코드는 그냥 자연스럽게 흘러가면 된다. 이제 @PostConstruct, @PreDestory를 사용할 경우를 보자.
public class NetworkClient {
private String url;
public NetworkClient() {
System.out.println("생성자 호출, url = " + url);
// connect();
// call("초기화 연결 메시지");
}
public void setUrl(String url) {
this.url = url;
}
@PostConstruct
public void connect() {
System.out.println("NetworkClient.connect");
System.out.println("connect: " + url);
call("초기화 연결 메시지");
}
public void call(String message) {
System.out.println("call: " + url + " message = " + message);
}
@PreDestroy
public void disconnect() {
System.out.println("NetworkClient.disconnect");
System.out.println("close: " + url);
}
}
/**
생성자 호출, url = null
1
2
NetworkClient.connect
connect: hi
call: hi message = 초기화 연결 메시지
0
3
00:43:23.946 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@71def8f8, started on Tue Mar 15 00:43:23 KST 2022
NetworkClient.disconnect
close: hi
*/
순서를 생각해보자.
- 스프링 컨테이너를 생성하면서 LifeCycleConfig과 NetworkClient 빈을 등록함
- NetworkClient 빈을 등록하기 위해 생성자 호출
- 생성자 호출
- 1번 출력
- setUrl
- 2번 출력
- @PostConstruct인 connect 메서드 실행
- 스프링 컨테이너 생성 완료
- 빈 조회 후 3 출력
이제 비교를 해보자
- 1번
- 코드 flow 그대로 흘러갔음
- 2번
- connect를 따로 호출하지 않았는데 url이 주입되고 나서 connect를 호출함
- ac.close() 후에 스프링 컨테이너가 종료되기 전에 disconnect를 호출함
결론
- 스프링 컨테이너가 생성 -> 빈 생성 -> 의존관계 주입이 완료되면 @PostConstruct 메서드를 실행해줌
- 사용하고 스프링이 종료되기 전의 시점에는 @PreDestroy 메서드를 실행해줌
- 왜 필요할까?
- 이처럼 의존관계 주입이 완료되고 나서 실행해야 할 메서드가 있는데 작업이 완료된 시점은 우리가 모름
- 따라서 스프링이 의존관계 주입이 완료되면 콜백 메서드를 통해 알려줌
스프링 빈 라이프사이클
- 객체 생성
- 의존관계 주입
- 필요한 데이터를 사용할 수 있음
일반적으로 객체 생성을 하고, 의존관계 주입을 한 뒤, 데이터에 대한 초기화를 진행하고 데이터를 사용할 수 있다.
아래의 사진을 보면 1번은 객체를 생성하는 것, 2번은 url이 외부에서 setter 주입을 통해 데이터를 받고 있다.
1번에서 객체를 생성할 때, 2번은 실행되기 전이므로 당연히 url은 null일 것이고, 연결은 실패할 것이다.
만약에 객체 생성을 하고 의존관계 주입이 모두 된 뒤에 connect() 메서드를 실행하면 되지 않을까?
그래서 스프링은 의존관계 주입이 완료되면 초기화 시점을 알려주는 다양한 기능을 제공한다고 한다.
3가지 빈 생명주기 콜백
인터페이스 사용
InitializingBean, DisposableBean 인터페이스를 구현하고 해당 추상 메서드를 오버라이딩 하면 된다.
각각 afterPropertiesSet, destory 메서드가 있다.
afterPropertiesSet은 의존관계 주입이 모두 끝나고 난 뒤, destory는 종료할 때 불러진다.
그래서 connect 메서드와 call은 afterPropertiesSet메서드에, disconnect 메서드는 destory에 넣으면 된다.
public class NetworkClient implements InitializingBean, DisposableBean{
//중복 코드
//...
@Override
public void destroy() throws Exception {
disconnect();
}
@Override
public void afterPropertiesSet() throws Exception {
connect();
call("초기화 연결 메시지");
}
}
메서드 지정
초기화 메서드와 소멸 메서드를 지정할 수 있다.
설정 정보에 @Bean 옆에 initMethod와 destroyMethod를 사용하여 각각 매칭되는 메서드 이름을 적으면 된다.
// @Bean(initMethod = "init", destroyMethod = "destroy")
@Bean(initMethod = "init")
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hello-spring.dev");
return networkClient;
}
}
어노테이션 사용
초기화할 때 부를 메서드를 @PostConstruce, 종료 메서드를 @PreDestory 어노테이션을 붙인다.
위 방법들을 사용하면 결과값이 아래와 같은 사진처럼 나온다.
@PreDestroy
public void destroy() throws Exception {
disconnect();
}
@PostConstruct
public void init() throws Exception {
connect();
call("초기화 연결 메시지");
}
메서드 지정 vs 어노테이션 사용
둘의 기능(?)을 구분할 수 있는 가장 큰 차이점은
메서드 지정은 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있지만
어노테이션은 외부 라이브러리에는 적용하지 못한다.
이 부분에 대해서도 궁금한 점이 생겨 QnA에 내가 생각한 것이 맞는지 확인을 해봤다.
Q.
왜 어노테이션을 사용하면 외부 라이브러리에 사용하지 못할까?
예로 gradle을 들어보겠습니다. gradle은 수정할 수 없는 외부 라이브러리이다.
여기서 테스트를 한다고 했을 때
우리는 테스트 코드를 짜면서 직접 @Bean으로 등록할 때, 해당 라이브러리에 있는 클래스 안에 있는 메서드들을 파악하고 빈으로 직접 등록하여 초기화 종료를 할 수 있다.
그러나 어노테이션은 코드에 @을 붙여야하는데 코드를 수정할 수 없기 때문에 사용할 수 없다.
A.
맞다. @Bean의 init method, destory method를 통해서 초기화, 종료 메서드를 지정할 수 있다.
출처 : 인프런 스프링 핵심 원리 - 기본편 (김영한 강사님)
'Spring 강의 > 스프링 핵심 원리 - 기본편' 카테고리의 다른 글
스프링 핵심 원리 - 빈 스코프(2022.03.21 수정) (0) | 2021.08.09 |
---|---|
스프링 핵심 원리 - 의존관계 자동 주입(2022.03.12 수정) (0) | 2021.08.06 |
스프링 핵심 원리 - 컴포넌트 스캔 (0) | 2021.08.06 |
스프링 핵심 원리 - 싱글톤 컨테이너 (0) | 2021.08.04 |
스프링 핵심 원리 - 스프링 컨테이너와 스프링 빈 (0) | 2021.08.02 |