개발자로 후회없는 삶 살기
[문법] 빈 생명주기 콜백 본문
서론
※ 과거에 기록한 내용에서 중요한 부분만 발췌하여 모두가 이해하기 쉽게 다시 서술한다.
본론
- 빈 생명주기 콜백
빈이 생성되거나 죽기 직전에 스프링이 빈 안에 있는 메서드를 호출해줄 수 있는 기능이다. 빈이 처음 등록되면서 생성될 때, 초기화할 때 호출되고 빈이 사라지기 직전에 안전하게 종료할 수 있는 메서드를 호출하는 내용이다. 3가지 방식이 있다.
=> 예제
서버가 뜰 때 미리 외부 네트워크와 연결을 하고 내려갈 때 미리 연결을 끊어야하는 서버가 있어야 한다고 해보자. 어플 시작 시점에 connect()를 호출하여 미리 연결을 맺어 두어야하고 종료할 때 discount()를 호출해서 연결을 끊어야한다.
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 message) {
System.out.println("call:" + url + "message = " + message);
}
public void disconnect() {
System.out.println("close: " + url);
}
}
connect : 서비스를 시작할 때 서버와 연결
call : 연결할 서버에 메세지를 던짐
disconnect : 서비스 종료 시 안전하게 연결을 끊음
서비스를 시작할 때 호출하는 메서드를 만든다. 연결한 서버에 메세지를 던질수 있는 call 메서드를 만들고 서비스 종료 시 안전하게 연결을 끊을 메서드를 만들었다. 생성자에서 connect하고 call 한다.
-> 테스트 작성
public class BeanLifeCycleTest {
@Test
void lifeCycleTest() {
AnnotationConfigApplicationContext 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("https://");
return networkClient;
}
}
}
빈 등록할 때 setUrl을 한다.
실행을 해보면 connect가 null로 연결된다. 생성자를 호출하면 처음에 url이 없어서 null 이다. 생성이 끝난 후 set으로 url을 넣어줬다. 객체가 생성된 후에 url 초기화가 일어난 것이다. 실무에서는 외부에서 요청이 있는 후에 초기화를 해야하는 경우가 많다.
생성자를 보면 url 없이 connect가 호출됐는데, 이처럼 설계된 이유는 개발자가 빈이 생성된 시점을 알 수 없기 때문이다. 스프링 빈은 간단하게 객체를 생성하고 의존관계 주입을 하는 라이프 사이클이 생긴다. 객체를 전부 다 생성한 후에 의존관계 주입을 한다. (예외 : 생성자 주입) 스프링 빈은 객체를 생성하고 DI가 다 끝난 다음에야 필요한 데이터를 사용할 수 있는 준비가 완료된다. 따라서 초기화 작업은 DI가 다 완료된 다음에 호출해야 한다.
✅ 개발자가 DI까지 다 완료된 시점을 어떻게 알 수 있을까요?
알 수 없어서 set을 늦게 하여 URL에 null이 들어간 상황이다. 자동 의존 관계 주입을 하면 개발자는 알 수가 없어서 연결이 다 됐다고 개발자에게 알려줘야 한다. 스프링은 의존 관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공한다. 또한 빈이 종료되기 직전에 소멸 콜백한다. 따라서 작성한 disconnect 같은 종료 작업 메서드를 안전하게 진행할 수 있다.
-> 스프링 빈의 이벤트 라이프사이클 정리
1) 스프링 컨테이너 생성
2) 스프링 빈 생성
3) 의존관계 주입
4) 초기화 콜백("이제 의존관계 주입 끝났으니 하고 싶은 것 다 해"를 알려준다.)
5) 사용(어플리케이션 동작)
6) 소멸전 콜백
7) 스프링 종료
초기화 콜백은 빈이 생성되고 , DI가 완료된 후 호출되고 소멸 전 콜백은 빈이 소멸되기 직전에 호출된다. 스프링은 다양한 방식의 생명주기 콜백을 지원한다. 근데 "생성자 주입에서 다 DI도 하고 초기화도 하면 되지 않나? 그러면 완벽히 연결이 끝나고 바로 사용할 수 있지 않나?" 라고 고민할 수 있다.
-> 객체의 생성과 초기화를 분리
생성자 주입에서 DI도 하고 초기화도 하면 되지 않을까? 왜 분리할까? 단일 책임 원칙에 따라서 객체를 생성하는 것과 초기화는 분리하는 게 좋다.
객체 생성 : 필수 정보만 받고 객체 생성에 집중
초기화 : 생성된 값들을 활용해서 외부 커낵션 등 무거운 동작 수행
따라서, 객체를 생성하는 부분과 초기화하는 무거운 작업을 명확하게 분리하는게 유지보수 측면에서 좋다.
- 인터페이스 방법
public class NetworkClient implements InitializingBean
인터페이스로 초기화, 소멸 콜백을 받을 수 있다. InitializingBean을 impliment를 한다.(등록할 빈 구현체에다가 한다.)
// 초기화 콜백 전
public NetworkClient() {
System.out.println("생성자 호출, url = " + url);
connect();
call("초기화 연결 메서드");
}
// 초기화 콜백 후
public NetworkClient() {
System.out.println("생성자 호출, url = " + url);
}
public void afterPropertiesSet() throws Exception {
connect();
call("초기화 연결 메서드");
}
// 소멸 콜백
@Override
public void destroy() throws Exception {
disconnect();
}
생성과 등록, 주입이 끝나면 초기화 작업을 한다. DI가 끝난 후 after이 호출된다. 생성과 DI가 끝났으니 연결과 초기화하는 무거운 작업을 해도 된다는 뜻이다. 소멸콜백은 disposableBean을 구현하고 destoy를 오버라이딩하고 disconnect를 호출하도록 한다. 이렇게 하면 스프링 컨테이너가 생성이 되고 DI가 끝난 후 after이 호출이 되고 빈이 종료될 때 destroy가 호출이 된다.
테스트를 해보면 생성자 호출하는 단계에서는 URL이 없고 생성과 DI가 다 끝난 후에 after이 호출이 되면서 Connect가 호출이 된다.
보면 closing이 빈이 소멸하기 직전이라는 것인데 그때 destory가 발생된다. destroy에 disconnect를 구현해놨으니 어플이 안전하게 소멸될 것이다.
🚨 초기화 소멸 인터페이스의 단점
스프링 전용 인터페이스이기 때문에 전용 인터페이스에 의존적이고 오버라이딩을 하는 것 자체가 부담스럽다.
- 메서드 방법
@Configuration
static class LifeCycleConfig {
@Bean(initMethod = "init", destroyMethod = "close")
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("https://");
return networkClient;
}
}
빈을 등록하는 시점에 "얘가 초기화야", "얘가 소멸이야"라고 지정해주는 방법이 있다. @ Bean의 initMethod, destroyMethod를 명시하면 된다. 인터페이스 방법의 after와 destroy 방식을 내가 만든 메서드로 지정하는 것이다.
public void init() {
System.out.println("after");
connect();
call("초기화 연결 메서드");
}
public void close() {
System.out.println("destroy");
disconnect();
}
메서드 이름을 맘대로 할 수 있고 스프링에 의존적이지 않고 좋다. 또한 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있는 것이 제일 큰 장점이다.
+ inferred
@Bean(initMethod = "init", destroyMethod = "(inferred)")
@Bean의 destroymethod는 특별한 기능이 있다. defualt가 "(inferred)"로 되어있는데 아까 외부 라이브러리에도 메서드를 적용할 수 있다고 했다. 외부 라이브러리 대부분이 close나 shutdown 종료 메서드 이름인데, 따라서 이 추론이라는게 close나 shutdown이라는 메서드를 자동으로 등록된 빈에서 찾아서 종료 시점에 알아서 호출을 해준다.
destory를 지정하지 않아도 알아서 추론해서 호출된다.
- 에너테이션 방법
@PostConstruct
public void init() {
System.out.println("after");
connect();
call("초기화 연결 메서드");
}
@PreDestroy
public void close() {
System.out.println("destroy");
disconnect();
}
Bean에 있던 지정하는 것을 다 지우고 아까 초기화와 종료 때 호출 될 메서드에 @PostConstruct, @PreDestroy 어노를 붙이면 끝이다.
-> 특징
최신 스프링에서 가장 권장하는 방법으로, @Bean에 지정하는 것이 아니니 컴포넌트 스캔과 잘 어울리고, 유일한 단점은 외부 라이브러리에는 적용하지 못 해서 그 때는 메서드 방법을 쓰면 된다.
'[백엔드] > [spring | 학습기록]' 카테고리의 다른 글
[문법] Spring 핸들러 어댑터, API 설계 실전 (0) | 2024.08.02 |
---|---|
[문법] 웹 서버와 서블릿 (0) | 2024.08.01 |
[문법] 의존관계 자동 주입 (0) | 2024.07.31 |
[문법] Spring 프레임워크 사용 목적 및 Web 통신 원리 (0) | 2024.07.29 |
spring PART.중간점검 5 (0) | 2023.05.24 |