Spring을 사용하다 보면 "Bean이 언제 생성되고 언제 사라지는 걸까?"라는 궁금증이 생길 때가 있습니다. 특히 데이터베이스 연결이나 네트워크 소켓처럼 애플리케이션 시작 시 미리 연결해두고, 종료 시 안전하게 끊어야 하는 리소스를 다룰 때 이 생명주기를 정확히 이해하는 것이 중요합니다.
처음 Spring을 배울 때는 그냥 @Component나 @Service를 붙이면 알아서 다 해주겠거니 했는데, 실제로 운영 환경에서 리소스 누수 문제를 겪고 나서야 생명주기의 중요성을 깨닫게 되었습니다.
Spring Bean의 생명주기란?
Spring Bean은 스프링 컨테이너가 관리하는 자바 객체입니다. 일반적인 자바 객체는 new 키워드로 생성하고 참조가 사라지면 GC(Garbage Collector)가 알아서 정리하지만, Spring Bean은 스프링 컨테이너가 생성부터 소멸까지 전 과정을 관리합니다.
이 과정을 사람의 일생에 비유하면 이해하기 쉽습니다. 사람이 태어나서(생성), 교육을 받고(의존관계 주입), 성인이 되어 일을 시작하기 전 준비를 하고(초기화), 열심히 살다가(사용), 은퇴 준비를 하고(소멸 전 정리), 생을 마감하는(소멸) 것과 비슷합니다.
생명주기의 전체 흐름
Spring Bean의 생명주기는 다음과 같은 순서로 진행됩니다:
- 스프링 컨테이너 생성
- 스프링 빈 생성 (생성자 호출)
- 의존관계 주입 (DI)
- 초기화 콜백 호출
- 사용 (실제 비즈니스 로직 수행)
- 소멸 전 콜백 호출
- 스프링 종료
각 단계를 코드와 함께 자세히 살펴보겠습니다.
1단계: 스프링 컨테이너와 빈 생성
스프링 애플리케이션이 시작되면 먼저 스프링 컨테이너(ApplicationContext)가 생성됩니다. 컨테이너는 설정 정보를 읽어서 어떤 Bean을 생성해야 하는지 파악하고, 해당 클래스의 생성자를 호출하여 객체를 만듭니다.
아래 코드는 생성자와 setter가 어떤 순서로 호출되는지 확인하기 위한 예제입니다. 각 메서드에 출력문을 넣어서 호출 시점을 눈으로 확인할 수 있게 했습니다.
@Component
public class DatabaseConnection {
private String url;
private Connection connection;
public DatabaseConnection() {
System.out.println("1. 생성자 호출 - 객체 생성");
}
@Value("${database.url}")
public void setUrl(String url) {
this.url = url;
System.out.println("2. 의존관계 주입 - url 설정: " + url);
}
}
출력 결과를 보면 "1. 생성자 호출"이 먼저 나오고, 그 다음에 "2. 의존관계 주입"이 나옵니다. 여기서 중요한 점은 생성자 호출 시점에는 아직 의존관계 주입이 완료되지 않았다는 것입니다. 즉, 생성자 안에서 url 값을 사용하려고 하면 null이 나올 수 있습니다.
2단계: 의존관계 주입 (Dependency Injection)
빈 객체가 생성된 후, 스프링은 @Autowired, @Value, setter 주입 등을 통해 필요한 의존관계를 주입합니다.
앞서 1단계에서는 setter 주입 방식을 보여드렸는데, 이번에는 생성자 주입 방식의 예제를 살펴보겠습니다. 생성자 주입은 객체 생성과 의존관계 주입이 동시에 일어난다는 점에서 setter 주입과 다릅니다.
@Component
public class UserService {
private final UserRepository userRepository;
// 생성자 주입 - 객체 생성과 동시에 의존관계 주입
// Spring 4.3부터 생성자가 1개면 @Autowired 생략 가능
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
System.out.println("UserService 생성 + 의존관계 주입 완료");
}
}
위 코드에서 UserService는 생성자의 파라미터로 UserRepository를 받습니다. 스프링이 UserService 객체를 만들 때 생성자를 호출하면서 동시에 UserRepository를 넣어주기 때문에, 객체가 생성된 시점에 이미 모든 의존관계가 준비되어 있습니다. 이것이 생성자 주입이 권장되는 이유 중 하나입니다.
3단계: 초기화 콜백 - @PostConstruct
의존관계 주입이 모두 완료되면, 스프링은 초기화 콜백을 호출합니다. 이 시점에서 외부 리소스 연결, 초기 데이터 로딩 등의 작업을 수행할 수 있습니다.
@PostConstruct 사용법
가장 간편하고 권장되는 방식은 @PostConstruct 어노테이션을 사용하는 것입니다.
아래 코드는 1단계에서 본 DatabaseConnection에 @PostConstruct를 추가한 버전입니다. 이제 생성자 → setter → 초기화 콜백 순서로 3단계가 모두 출력됩니다.
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
@Component
public class DatabaseConnection {
private String url;
private Connection connection;
public DatabaseConnection() {
System.out.println("1. 생성자 호출");
}
@Value("${database.url}")
public void setUrl(String url) {
this.url = url;
System.out.println("2. 의존관계 주입 - url: " + url);
}
@PostConstruct
public void init() {
System.out.println("3. 초기화 콜백 - 데이터베이스 연결 시작");
// 실제 데이터베이스 연결 로직
// this.connection = DriverManager.getConnection(url, username, password);
System.out.println(" 데이터베이스 연결 완료!");
}
}
실행 결과:
1. 생성자 호출
2. 의존관계 주입 - url: jdbc:mysql://localhost:3306/mydb
3. 초기화 콜백 - 데이터베이스 연결 시작
데이터베이스 연결 완료!
왜 생성자에서 초기화하면 안 될까?
"그냥 생성자에서 연결하면 되는 거 아닌가?"라고 생각할 수 있습니다. 하지만 앞서 말했듯이 생성자 호출 시점에는 의존관계 주입이 완료되지 않았을 수 있습니다. 특히 setter 주입이나 필드 주입을 사용하는 경우, 생성자에서 주입된 값을 사용하면 NullPointerException이 발생할 수 있습니다.
// 잘못된 예시 - 이렇게 하면 안 됩니다
@Component
public class BadExample {
@Value("${database.url}")
private String url;
public BadExample() {
// 이 시점에 url은 아직 null입니다!
System.out.println("url: " + url); // null 출력
connect(url); // NullPointerException 발생 가능
}
}
4단계: 소멸 전 콜백 - @PreDestroy
애플리케이션이 종료되기 전, 스프링은 소멸 전 콜백을 호출합니다. 이 시점에서 데이터베이스 연결 해제, 파일 핸들 닫기, 스레드 풀 종료 등의 정리 작업을 수행합니다.
아래 코드는 @PreDestroy를 사용해서 애플리케이션 종료 시 데이터베이스 연결을 안전하게 끊는 예제입니다. @PostConstruct에서 연결을 열고, @PreDestroy에서 연결을 닫는 패턴이 실무에서 자주 사용됩니다.
@Component
public class DatabaseConnection {
private Connection connection;
@PostConstruct
public void init() {
System.out.println("초기화: 데이터베이스 연결");
// 연결 로직
}
@PreDestroy
public void cleanup() {
System.out.println("소멸 전 콜백: 데이터베이스 연결 해제");
if (connection != null) {
try {
connection.close();
System.out.println("연결 해제 완료");
} catch (SQLException e) {
System.err.println("연결 해제 중 오류 발생");
}
}
}
}
전체 흐름을 확인하는 예제
지금까지 각 단계를 따로따로 살펴봤는데, 이번에는 생성 → 주입 → 초기화 → 사용 → 소멸의 전체 흐름을 하나의 코드로 확인해 보겠습니다. 아래 예제를 실행하면 각 단계가 어떤 순서로 호출되는지 콘솔에서 직접 확인할 수 있습니다.
@Component
public class LifecycleExample {
private String name;
public LifecycleExample() {
System.out.println("[1] 생성자 호출 - 빈 객체 생성");
System.out.println(" name 값: " + name); // null
}
@Value("LifecycleBean")
public void setName(String name) {
this.name = name;
System.out.println("[2] setter 호출 - 의존관계 주입");
System.out.println(" name 값: " + name);
}
@PostConstruct
public void init() {
System.out.println("[3] @PostConstruct - 초기화 콜백");
System.out.println(" 초기화 작업 수행 (name: " + name + ")");
}
public void doSomething() {
System.out.println("[4] 비즈니스 로직 수행");
}
@PreDestroy
public void destroy() {
System.out.println("[5] @PreDestroy - 소멸 전 콜백");
System.out.println(" 정리 작업 수행");
}
}
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext context =
SpringApplication.run(Application.class, args);
LifecycleExample bean = context.getBean(LifecycleExample.class);
bean.doSomething();
context.close(); // 컨테이너 종료
}
}
실행 결과:
[1] 생성자 호출 - 빈 객체 생성
name 값: null
[2] setter 호출 - 의존관계 주입
name 값: LifecycleBean
[3] @PostConstruct - 초기화 콜백
초기화 작업 수행 (name: LifecycleBean)
[4] 비즈니스 로직 수행
[5] @PreDestroy - 소멸 전 콜백
정리 작업 수행
초기화/소멸 콜백의 다른 방법들
@PostConstruct와 @PreDestroy 외에도 초기화/소멸 콜백을 지정하는 방법이 있습니다.
방법 1: InitializingBean, DisposableBean 인터페이스
스프링이 제공하는 인터페이스를 구현하는 방식입니다. afterPropertiesSet()이 초기화 콜백, destroy()가 소멸 콜백 역할을 합니다.
@Component
public class ExampleBean implements InitializingBean, DisposableBean {
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("초기화 - InitializingBean");
}
@Override
public void destroy() throws Exception {
System.out.println("소멸 - DisposableBean");
}
}
이 방식은 스프링 전용 인터페이스에 의존하게 되므로 권장되지 않습니다.
방법 2: @Bean의 initMethod, destroyMethod 속성
외부 라이브러리 클래스처럼 소스 코드를 직접 수정할 수 없는 경우에 사용하는 방식입니다. @Bean 어노테이션에 초기화/소멸 메서드 이름을 지정해주면 됩니다.
public class ExternalLibraryClient {
public void connect() {
System.out.println("연결");
}
public void disconnect() {
System.out.println("연결 해제");
}
}
@Configuration
public class AppConfig {
@Bean(initMethod = "connect", destroyMethod = "disconnect")
public ExternalLibraryClient externalLibraryClient() {
return new ExternalLibraryClient();
}
}
이 방식은 외부 라이브러리 클래스처럼 코드를 수정할 수 없는 경우에 유용합니다.
참고: @Bean을 사용할 때 destroyMethod를 별도로 지정하지 않아도, 해당 클래스에 close나 shutdown이라는 이름의 메서드가 있다면 스프링이 자동으로 이를 추론하여 종료 시점에 호출해줍니다. 이 기능을 끄려면 destroyMethod = ""처럼 빈 문자열을 설정하면 됩니다.
권장 순서
- @PostConstruct, @PreDestroy (가장 권장)
- 간편하고, 스프링이 아닌 다른 컨테이너에서도 동작 (JSR-250 표준)
- @Bean의 initMethod, destroyMethod
- 외부 라이브러리에 적용할 때 사용
- InitializingBean, DisposableBean
- 스프링에 강하게 의존하므로 가급적 사용하지 않음
주의할 점
1. 싱글톤 빈과 프로토타입 빈의 차이
싱글톤 빈은 스프링 컨테이너가 종료될 때까지 살아있으므로 @PreDestroy가 정상적으로 호출됩니다. 하지만 프로토타입 빈은 스프링 컨테이너가 생성과 의존관계 주입까지만 관여하고 이후에는 관리하지 않습니다. 따라서 프로토타입 빈의 @PreDestroy는 호출되지 않습니다.
@Scope("prototype")
@Component
public class PrototypeBean {
@PreDestroy
public void destroy() {
// 이 메서드는 호출되지 않습니다!
System.out.println("프로토타입 빈 소멸");
}
}
따라서 프로토타입 빈이 데이터베이스 연결이나 파일 핸들 같은 리소스를 점유하고 있다면, 빈을 사용하는 클라이언트 코드에서 직접 정리 메서드를 호출해주어야 합니다. 그렇지 않으면 리소스 누수가 발생할 수 있습니다.
2. 초기화 작업은 가볍게
@PostConstruct에서 너무 무거운 작업을 수행하면 애플리케이션 시작 시간이 길어집니다. 꼭 필요한 초기화만 수행하고, 지연 로딩이 가능한 작업은 나중으로 미루는 것이 좋습니다.
정리
Spring Bean의 생명주기를 정리하면 다음과 같습니다:
- 스프링 컨테이너 생성: ApplicationContext가 생성되고 설정 정보를 읽음
- 빈 생성: 생성자를 호출하여 객체 생성
- 의존관계 주입: @Autowired, @Value 등을 통해 필요한 의존관계 주입
- 초기화 콜백: @PostConstruct 메서드 호출 - 외부 리소스 연결 등 수행
- 사용: 실제 비즈니스 로직에서 빈 사용
- 소멸 전 콜백: @PreDestroy 메서드 호출 - 리소스 정리 수행
- 스프링 종료: 컨테이너 종료와 함께 빈 소멸
초기화/소멸 콜백이 필요한 경우 @PostConstruct와 @PreDestroy를 사용하는 것이 가장 간편하고 권장되는 방식입니다. 다만, 외부 라이브러리를 사용하는 경우에는 @Bean의 initMethod와 destroyMethod 속성을 활용하면 됩니다.
'프레임워크 > 스프링' 카테고리의 다른 글
| DispatcherServlet의 역할: 요청이 들어와서 응답이 나갈 때까지의 여정 (1) | 2025.12.19 |
|---|---|
| DI(의존성 주입)과 IoC(제어의 역전): 왜 우리가 직접 new를 안 쓸까? (0) | 2025.12.18 |
| Spring_내용설명_02) Spring Framework의 Bean과 Container (0) | 2019.05.21 |
| Spring_내용설명_01) Spring Framework의 IoC(Inversion of Control)란 (0) | 2019.05.21 |
| 토비의 스프링 용어 정리 (0) | 2018.11.20 |