1.3 스프링 컨테이너와 Bean — 생명주기와 스코프
앞장의 제어의 역전(IoC)과 의존성 주입(DI) 개념이 "어떤 원리로 코딩해야 하는가(철학)" 였다면, 이를 실제로 구동하고 관리해 주는 물리적인 진짜 공장 기계 의 이름이 바로 스프링 컨테이너(Spring Container)입니다. 그리고 이 기계가 만들어 관리하는 모든 부품들을 빈(Bean) 이라고 부릅니다.
🏭 1. 스프링 컨테이너 (ApplicationContext)
ApplicationContext(애플리케이션 컨텍스트)라는 인터페이스가 바로 스프링 컨테이너의 핵심입니다.
개발자가 @Configuration 클래스에 @Bean을 등록해 두거나, 각 레이어의 컴포넌트(@Controller, @Service, @Repository) 어노테이션을 붙여놓으면, 스프링 부트가 실행될 때 이 거대한 애플리케이션 컨텍스트 공장장 이 클래스들을 스캔 한 번 쓱(돌리며) 탐색합니다.
수집한 클래스의 바이트코드를 토대로 실제 런타임 인스턴스(객체)를 new 연산자로 생성하여 컨테이너 메모리 내부 보관소 안의 "Bean 저장소 해시 맵(Key: 메서드명, Value: 객체)" 에 꽉 꽉 집어넣습니다.
// 개발자가 컨테이너에서 직접 Bean을 꺼내보는 원리 (실무에서는 이렇게 할 필요 없이 자동 주입됨)
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// "스프링아, 네 보관소(HashMap)에서 OrderService 타입 빙(Bean) 좀 꺼내다 줘"
OrderService orderService = context.getBean("orderService", OrderService.class);
orderService.createOrder();
이후 OrderService가 외부에서 MemberRepository 줘! 하고 애타게 찾으면, 컨테이너 창고에서 이 부품을 꺼내 무심하게 툭 꽂아주게 됩니다. (이것이 의존성 주입입니다.)
🥇 2. 싱글톤 스코프 (Singleton Scope)와 상태 무결성
만약 쇼핑몰에 고객 10,000명이 동시에 똑같이 접속하여 상품 조회 서비스(ItemService) 객체를 요구할 때, 톰캣과 자바 엔진이 매번 new ItemService()를 무식하게 1만 개 새로 뚝딱뚝딱 찍어내 메모리에 올리면 어떻게 될까요? 초당 수 기가바이트의 램(RAM)이 소모되고 가비지 컬렉터(GC)가 부하를 견디지 못하고 터져버릴 겁니다.
철학: 오직 단 하나만 띄운다 (싱글톤 패턴)
그래서 빈(Bean) 스코프(Scope)의 디폴트 기본값은 무조건 싱글톤(Singleton) 입니다.
스프링 컨테이너가 기동되는 순간 메모리에 ItemService 객체를 우주 상에 오직 단 1개 만 딱 만들어 놓습니다. 그리고 만 명이 찾아와도 똑같은 1개의 유일무이한 객체 레퍼런스(참조 주소)만을 계속 돌려쓰게(공유) 만듭니다. 극한의 성능 효율을 자랑하죠!
💥 상태를 금지하라! (Stateless 설계 강제)
객체를 만 명이 공유하므로, 싱글톤 객체 내부에는 절대 상태 유지형 필드(Stateful) 변수를 선언해서는 안 됩니다.
@Service
public class StatefulService {
// 💥 극강의 살인 버그: 이 필드를 수만 명이 동시에 공유하게 됨!!!
private int price;
public void order(String name, int price) {
System.out.println("name = " + name + " price = " + price);
this.price = price; // 여기서 클라이언트 A의 만원을 세팅했는데, 0.1초 뒤 클라이언트 B가 이 값을 이만 원으로 덮어씀
}
public int getPrice() {
return price; // 클라이언트 A는 자기 결제금액이 이만 원으로 변조된 기적을 겪음
}
}
모든 빈의 비즈니스 로직은 철저하게 필드 대신 메서드 '파라미터(매개변수)'와 '지역변수(스레드 내부 변수)', 'ThreadLocal'을 사용해서 무상태성(Stateless)으로 설계하는 것이 백엔드 엔지니어의 최우선 불문율입니다.
♻️ 3. Bean의 라이프사이클 (생명주기 콜백)
스프링 프레임워크가 부품(빈)을 생성하고 끝내는 파란만장한 과정에도 규칙적인 흐름이 존재합니다.
- 객체 쌩얼 생성: 순수하게
new생성자로 객체를 찍어냄 (아직 의존성 주입 안 됨) - 의존관계 주입 완료: 필드 주입이나 세터 주입 배선 작업 꽂아넣기 완료
- 🔥 초기화 콜백 (
@PostConstruct): 세팅 완료! "나 이제 디비 커넥션 연결하고 초기 데이터 밀어 넣는 부가 작업 간다잉!" - 🚀 사용 (통상 서비스 동작 기간): 애플리케이션 수명 동안 미친 듯이 일함
- 🧯 소멸 전 콜백 (
@PreDestroy): "서버 셧다운 알람 왔다! 열어둔 소켓, 디비 자원 해제하고 반납해라!" - 안전한 컨테이너 메모리에서 폐기 / 종료 (Shutdown)
@Service
public class DatabaseResourceService {
// 1번. 생성자 (객체 공간 할당 시)
public DatabaseResourceService() {
System.out.println("생성자 호출: 객체가 막 태어남");
}
// 2번. 주입 완료 후 초기화 (자원 세팅 등)
@PostConstruct
public void init() {
System.out.println("초기화 콜백: 의존성 주입 끝나자마자 DB 커넥션 맺기 얍!");
}
// 3번. 종료 직전 자원 반납
@PreDestroy
public void close() {
System.out.println("소멸 전 콜백: 서버 컴퓨터 재부팅 되기 직전에 안전하게 DB 커넥션 닫기 얍!");
}
}
🎯 4. 고수 팁 (Pro Tips)
💡 싱글톤을 파괴하는 Prototype Scope 빈
필요에 의해 1만 명이 요구할 때마다 싱글톤 마법을 무력화시키고 무식하게 1만 개의 서로 완전 독립된 새 인스턴스를 찍어내(
new) 배달해 주길 원한다면, 클래스 위에@Scope("prototype")를 명시하면 됩니다.
// 사용자가 요청할 때마다 1회용 객체를 새로 찍어줌
@Component
@Scope("prototype")
public class DisposableTask {
public void execute() {
// ...
}
}
중요한 주의점은, 프로토타입 빈은 스프링이 생성까지만 해주고 컨테이너에서 손절해버립니다. 따라서 위에서 설명한 종료 소멸 메서드(@PreDestroy)가 절대 호출되지 않기 때문에 호출받은 클라이언트 측이 스스로 소멸 타이밍을 책임져야 합니다. (물론 실무에서 마주칠 일은 거의 제로에 가깝습니다.)