1. 서론
실제 애플리케이션 개발에서는 데이터베이스 연결, 네트워크 소켓 연결 등 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 종료 시점에 이러한 연결을 안전하게 해제해야 하는 작업이 필요합니다. 스프링에서는 이러한 초기화 작업과 종료 작업의 시점을 알려주는 다양한 생명주기 콜백 기능을 제공합니다.
이번 포스팅에서는 스프링 빈의 생명주기와 생명주기 콜백을 활용하는 다양한 방법에 대해 알아보겠습니다. 특히 객체의 초기화와 종료 시점을 스프링 컨테이너가 어떻게 관리하는지, 그리고 개발자가 이를 어떻게 제어할 수 있는지 실제 예제 코드와 함께 살펴보겠습니다.
2. 스프링 빈의 생명주기
스프링 컨테이너에 의해 관리되는 빈은 생성부터 소멸까지 일정한 생명주기를 갖습니다. 이러한 생명주기를 제대로 이해하고 활용하면 리소스를 효율적으로 관리할 수 있습니다.
2.1. 빈 생명주기의 문제 상황
생명주기의 중요성을 이해하기 위해 다음과 같은 간단한 예제를 살펴보겠습니다. 외부 네트워크에 연결하는 클라이언트 객체를 생성하고, 연결을 시작하고 종료하는 상황을 가정해 봅시다.
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);
}
}
Java
복사
이 클래스를 스프링 빈으로 등록하고 실행해보면 문제가 발생합니다:
@Configuration
static class LifeCycleConfig {
@Bean
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("<http://hello-spring.dev>");
return networkClient;
}
}
Java
복사
실행 결과:
생성자 호출, url = null
connect: null
call: null message = 초기화 연결 메시지
Plain Text
복사
문제는 생성자에서 url이 null인 상태로 connect() 메서드가 호출된다는 점입니다. 객체가 생성된 다음에야 외부에서 setUrl() 메서드를 통해 url 값이 주입되기 때문입니다.
2.2. 스프링 빈 생명주기의 단계
스프링 빈은 다음과 같은 생명주기를 가집니다.
1.
스프링 컨테이너 생성
2.
스프링 빈 생성
3.
의존관계 주입
4.
초기화 콜백
•
빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
5.
사용
6.
소멸전 콜백
•
빈이 소멸되기 직전에 호출
7.
스프링 종료
여기서 중요한 점은 의존관계 주입이 모두 완료된 후에야 초기화 작업을 진행해야 한다는 것입니다. 스프링은 의존관계 주입이 완료되면 빈에게 콜백 메서드를 통해 초기화 시점을 알려주고, 컨테이너가 종료되기 직전에는 소멸 콜백을 제공합니다.
참고: 객체의 생성과 초기화를 분리하자
생성자는 필수 정보를 받고 메모리를 할당해 객체를 생성하는 역할에 집중하고, 초기화는 생성된 값들을 활용해 무거운 동작(외부 연결 등)을 수행하는 것이 좋습니다. 이렇게 분리하면 유지보수 관점에서 더 명확합니다.
3. 빈 생명주기 콜백 지원 방법
스프링은 크게 3가지 방법으로 빈 생명주기 콜백을 지원합니다.
1.
인터페이스(InitializingBean, DisposableBean)
2.
설정 정보에 초기화 메서드, 종료 메서드 지정
3.
@PostConstruct, @PreDestroy 애노테이션
각 방법의 특징과 예제를 살펴보겠습니다.
3.1. 인터페이스 방식 (InitializingBean, DisposableBean)
가장 오래된 방식으로, 스프링에서 제공하는 인터페이스를 구현하는 방법입니다.
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("call: " + url + " message = " + message);
}
// 서비스 종료시 호출
public void disConnect() {
System.out.println("close + " + url);
}
@Override
public void afterPropertiesSet() throws Exception {
connect();
call("초기화 연결 메시지");
}
@Override
public void destroy() throws Exception {
disConnect();
}
}
Java
복사
•
InitializingBean은 afterPropertiesSet() 메서드로 초기화를 지원합니다.
•
DisposableBean은 destroy() 메서드로 소멸을 지원합니다.
•
장점
◦
스프링 컨테이너가 직접 관리하므로 별도 설정이 필요 없습니다.
•
단점
◦
스프링 전용 인터페이스에 의존하게 됩니다.
◦
메서드 이름을 변경할 수 없습니다.
◦
코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없습니다.
3.2. 빈 등록 초기화, 소멸 메서드 지정
설정 정보에서 @Bean의 속성으로 초기화, 소멸 메서드를 지정하는 방법입니다.
public class NetworkClient {
private String url;
// 생성자, setter 등 다른 메서드는 동일
// 서비스 시작 시 호출
public void init() {
System.out.println("NetworkClient.init");
connect();
call("초기화 연결 메시지");
}
// 서비스 종료 시 호출
public void close() {
System.out.println("NetworkClient.close");
disConnect();
}
}
@Configuration
static class LifeCycleConfig {
@Bean(initMethod = "init", destroyMethod = "close")
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("<http://hello-spring.dev>");
return networkClient;
}
}
Java
복사
•
장점
◦
메서드 이름을 자유롭게 설정할 수 있습니다.
◦
스프링 코드에 의존하지 않습니다.
◦
코드를 고칠 수 없는 외부 라이브러리에도 적용 가능합니다.
•
특별한 기능
◦
destroyMethod는 기본값이 (inferred)(추론)으로 설정되어 있어, close, shutdown이라는 이름의 메서드를 자동으로 호출해줍니다.
◦
이 기능을 사용하지 않으려면 destroyMethod=""로 설정하면 됩니다.
3.3. 애노테이션 방식 (@PostConstruct, @PreDestroy)
최신 스프링에서 가장 권장하는 방법으로, 자바 표준 애노테이션을 사용합니다.
public class NetworkClient {
private String url;
// 생성자, setter 등 다른 메서드는 동일
@PostConstruct
public void init() {
System.out.println("NetworkClient.init");
connect();
call("초기화 연결 메시지");
}
@PreDestroy
public void close() {
System.out.println("NetworkClient.close");
disConnect();
}
}
@Configuration
static class LifeCycleConfig {
@Bean
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("<http://hello-spring.dev>");
return networkClient;
}
}
Java
복사
장점:
•
최신 스프링에서 가장 권장하는 방법입니다.
•
애노테이션 하나만 붙이면 되므로 매우 편리합니다.
•
자바 표준 기술(JSR-250)이므로 스프링이 아닌 다른 컨테이너에서도 동작합니다.
•
컴포넌트 스캔과 잘 어울립니다.
단점:
•
외부 라이브러리에는 적용할 수 없습니다. 이 경우 @Bean의 initMethod, destroyMethod 속성을 사용해야 합니다.
4. 생명주기 콜백 방식 비교와 권장사항
세 가지 방식을 비교해보면 각각 장단점이 있지만, 최신 스프링에서는 @PostConstruct, @PreDestroy 애노테이션 방식을 가장 권장합니다.
1.
기본적으로 @PostConstruct, @PreDestroy 애노테이션을 사용
•
코드가 간결하고 자바 표준이므로 다른 컨테이너에서도 동작
•
컴포넌트 스캔과 함께 사용하기 좋음
2.
외부 라이브러리를 초기화, 종료해야 하는 경우
•
@Bean의 initMethod, destroyMethod 속성 사용
•
코드를 수정할 수 없는 클래스에 대해 외부에서 설정 가능
3.
인터페이스 방식은 거의 사용하지 않음
•
스프링 초창기 방식으로, 최근에는 더 나은 대안이 있어 잘 사용하지 않음
5. 결론
스프링 빈의 생명주기 콜백은 애플리케이션의 안정적인 리소스 관리를 위해 매우 중요합니다. 객체의 초기화와 종료 작업을 적절한 시점에 수행함으로써 자원 누수를 방지하고 애플리케이션의 안정성을 높일 수 있습니다.
세 가지 생명주기 콜백 방식 중에서 @PostConstruct와 @PreDestroy 애노테이션을 사용하는 방식이 가장 편리하고 권장되며, 외부 라이브러리를 다룰 때는 @Bean의 속성을 활용하는 방식을 사용하면 됩니다.
빈 생명주기 콜백을 적절히 활용하면 데이터베이스 연결, 네트워크 소켓 연결 등의 리소스를 효율적으로 관리할 수 있으며, 애플리케이션의 안정성과 성능을 향상시킬 수 있습니다.