스프링에서 빈은 단순히 new로 만든 객체가 아니다.
컨테이너가 객체를 생성하고, 의존성을 주입하고, 필요한 후처리를 거쳐 사용할 수 있는 상태로 만든 뒤, 종료 시점에는 정리까지 담당하는 관리 대상이다.
그래서 빈의 생명주기를 이해할 때는 @PostConstruct, @PreDestroy만 따로 볼 것이 아니라, 생성 → 주입 → 초기화 → 후처리 → 사용 → 소멸이라는 전체 흐름으로 보는 편이 훨씬 자연스럽다.
스프링에서 빈은 컨테이너가 생성하고 관리하는 객체다.
같은 클래스라도 내가 직접 new 해서 만들면 그냥 일반 객체이고, 스프링이 설정 정보를 바탕으로 생성하고 관리하면 빈이 된다.
즉, 빈의 핵심은 객체 그 자체 보다 누가 만들고 관리하느냐에 있다.
스프링 빈의 라이프사이클은 크게 다음 순서로 이해하면 된다.
Aware 콜백BeanPostProcessor 초기화 전 처리BeanPostProcessor 초기화 후 처리이 과정 속에서 @PostConstruct가 어디쯤 동작하는지, 프록시는 언제 적용되는지, @PreDestroy는 언제 호출되는지 알 수 있다.
가장 먼저 스프링은 어떤 빈을 만들지에 대한 정보를 읽는다.
이 정보는 @Component, @Service, @Repository, @Configuration, @Bean 같은 설정에서 온다.
이 시점에는 아직 객체가 만들어진 것이 아니다.
어떤 클래스를 빈으로 등록할지, 스코프는 무엇인지, 초기화 메서드나 소멸 메서드는 있는지 같은 설계가 준비되는 단계다.
그다음 스프링은 실제 객체를 생성한다.
기본적으로는 생성자를 사용하고, 경우에 따라서는 팩토리 메서드를 사용할 수도 있다.
이 단계에서 중요한 것은, 스프링이 객체를 그냥 만들기만 하는 것이 아니라 등록된 빈 정의에 따라 관리 가능한 상태로 생성한다는 점이다.
객체가 만들어지면 필요한 의존성을 넣는다.
생성자 주입, setter 주입, 필드 주입 같은 방식이 여기에 해당한다.
예를 들어 아래 코드에서는 OrderService가 PaymentService를 필요로 한다.
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
여기서 OrderService는 “나는 PaymentService가 필요하다”만 드러낸다.
실제로 PaymentService를 생성하고 연결하는 일은 스프링 컨테이너가 맡는다.
의존성 주입이 끝나면, 필요할 경우 Aware 계열 콜백이 호출된다.
대표적으로 BeanNameAware, BeanFactoryAware, ApplicationContextAware 등이 있다.
이 단계는 빈이 스프링 컨테이너의 정보에 직접 접근할 수 있게 되는 구간이라고 보면 된다.
예를 들어 ApplicationContextAware를 구현하면 컨테이너 자신을 전달받을 수 있다.
@Component
public class MyBean implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
}
그다음은 BeanPostProcessor가 개입한다.
이 인터페이스는 빈의 초기화 전과 후에 끼어들 수 있는 확장 포인트다.
즉, 스프링은 빈을 생성하고 주입한 뒤 곧바로 사용하는 것이 아니라,
중간에 후처리기를 통해 빈을 가공할 수 있다.
초기화 전 단계에서는 postProcessBeforeInitialization()이 호출된다.
이제 빈 자신의 초기화 로직이 실행된다.
여기서 많이 보는 것이 바로 다음 세 가지다.
@PostConstructInitializingBean.afterPropertiesSet()예를 들면 다음과 같다.
@Component
public class CacheManager {
@PostConstruct
public void init() {
System.out.println("초기화");
}
}
또는 이렇게도 가능하다.
@Component
public class CacheManager implements InitializingBean {
@Override
public void afterPropertiesSet() {
System.out.println("초기화");
}
}
여러 방식을 같이 쓰면 순서는 다음과 같다.
@PostConstructafterPropertiesSet()즉, 초기화 콜백도 아무렇게나 호출되는 것이 아니라 정해진 순서가 있다.
초기화가 끝나면 postProcessAfterInitialization()이 호출된다.
이 단계가 중요한 이유는 여기서 프록시가 만들어질 수 있기 때문이다.
즉, 우리가 최종적으로 주입받는 객체는 원본 빈일 수도 있지만,
경우에 따라서는 후처리를 거쳐 감싸진 프록시 객체일 수도 있다.
@Transactional, @Async 같은 기능을 떠올리면 이해하기 쉽다.
여기까지 오면 빈은 애플리케이션에서 실제로 사용 가능한 상태가 된다.
컨트롤러가 서비스를 주입받고, 서비스가 리포지토리를 주입받아 사용하는 일반적인 흐름이 바로 이 단계다.
즉, 우리가 평소 보는 스프링 애플리케이션의 협력 구조는
앞선 여러 단계를 거쳐 준비된 빈들이 연결된 결과다.
컨테이너가 종료되면 빈도 정리 단계에 들어간다.
이때 호출되는 것이 소멸 콜백이다.
대표적인 방식은 다음과 같다.
@PreDestroyDisposableBean.destroy()예를 들면 다음과 같다.
@Component
public class ConnectionManager {
@PreDestroy
public void close() {
System.out.println("리소스 정리");
}
}
여러 방식을 함께 사용하면 소멸 순서는 다음과 같다.
@PreDestroydestroy()즉, 종료 단계 역시 스프링이 정해진 순서로 관리한다.
핵심만 다시 정리하면 이렇다.
스프링은 객체를 만들고 끝나는 것이 아니라,
주입, 초기화, 후처리, 소멸까지 관리한다.
@PostConstruct, @PreDestroy는 부가 기능이 아니라
빈 라이프사이클의 정식 단계다.
그래서 초기화 메서드와 최종적으로 사용되는 프록시 객체를 같은 시점으로 보면 안 된다.
우리가 실제로 사용하는 객체는 원본 객체 그대로일 수도 있고,
프록시가 감싼 형태일 수도 있다.
전체적인 흐름은 다음과 같다.
빈 정의 등록
→ 객체 생성
→ 의존성 주입
→ Aware 콜백
→ BeanPostProcessor beforeInitialization
→ @PostConstruct
→ afterPropertiesSet()
→ custom init-method
→ BeanPostProcessor afterInitialization
→ 빈 사용
→ @PreDestroy
→ destroy()
→ custom destroy-method
이 흐름을 알고 있으면 스프링이 왜 단순 객체 생성기가 아니라
객체의 전체 생명주기를 관리하는 컨테이너인지 더 분명하게 보인다.
빈의 생명주기를 이해하면 @PostConstruct, @PreDestroy 같은 애너테이션도 훨씬 맥락 있게 보인다.
또 왜 후처리기가 필요한지, 왜 프록시가 중요한지, 왜 빈이 단순 객체와 다른지도 자연스럽게 연결된다.
면접에서도 나오는 질문으로 알고있으니 다음과 같은 흐름으로 기억하면 좋을 듯 싶다.
등록되고, 생성되고, 주입되고, 초기화되고, 후처리되고, 사용되다가, 마지막에 정리된다.