💡 본 게시글은 김영한님의 인프런(Inflearn) 강의 스프링 핵심 원리 - 기본편에 대해 공부하고, 정리한 내용입니다.
1. 빈 생명주기 콜백 시작
1) 빈 생명주기 콜백의 필요성
(1) 목적:
- 스프링 빈은 객체의 생성과 소멸 과정에서 특정 로직을 실행할 필요가 있습니다. 이러한 로직은 자원을 할당하거나 해제하는 데 중요할 수 있습니다.
(2) 사용 사례:
- 예를 들어, 데이터베이스 커넥션 풀은 애플리케이션 시작 시점에 데이터베이스와의 연결을 미리 맺고, 애플리케이션 종료 시점에 이러한 연결을 안전하게 종료해야 합니다.
- 네트워크 소켓도 마찬가지로, 애플리케이션 실행 중에는 유지되어야 하며, 종료 시점에 적절하게 닫혀야 합니다.
(3) 이유:
- 이러한 작업들은 자원의 효율적 사용과 메모리 누수 방지, 연결 오류 방지 등을 위해 필수적입니다.
2) 예제: NetworkClient 클래스
- 이제 스프링을 통한 초기화와 종료 작업의 예제를 설명드리겠습니다. 이 예제에서는 스프링 빈의 생명주기에 맞춰 특정한 작업(여기서는 네트워크 연결 및 해제)을 실행하는 방법을 다룹니다.
(1) 클래스 정의
- NetworkClient는 외부 네트워크에 연결하는 가상의 객체입니다. 실제 네트워크 연결 대신 문자 출력을 통해 작업을 시뮬레이션합니다.
- 생성자에서 네트워크 연결(connect)과 메시지 전송(call)을 수행합니다.
- 종료 메서드(disconnect)에서는 네트워크 연결을 해제합니다.
(2) NetworkClient 코드
package hello.core.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);
}
public void call(String message) {
System.out.println("call: " + url + " message = " + message);
}
//서비스 종료시 호출
public void disconnect() {
System.out.println("close: " + url);
}
}
3) 예제: 스프링 환경 설정 및 실행
(1) 스프링 환경 설정
- 스프링 설정 클래스(LifeCycleConfig)에서 NetworkClient 빈을 정의합니다.
- networkClient 빈을 생성하고 URL을 설정한 후 반환합니다.
(2) 스프링 실행 및 종료
- 테스트 메서드(lifeCycleTest)에서 스프링 컨테이너를 생성하고 NetworkClient 빈을 가져옵니다.
- 컨테이너를 종료(close)할 때 NetworkClient의 연결 종료 로직이 수행되어야 합니다.
(3) 스프링 설정 및 테스트 코드
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;
}
}
}
4) 예제: NetworkClient 실행 결과 분석
(1) 실험 과정
- NetworkClient 객체의 생성자가 호출될 때, url은 아직 초기화되지 않은 상태(null)입니다.
- 이 때, 생성자 내부에서 connect()와 call() 메소드를 호출합니다.
- 결과적으로, 이 메소드들은 null인 url로 작업을 수행하게 됩니다.
(2) 실행 결과
- 생성자에서 url이 null인 상태에서 connect()와 call() 메소드가 호출되는 것을 확인할 수 있습니다.
생성자 호출, url = null
connect: null
call: null message = 초기화 연결 메시지
(3) 문제점
- NetworkClient 객체의 생성 과정
- 객체 생성 시점에는 url이 설정되지 않습니다. 즉, NetworkClient는 아직 외부 리소스와의 연결을 위한 필요한 정보(url)를 갖고 있지 않습니다.
- 의존성 주입의 지연
- 스프링은 객체를 생성한 후, 필요한 의존성(여기서는 url)을 주입합니다.
- NetworkClient의 경우, setUrl()이라는 수정자 메소드(setter)를 통해 url이 주입되어야 합니다.
- 그러나 생성자 로직에서는 이러한 의존성 주입이 이루어지기 전이므로 url이 null인 상태입니다.
- 문제의 핵심
- 생성자 내에서 필요한 의존성이 주입되기 전에 connect()와 call() 같은 메소드를 호출하는 것은 안정성 측면에서 문제가 있습니다.
- 이렇게 되면, 객체가 완전히 초기화되지 않은 상태에서의 로직 실행이 발생하여 예상치 못한 오류나 문제를 발생시킬 수 있습니다.
5) 스프링 빈의 라이프사이클
(1) 객체 생성 및 의존관계 주입
- 스프링 컨테이너는 빈(Bean) 객체를 생성하고, 이후 의존관계 주입을 진행합니다.
- 의존관계 주입은 생성자 주입, 수정자 주입(setter 주입), 필드 주입 등 다양한 방법으로 수행될 수 있습니다.
(2) 초기화 콜백
- 의존관계 주입이 완료된 후, 빈은 초기화를 위한 작업을 수행해야 할 수 있습니다.
- 스프링은 이 시점을 알려주기 위해 초기화 콜백 메커니즘을 제공합니다.
- 초기화 콜백은 빈이 생성되고 모든 의존관계가 주입된 후에 실행됩니다.
(3) 소멸전 콜백
- 애플리케이션 종료 과정에서 빈은 소멸 전에 필요한 정리 작업을 수행할 수 있습니다.
- 스프링은 소멸전 콜백을 통해 빈의 정리 및 소멸 작업을 안내합니다.
6) 스프링 빈의 이벤트 라이프사이클
- 스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸전 콜백 → 스프링 컨테이너 종료
- 초기화 콜백은 빈의 생명주기에서 중요한 지점인 "의존관계 주입 완료" 후에 호출됩니다.
- 소멸전 콜백은 빈이 소멸되기 직전, 즉 스프링 컨테이너가 종료될 때 호출됩니다.
※ 참고: 객체 생성과 초기화의 분리
(1) 생성자의 역할:
- 생성자는 객체를 생성하는 책임을 가집니다. 이는 주로 필수 정보(파라미터)를 받고, 메모리에 객체를 할당하는 과정을 포함합니다.
- 생성자는 최소한의 설정을 통해 객체를 안전한 상태로 만드는 역할을 수행합니다.
(2) 초기화의 역할:
- 초기화 과정은 생성된 객체가 실제 사용될 준비를 하는 단계입니다. 이는 보통 객체가 다루는 외부 리소스(데이터베이스 커넥션, 네트워크 소켓 등)와의 연결을 설정하는 등의 "무거운" 작업을 포함할 수 있습니다.
- 초기화 과정은 객체가 생성된 후에 수행됩니다. 이는 외부 설정이나 의존성 주입이 필요한 경우에 특히 중요합니다.
(3) 분리의 중요성:
- 생성자 내에서 복잡하고 무거운 초기화 작업을 수행하면, 객체 생성 과정 자체가 복잡해지고 오류가 발생할 가능성이 증가합니다.
- 반면, 객체 생성과 초기화를 명확히 분리함으로써 각 단계의 복잡성을 관리하고, 오류 발생 시 디버깅이 용이해집니다.
※ 참고: 싱글톤 빈의 소멸 과정
(1) 싱글톤 빈의 생명주기
- 싱글톤 빈은 스프링 컨테이너의 시작과 함께 생성되고, 컨테이너의 종료와 함께 소멸됩니다.
- 이는 싱글톤 스코프를 가진 빈들이 스프링 컨테이너의 생명주기를 따른다는 것을 의미합니다.
(2) 소멸전 콜백
- 스프링 컨테이너가 종료되기 직전, 싱글톤 빈들에 대한 소멸전 콜백이 실행됩니다.
- 이 콜백은 빈의 정리 작업이나 외부 리소스 해제 등의 종료 작업을 위해 사용됩니다.
(3) 콜백 시점
- 소멸전 콜백은 스프링 컨테이너가 실제로 종료되기 바로 직전에 호출됩니다.
- 이는 싱글톤 빈이 컨테이너와 함께 존재하며, 컨테이너의 생명주기에 연동되어 있음을 의미합니다.
(4) 빈의 다양한 생명주기
- 스프링에서는 싱글톤 외에도 다양한 스코프(예: 프로토타입, 요청, 세션 스코프 등)를 가진 빈들이 있습니다.
- 이러한 빈들은 싱글톤과 다르게 더 짧은 생명주기를 가지며, 이에 따라 소멸 시점도 달라집니다.
- 각 스코프에 따라 빈의 소멸 시점과 방식이 다르며, 이는 스프링의 '스코프' 개념에서 더 자세히 다루어집니다.
2. 라이프사이클 콜백
- 스프링 프레임워크는 빈의 생명주기 동안 특정 작업을 수행할 수 있도록 하는 라이프사이클 콜백을 제공합니다. 스프링은 빈이 생성되고 소멸되는 시점에 각각 초기화와 소멸 작업을 수행할 수 있는 세 가지 주요 방법을 지원합니다.
1) 인터페이스 InitializingBean, DisposableBean
(1) 예제 코드
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();
}
}
① 클래스 구조:
- NetworkClient 클래스는 외부 네트워크 연결을 관리하는 가상의 클래스입니다.
- url 변수: 네트워크 연결 URL을 저장합니다.
- connect(): 네트워크에 연결합니다.
- call(String message): 네트워크를 통해 메시지를 전송합니다.
- disConnect(): 네트워크 연결을 해제합니다.
② 생성자:
- 생성자에서는 url이 아직 할당되지 않았기 때문에 null입니다. 이 시점에서 connect()나 call() 메서드를 호출해도 의미 있는 동작을 하지 못합니다.
③ InitializingBean, DisposableBean 인터페이스 구현:
- afterPropertiesSet(): 모든 프로퍼티 설정이 완료된 후, 즉 의존관계 주입이 끝난 후에 호출됩니다. 여기서 네트워크 연결(connect)과 메시지 전송(call)을 수행합니다.
- destroy(): 스프링 컨테이너가 종료될 때 호출되며, 네트워크 연결 해제(disConnect)를 수행합니다.
(2) 출력 결과 상세 분석
- 초기화 과정:
- 생성자 호출 시 url은 null입니다. 이는 의존성 주입 전이므로 url이 설정되지 않았음을 의미합니다.
- afterPropertiesSet()에서 connect()와 call()이 호출됩니다. 이 시점에서는 url이 이미 설정되어 있어 올바른 네트워크 연결과 메시지 전송이 이루어집니다.
- 소멸 과정:
- 스프링 컨테이너 종료 시 destroy() 메서드가 호출됩니다. 이 메서드 내에서 disConnect()가 호출되어 네트워크 연결이 안전하게 해제됩니다.
(3) 인터페이스 방식의 단점 및 대안
- 단점:
- 스프링 전용 인터페이스에 의존적입니다.
- 메서드 이름 변경 불가능.
- 외부 라이브러리에 적용하기 어렵습니다.
- 현대적 대안:
- @PostConstruct, @PreDestroy 애노테이션을 사용합니다. 이들은 자바 표준 방식으로 더 깔끔하고 유연한 코드 작성을 가능하게 합니다.
- 이 방식은 스프링이 아닌 다른 컨테이너에서도 사용할 수 있으며, 사용자 정의 메서드 이름을 자유롭게 지정할 수 있습니다.
2) 빈 등록 초기화, 소멸 메서드 지정
- 스프링에서는 @Bean 애노테이션을 사용하여 초기화와 소멸 메서드를 명시적으로 지정할 수 있습니다. 이 방법은 스프링 코드에 의존하지 않으며, 메서드 이름을 자유롭게 지정할 수 있는 장점이 있습니다.
(1) 예제 코드 - NetworkClient: 설정 정보를 사용하도록 변경
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.close");
disConnect();
}
}
① 클래스 정의 및 메서드:
- NetworkClient는 네트워크 연결을 관리하는 클래스입니다.
- connect() 메서드는 네트워크 연결을 설정하고, call(String message) 메서드는 메시지를 전송합니다.
- disConnect() 메서드는 네트워크 연결을 해제합니다.
- init() 메서드는 초기화 작업을, close() 메서드는 소멸 작업을 담당합니다.
② 초기화 및 소멸 메서드:
- init(): 객체 생성 후 초기화를 위한 메서드로, 여기에서 네트워크 연결을 설정합니다.
- close(): 객체 소멸 전에 호출되는 메서드로, 네트워크 연결을 해제합니다.
(2) 예제 코드 - LifeCycleConfig: 설정 정보를 사용하도록 변경
@Configuration
static class LifeCycleConfig {
@Bean(initMethod = "init", destroyMethod = "close")
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hello-spring.dev");
return networkClient;
}
}
① @Bean 애노테이션:
- @Bean(initMethod = "init", destroyMethod = "close")를 사용하여 초기화와 소멸 메서드를 지정합니다.
- 이 방식은 메서드 이름을 자유롭게 지정할 수 있으며, 스프링 코드에 의존하지 않습니다.
② 설정 클래스:
- LifeCycleConfig 설정 클래스에서 networkClient 빈을 정의하고, 초기화 및 소멸 메서드를 지정합니다.
(2) 예제 출력 결과
생성자 호출, url = null
NetworkClient.init
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 초기화 연결 메시지
13:33:10.029 [main] DEBUG
org.springframework.context.annotation.AnnotationConfigApplicationContext -
Closing NetworkClient.close
close + http://hello-spring.dev
① 초기화 과정:
- "생성자 호출, url = null"은 객체가 생성된 후 url이 설정되기 전임을 나타냅니다.
- NetworkClient.init는 초기화 메서드가 호출되었음을 나타내며, 이때 네트워크 연결이 설정됩니다.
② 소멸 과정:
- 스프링 컨테이너 종료 시 NetworkClient.close는 소멸 메서드가 호출되었음을 나타내며, 네트워크 연결이 해제됩니다.
(3) 설정 정보 사용의 특징
- 메서드 이름 자유롭게 지정:
- 초기화 및 소멸 메서드의 이름을 개발자가 자유롭게 지정할 수 있습니다.
- 스프링 코드 의존성 없음:
- NetworkClient 클래스는 스프링 전용 인터페이스나 애노테이션에 의존하지 않습니다.
- 외부 라이브러리 적용 용이:
- 코드를 직접 수정할 수 없는 외부 라이브러리에도 초기화 및 종료 메서드를 적용할 수 있습니다.
(4) 종료 메서드 추론
- destroyMethod 속성의 추론 기능:
- @Bean의 destroyMethod는 기본값이 (inferred)로 설정되어 있으며, close나 shutdown 같은 표준 메서드 이름을 가진 종료 메서드를 자동으로 호출합니다.
- 추론 기능을 사용하지 않으려면 destroyMethod=""로 설정하여 비활성화할 수 있습니다.
(5) 결론
- 스프링의 @Bean 애노테이션을 통한 초기화 및 소멸 메서드 지정은 유연하고 명확한 빈 생명주기 관리를 가능하게 합니다.
- 이 방법은 메서드 이름을 자유롭게 지정할 수 있고, 스프링 코드에 의존하지 않는 장점이 있습니다.
3) 애노테이션 @PostConstruct, @PreDestroy
- @PostConstruct와 @PreDestroy 애노테이션을 사용한 스프링 빈의 초기화 및 소멸 과정에 대해 자세하게 설명드리겠습니다. 이 예제에서는 NetworkClient 클래스를 사용하여 스프링 빈의 생명주기와 관련된 초기화와 소멸 작업을 어떻게 처리하는지 보여줍니다.
(1) 예제 코드 - NetworkClient
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.close");
disConnect();
}
}
① 클래스 구조:
- NetworkClient는 네트워크 연결을 관리하는 클래스입니다.
- url 변수: 네트워크 연결 URL을 저장합니다.
- connect(): 네트워크에 연결합니다.
- call(String message): 네트워크를 통해 메시지를 전송합니다.
- disConnect(): 네트워크 연결을 해제합니다.
② 생성자:
- 객체가 생성될 때 호출됩니다. 이 시점에서 url은 설정되지 않았으므로 null로 출력됩니다.
③ @PostConstruct와 @PreDestroy 애노테이션:
- @PostConstruct 애노테이션이 붙은 init() 메서드는 빈이 생성되고 의존성 주입이 완료된 후에 자동으로 호출됩니다. 여기서 네트워크 연결을 초기화합니다.
- @PreDestroy 애노테이션이 붙은 close() 메서드는 빈이 소멸되기 직전에 호출됩니다. 이 메서드는 네트워크 연결을 해제하는 작업을 수행합니다.
(2) 예제 코드 - LifeCycleConfig
@Configuration
static class LifeCycleConfig {
@Bean
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hello-spring.dev");
return networkClient;
}
}
(3) 실행 결과 분석
- 초기화 과정:
- "생성자 호출, url = null": 객체가 생성된 직후 상태를 나타냅니다.
- NetworkClient.init: @PostConstruct 애노테이션에 의해 초기화 메서드가 호출됩니다. 이 시점에 네트워크 연결이 설정됩니다.
- 소멸 과정:
- NetworkClient.close: 스프링 컨테이너가 종료되면서 @PreDestroy 애노테이션이 붙은 소멸 메서드가 호출됩니다. 네트워크 연결이 해제됩니다.
(4) @PostConstruct, @PreDestroy 애노테이션의 특징
- 자바 표준 애노테이션:
- @PostConstruct와 @PreDestroy는 javax.annotation 패키지에 속해 있습니다. 이들은 JSR-250 스펙의 일부로, 자바 표준 애노테이션입니다.
- 이는 스프링뿐만 아니라 다른 자바 기반 컨테이너에서도 사용될 수 있음을 의미합니다.
- 편리한 사용법:
- 애노테이션 하나만 클래스의 메서드에 붙이면 되므로 사용이 매우 편리합니다.
- @PostConstruct는 모든 의존관계가 주입된 후 실행되어야 하는 초기화 로직에 사용됩니다.
- @PreDestroy는 빈이 소멸되기 전에 필요한 정리 작업에 사용됩니다.
- 스프링과의 호환성:
- 이 애노테이션들은 스프링의 컴포넌트 스캔과 잘 어울립니다. 클래스에 @Component 애노테이션과 함께 사용하여 스프링 빈의 생명주기를 쉽게 관리할 수 있습니다.
- 외부 라이브러리에는 적용 불가:
- @PostConstruct와 @PreDestroy는 개발자가 직접 작성한 클래스에만 적용할 수 있습니다. 외부 라이브러리에 이 애노테이션을 직접 추가할 수는 없습니다.
- 외부 라이브러리에 초기화와 종료 로직을 적용하려면, @Bean 애노테이션을 사용한 설정 방식을 활용해야 합니다. 이 방식을 사용하면 초기화 및 소멸 메서드를 외부 라이브러리에도 적용할 수 있습니다.
(5) 결론
- @PostConstruct와 @PreDestroy 애노테이션은 스프링 빈의 초기화와 소멸 과정을 간편하게 관리할 수 있게 해주는 자바 표준 애노테이션입니다.
- 이들은 스프링에 종속적이지 않으며, 다른 컨테이너에서도 동작할 수 있는 큰 장점을 가지고 있습니다.
- 하지만 외부 라이브러리에는 직접 적용할 수 없기 때문에, 이 경우 @Bean을 사용한 설정 방식으로 대체해야 합니다.
3. 총 정리
1) 일반적인 스프링 애플리케이션 개발에서는 빈의 초기화와 소멸에 @PostConstruct와 @PreDestroy 애노테이션을 권장합니다. 이는 간결하고 명확한 방식으로 빈의 생명주기를 관리할 수 있게 해줍니다.
※ @PostConstruct와 @PreDestroy 애노테이션 사용
- 이들 애노테이션은 자바 표준 방법으로, 스프링이 아닌 다른 자바 기반 컨테이너에서도 사용될 수 있습니다.
- @PostConstruct는 빈의 모든 의존성이 주입된 후에 필요한 초기화 작업을 위해 사용됩니다.
- @PreDestroy는 빈이 소멸되기 전에 필요한 정리 작업을 위해 사용됩니다.
- 이 방법은 코드에 직접 애노테이션을 추가하는 방식이므로, 개발자가 작성한 클래스에만 적용할 수 있습니다.
2) 외부 라이브러리를 스프링 빈으로 사용하는 경우, 즉 코드를 직접 수정할 수 없는 경우에는 @Bean의 initMethod와 destroyMethod 속성을 활용하여 빈의 초기화와 소멸 메서드를 지정합니다. 이는 빈의 생명주기를 외부에서 제어할 수 있는 유연성을 제공합니다.
※ @Bean의 initMethod와 destroyMethod 사용
- 이 방법은 스프링 설정 클래스에서 빈을 등록할 때 사용됩니다.
- initMethod 속성은 빈의 초기화를 위한 메서드를 지정하는 데 사용됩니다.
- destroyMethod 속성은 빈의 소멸 과정에서 호출될 메서드를 지정하는 데 사용됩니다.
- @Bean 방식은 외부 라이브러리와 같이 코드를 수정할 수 없는 경우에 특히 유용합니다.
'Spring > 스프링 핵심 원리 - 기본' 카테고리의 다른 글
09. 빈 스코프 (작성 중!) (1) | 2024.02.07 |
---|---|
07. 의존관계 자동 주입 (1) | 2024.01.29 |
06. 컴포넌트 스캔 (0) | 2024.01.26 |
05. 싱글톤 컨테이너 (0) | 2024.01.20 |
04. 스프링 컨테이너와 스프링 빈 (0) | 2024.01.17 |