빈 생명주기 콜백

박병욱·2025년 4월 18일

Spring

목록 보기
8/13
post-thumbnail

🔄️ 빈 생명주기 콜백 시작

데이터베이스 커넥션 풀이나, 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화와 종료 작업이 필요하다. 스프링을 통해 어떻게 초기화 작업과 종료 작업을 진행하는지 간단한 예제로 알아보자.

정말 간단하게 서버가 뜰 때 미리 외부 네트워크에 연결하고, 서버가 종료할 때 미리 외부 네트워크와의 연결을 끊는 서버가 있다고 가정하자.

 

이제 만들 NetworkClient가 애플리케이션을 시작할 때는 connect()를 호출해서 미리 연결을 맺고, 애플리케이션이 종료되면 disconnect()를 호출해서 연결을 끊는다.

package hello.core.lifecycle;

public class NetworkClient {
	
	private String url;  // 접속해야 할 서버 URL
	
    // 인스턴스 생성 시 connect() 메서드 호출
	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);
	}
}
package hello.core.lifecycle;

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
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();  // 스프링 컨테이너를 종료, ConfigurableApplicationContext 필요
	}
	
	@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 message = 초기화 연결 메시지

 

당연하게도 NetworkClient 인스턴스를 생성할 때, URL 정보가 없기 때문에 생성자에서 호출되는 connect() 메서드로 연결을 해봤자 값이 없다. 객체를 생성한 다음, setUrl() 메서드로 URL 값을 주입해줘야 내가 원한 결과를 끌어낼 수 있다. 근데 개발자가 의존관계 주입이 모두 완료된 시점을 어떻게 알 수 있는 거지? 조상님이 알려주나?

 

다행스럽게도 스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공한다. 또, 스프링은 스프링 컨테이너가 종료되기 직전에, 혹은 스프링 빈이 종료되기 직전에 소멸 콜백을 준다. 덕분에 안전하게 종료 작업을 진행할 수 있다.

 

스프링 빈의 이벤트 라이프사이클을 살펴보면...

(싱글톤 Case)

  1. 스프링 컨테이너가 생성된다.

  2. 스프링 빈을 생성한다. 생성자 주입 같은 경우에는 객체를 생성해야 되기 때문에 이 단계에서 어느 정도 일어난다.

  3. 의존관계를 주입한다. 특히 수정자 주입이나 필드 주입 같은 경우는 이 단계에서 일어난다.

  4. 초기화 콜백을 준다. 의존관계 주입이 끝났으니까 네가 하고 싶은 대로 마음껏 하라는 것이다.

  5. 실제 애플리케이션이 동작한다.

  6. 소멸 전 콜백이 온다.

  7. 스프링이 종료된다.

 

  • 초기화 콜백 : 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
  • 소멸 전 콜백 : 빈이 소멸되기 직전에 호출

 

근데 그냥 생성자 주입으로 싹 다 해버려도 되지 않나…? 사실 이 방법에는 문제가 있다.

 

🤔그냥 생성자에서 URL 정보를 받아서 생성하면?

객체의 생성과 초기화는 분리하는 게 좋다. 단일 책임 원칙을 상기하자. 객체를 생성하는 건 정말 그 객체를 생성하는데 모든 집중을 다 해야 한다. 그리고 어쨌든 객체 초기화 작업을 한다는 것은, 그 객체가 동작한다는 말이다. 그래서 객체 생성을 생성하는 것과, 그 객체에 실제 뭔가 동작하거나 하는 행위는 별도의 초기화 메서드로 분리해서 설계하는 것이 좋다.

그리고 이렇게 하면 뭐가 좋냐? 객체를 딱 생성을 했다고 하자. 근데 실제 외부 커넥션을 맺는 건 최초의 행위가 올 때까지 미룰 수 있다. 그런 식으로 동작을 지연시킬 수 있는 장점도 있다.

 

자, 이제 생각해보자. 스프링은 “의존관계 주입 끝나서 나 이제 초기화해도 됨”, “스프링 빈 사라지기 직전에 나 이제 소멸되니까 커넥션 미리 닫는다” 등등… 이런 걸 어떻게 우리에게 알려줄까? 크게 3가지 방법이 있다.

  • InitializingBeanDisposableBean 인터페이스를 통해서 지원한다.

  • 설정 정보에 초기화 메서드, 종료 메서드를 지정할 수 있다.

  • @PostConstruct@PreDestroy 애노테이션을 붙인다.


😳 인터페이스 InitializingBean, DisposableBean (사용 X)

먼저 인터페이스로 초기화와 소멸 전 콜백을 받는 방법에 대해서 알아보자. 바로 코드를 보자.

package hello.core.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);
	}
	
	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();
	}
}

보다시피 InitializingBeanafterPropertiesSet() 메서드로 초기화를 지원한다. DisposableBeandestroy() 메서드로 소멸을 지원한다.

빈이 생성되고 의존관계 주입이 다 끝나고 나면 afterPropertiesSet 메서드가 호출되어 connect()가 호출되고, 해당 빈이 종료될 때 destroy() 메서드가 호출된다는 말이다.

 

<초기화, 소멸 인터페이스 단점>

  • 일단 이 인터페이스들은 스프링 전용 인터페이스다. 내 코드가 전부 스프링 전용 인터페이스에 의존적으로 설계를 해야 한다.
  • 초기화, 소멸 메서드의 이름을 변경할 수 없다.
  • 내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.

 

인터페이스를 사용하는 초기화, 종료 방법은 스프링 초창기에 나온 방법들이고, 지금은 다음의 더 나은 방법들이 있어서 사용하지 않는다고 봐도 무방하다.


🎏 빈 등록 초기화, 소멸 메서드 지정

빈을 등록하는 시점에 누가 초기화인지, 누가 소멸인지 딱 지정해주는 아주 간단한 방법이 있다. 설정 정보에 @Bean(initMethod = ”init”, destroyMethod = ”close”) 메서드처럼 이름만 주면 끝이다.

package hello.core.lifecycle;

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);
	}
	
	public void call(String message) {
		System.out.println("call: " + url + "message = " + message);
	}
	
	// 서비스 종료 시 호출
	public void disconnect() {
		System.out.println("close: " + url);
	}
	
	public void init() {
		System.out.println("NetworkClient.init");
		connect();
		call("초기화 연결 메시지");
	}
	
	public void close() {
		System.out.println("NetworkClient.destroy");
		disConnect();
	}
}
package hello.core.lifecycle;

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
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(LifeCyvcleConfig.class);
		NetworkClient client = ac.getBean(NetworkClient.class);
		ac.close();  // 스프링 컨테이너를 종료, ConfigurableApplicationContext 필요
	}
	
	@Configuration
	static class LifeCycleConfig {
		
		// 이런 식으로 초기화, 소멸 메서드를 지정할 수 있다.
		@Bean(initMethod = "init", destroyMethod = "close")
		public NetworkClient networkClient() {
			NetworkClient networkClient = new NetworkClient();
			networkClient.setUrl("http://hello-spring.dev");
			return networkClient;
		}
	}
}

 

보다시피 메서드 이름을 자유롭게 지을 수 있고, NetWorkClient 코드를 보면 더 이상 스프링 빈이 스프링 코드에 의존하지 않는다. 그리고 가장 큰 장점은 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다는 점이다.

 

그리고 @BeandestroyMethod 속성의 기본값은 AbstractBeanDefinition.INFER_METHOD로 되어 있다. 즉, (inferred) 추론으로 등록되어 있다. 외부 라이브러리는 대부분 close, shutdown이라는 이름의 종료 메서드를 사용한다. 이때 (inferred) 추론은 close, shutdown이라는 이름의 메서드를 자동으로 호출해주는 기능을 갖고 있다. 따라서 직접 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 잘 동작한다.


🎆 @PostConstruct, @PreDestroy (권장)

결론부터 말하면 이 방법을 쓰면 된다. 스프링에서도 그냥 대놓고 이 방법을 권장하고 있다. 바로 코드를 보자.

package hello.core.lifecycle;

import javax.annotation.PostConstruct
import javax.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);
	}
	
	public void call(String message) {
		System.out.println("call: " + url + "message = " + message);
	}
	
	// 서비스 종료 시 호출
	public void disconnect() {
		System.out.println("close: " + url);
	}
	
	@PostConstruct
	public void init() {
		System.out.println("NetworkClient.init");
		connect();
		call("초기화 연결 메시지");
	}
	
	@PreDestroy
	public void close() {
		System.out.println("NetworkClient.destroy");
		disConnect();
	}
}

이렇게 진짜 애노테이션만 붙여주면 끝이다.

 

package hello.core.lifecycle;

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
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(LifeCyvcleConfig.class);
		NetworkClient client = ac.getBean(NetworkClient.class);
		ac.close();  // 스프링 컨테이너를 종료, ConfigurableApplicationContext 필요
	}
	
	@Configuration
	static class LifeCycleConfig {
		
		@Bean
		public NetworkClient networkClient() {
			NetworkClient networkClient = new NetworkClient();
			networkClient.setUrl("http://hello-spring.dev");
			return networkClient;
		}
	}
}

 

여기서 중요한 점은 패키지다. 지금 javax.annotation.* 인데, 이 javax로 시작하면 자바 진영에서 공식적으로 지원하는 것이다. 그래서 꼭 스프링이 아니라 혹시라도 만약 다른 컨테이너를 쓴다고 하더라도 그대로 적용이 된다.

 

<@PostConstruct, @PreDestroy 애노테이션 특징>

  • 최신 스프링에서 가장 권장하는 방법이다.

  • 애노테이션 하나만 붙이면 되니까 엄청 편리하다.

  • 패키지를 잘 보면, javax.annotation.PostConstruct이다. 스프링에 종속적인 기술이 아니라 JSR-250라는 자바 표준이다. 따라서 스프링이 아닌 다른 컨테이너에서도 동작한다.

  • 컴포넌트 스캔과 잘 어울린다.

  • 유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다. 외부 라이브러리를 초기화, 종료 해야 하면 @BeaninitMethoddestroyMethod 속성을 사용하도록 하자.

 

<정리>

  • @PostConstruct, @PreDestroy 애노테이션을 사용하자.

  • 코드를 고칠 수 없는 라이브러리를 초기화, 종료해야 하면 @BeaninitMethod, destroyMethod를 사용하자.

0개의 댓글