1. 빈 생명주기 콜백의 필요성
데이터베이스 연결이나 네트워크 소켓처럼 시작 시 연결하고 종료 시 끊어야 하는 객체는 초기화와 종료 작업이 필수입니다.
문제 상황: 생성자에서 초기화 시도
package com.example.spring_study.lifecycle;
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);
}
// connect가 된 상태에서 call을 부를 수 있다고 가정
public void call(String message){
System.out.println("call: " + url + ", message: " + message);
}
// 서비스 종료시 호출하는 메서드
public void disconnect() {
System.out.println("close: " + url);
}
}
- 결과: url이 null인 상태로 connect()가 호출됨. (생성자 호출 시점에는 의존관계 주입이 안 되었기 때문)
- 해결: 객체 생성과 초기화를 분리하고, 의존관계 주입이 완료된 후 호출되는 콜백을 사용해야 함.
2. 방법 1: 인터페이스 (InitializingBean, DisposableBean)
스프링 초창기에 사용하던 방법으로, 인터페이스의 메서드를 오버라이딩합니다.
[전체 코드: NetworkClient.java]
package com.example.spring_study.lifecycle;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
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);
}
// connect가 된 상태에서 call을 부를 수 있다고 가정
public void call(String message){
System.out.println("call: " + url + ", message: " + message);
}
// 서비스 종료시 호출하는 메서드
public void disconnect() {
System.out.println("close: " + url);
}
@Override
// InitializingBean 부모에서 상속
public void afterPropertiesSet() throws Exception { // 의존관계 주입이 끝나면 호출해 주겠다는 뜻
// 생성자에서는 진짜 생성만 하고, 초기화하는 과정을 분리함
System.out.println("NetworkClient.afterPropertiesSet");
connect();
call("초기화 생성 메시지");
}
@Override
// DisposableBean 부모에서 상속
public void destroy() throws Exception { // Bean이 종료될 때 호출
System.out.println("NetworkClient.destroy");
disconnect();
}
}
- 특징: 스프링 전용 인터페이스에 의존함. 메서드 이름 변경 불가. 외부 라이브러리 적용 불가.
3. 방법 2: 빈 등록 시 초기화, 소멸 메서드 지정
설정 정보(@Bean)에 직접 메서드 이름을 지정하는 방식입니다.
[전체 코드: NetworkClient.java & Config]
package com.example.spring_study.lifecycle;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
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);
}
// connect가 된 상태에서 call을 부를 수 있다고 가정
public void call(String message){
System.out.println("call: " + url + ", message: " + message);
}
// 서비스 종료시 호출하는 메서드
public void disconnect() {
System.out.println("close: " + url);
}
public void init() throws Exception { // 의존관계 주입이 끝나면 호출해 주겠다는 뜻
// 생성자에서는 진짜 생성만 하고, 초기화하는 과정을 분리함
System.out.println("NetworkClient.init");
connect();
call("초기화 생성 메시지");
}
public void close() throws Exception { // Bean이 종료될 때 호출
System.out.println("NetworkClient.close");
disconnect();
}
}
package com.example.spring_study.lifecycle;
import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
public class BeanLifeCycleTest {
@Test
public void lifeCycleTest() {
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
NetworkClient client = ac.getBean(NetworkClient.class);
ac.close(); // 컨테이너가 종료되면서, close()가 호출됨
}
@Configuration
static class LifeCycleConfig {
@Bean(initMethod = "init", destroyMethod = "close") // NetWorkClient의 메소드 중에서 초기화와 종료 메소드를 지정
// -> 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있음
// Bean이 완전히 등록된 후에 "init()이 호출됨
// destroyMethod는 (inferred) 추론으로 등록되어 있어서, 종료 메서드는 따로 적어주지 않아도 @Bean으로 등록할 시에는 'close`, 'shutdown' 라는 이름의 메서드를 자동으로 호출해줌
// -> 즉, 이름 그대로 종료 메서드를 추론해서 호출(일반적으로 외부라이브러리의 경우, close, shudown을 종료 메서드 이름임)
// -> 이 추론 기능을 사용하기 싫으면 destroyMethod = ""로 지정하면 됨
public NetworkClient networkClient() {
// 객체의 생성과 초기화는 분리하는 것이 좋다
// 생성자는 필수 정보(파라미터)를 받고, 메모를 할당해서 객체를 생성하는 책임을 가짐
// 반면, 초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는 무거운 동작을 수행
// -> 이러한 무거운 동작을 생성자에서 하는 것보다 이를 분리하는 것이 유지보수 관점에서 좋음
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("<http://hello-spring.dev>");
return networkClient;
}
}
}
- 특징: 스프링 코드에 의존 안 함. 코드를 고칠 수 없는 외부 라이브러리에 사용 가능.
- 종료 메서드 추론: destroyMethod를 생략해도 close, shutdown 이름의 메서드를 자동으로 찾아서 호출해줌.
4. 방법 3: @PostConstruct, @PreDestroy (권장)
가장 편리하고 최신 스프링에서 강력하게 권장하는 표준 방식입니다.
[전체 코드: NetworkClient.java]
package com.example.spring_study.lifecycle;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
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);
}
// connect가 된 상태에서 call을 부를 수 있다고 가정
public void call(String message){
System.out.println("call: " + url + ", message: " + message);
}
// 서비스 종료시 호출하는 메서드
public void disconnect() {
System.out.println("close: " + url);
}
// 이 방법은 외부 라이브러리에는 적용하지 못함
@PostConstruct // 생성자 이후에 호출한다는 뜻
public void init() throws Exception { // 의존관계 주입이 끝나면 호출해 주겠다는 뜻
// 생성자에서는 진짜 생성만 하고, 초기화하는 과정을 분리함
System.out.println("NetworkClient.init");
connect();
call("초기화 생성 메시지");
}
@PreDestroy // 종료 이전에 호출한다는 뜻
public void close() throws Exception { // Bean이 종료될 때 호출
System.out.println("NetworkClient.close");
disconnect();
}
}
- 특징: 자바 표준(JSR-250)이라 스프링이 아닌 곳에서도 동작. 컴포넌트 스캔과 잘 어울림.
- 유일한 단점: 외부 라이브러리에는 적용 못 함 (코드 수정을 못 하니까).
5. 결론: 무엇을 써야 할까?
- 기본적으로 @PostConstruct, @PreDestroy를 사용하자.
- 코드를 고칠 수 없는 외부 라이브러리를 종료해야 하면 @Bean(initMethod, destroyMethod)를 사용
'INFLEARN' 카테고리의 다른 글
| [모든 개발자를 위한 HTTP 웹 기본 지식] 1. 인터넷 네트워크 (0) | 2026.03.26 |
|---|---|
| [스프링 핵심 원리 - 기본편] 17. 빈 스코프(Singleton, Prototype, Request, Proxy) (0) | 2026.03.24 |
| [스프링 핵심 원리 - 기본편] 15. 조회한 빈이 모두 필요할 때: List, Map 활용 (0) | 2026.03.21 |
| [스프링 핵심 원리 - 기본편] 14. 애노태이션 직접 생성하기 (1) | 2026.03.21 |
| [스프링 핵심 원리 - 기본편] 13. 조회 Bean이 2개 이상일 경우(@Qualifier vs @Primary) (0) | 2026.03.20 |