스프링을 공부하다 보면 반드시 부딪히는 개념이 싱글톤 컨테이너다.
많은 개발자가 “스프링이 왜 싱글톤을 기본 전략으로 사용할까?”라고 묻는다.
오늘은 이 질문을 쉽게 설명하면서도,
중간중간 기술적으로 반드시 알아야 하는 핵심 원리까지 깊이 있게 파헤쳐본다.
1. 웹 애플리케이션에서 싱글톤이 필수인 이유
웹 서버는 동시에 수많은 요청을 처리한다.
- 1초에 500명이 들어오면
- 서비스 객체도 500개가 만들어질까?
만약 그렇다면 서버는 10초 만에 메모리가 폭발한다.
비즈니스 로직 대부분은 상태를 저장하지 않는(stateless) 구조기 때문에
사실 객체는 하나만 만들어서 계속 재사용해도 된다.
그래서 스프링은 처음부터 “빈은 기본적으로 1개만 만든다(싱글톤)” 전략을 선택했다.
2. 전통 싱글톤 패턴의 문제점
GoF 싱글톤 패턴은 유명하다.
하지만 다음과 같은 문제 때문에 실제 서비스에서는 잘 사용하지 않는다.
- private 생성자 때문에 테스트가 어려움
- getInstance()가 DIP 위반
- 코드 강결합
- 확장성 부족
- 상태를 가지면 멀티스레드 문제 발생
그래서 스프링은 직접 싱글톤 패턴을 구현하지 않는다.
3. 스프링의 해답: 싱글톤 컨테이너
스프링은 다음 과정을 자동으로 처리한다.
- @Bean/@Component로 등록된 클래스를 스캔
- 객체를 1개만 생성
- 내부 캐시에 저장
- 다음 요청부터는 새 객체를 만들지 않고 같은 객체를 반환
그래서 개발자가 구식 싱글톤 패턴을 만들 필요도 없다.
4. 스프링 싱글톤이 내부적으로 어떻게 동작하는가?
스프링은 싱글톤을 위해 3-level cache를 사용한다.
(1) 스프링의 3-Level Singleton Cache 구조
스프링 BeanFactory에는 다음과 같은 캐시 3개가 존재한다.
1단계: singletonObjects
이미 완성된 싱글톤 빈 저장
2단계: earlySingletonObjects
초기화 전 단계의 빈 저장 (순환 의존성 해결용)
3단계: singletonFactories
빈을 생성하는 팩토리 저장 (ObjectFactory)
스프링은 getBean() 호출 시 아래 순서로 빈을 탐색한다.
if (singletonObjects contains bean) return bean;
if (earlySingletonObjects contains bean) return bean;
if (singletonFactories contains bean) {
// 생성하고 early로 이동
}
이 구조 덕분에 스프링은
순환 참조(A → B → A)조차 생성자 주입이 아니라면 해결할 수 있다.
(2) @Configuration + @Bean이 CGLIB 프록시를 쓰는 이유
스프링이 AppConfig를 CGLIB 프록시로 감싸는 이유는 오직 하나다.
“@Bean 메서드를 여러 번 호출해도 하나의 빈만 생성되도록 보장하기 위해서”
예:
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
AppConfig의 실제 코드는 다음과 같이 프록시로 바뀐다.
public MemberRepository memberRepository() {
if (singletonObjects.contains("memberRepository")) {
return singletonObjects.get("memberRepository");
}
MemberRepository repo = new MemoryMemberRepository();
singletonObjects.put("memberRepository", repo);
return repo;
}
이게 스프링이 제공하는 싱글톤 보장 메커니즘이다.
(3) 멀티스레드 환경에서도 안전한 이유
스프링의 singletonObjects는 ConcurrentHashMap이다.
그래서 동시성 문제가 발생하지 않는다.
5. 반드시 지켜야 하는 규칙: 싱글톤 빈은 상태를 가지면 안 된다
다음 코드는 절대 금지다.
private int price; // 위험
이유:
- 여러 요청이 같은 객체를 공유
- A 요청이 price = 10000
- B 요청이 price = 20000
- A가 price 조회하면 20000이 나와버림
실제 현업에서도 이 실수로 장애가 자주 발생한다.
6. 결론: 스프링 싱글톤은 패턴이 아니라 “컨테이너 전략”
정리하면 다음과 같다.
- 싱글톤 패턴의 단점은 모두 제거
- 성능 최적화
- 메모리 효율
- DI, AOP, MVC 등 핵심 기능의 기반
- 스레드 안정성 보장
- 3-level cache + CGLIB으로 안정적 동작
스프링의 싱글톤 컨테이너는
단순한 “객체 1개 전략”이 아니라
“매우 고급스럽고 안전한 컨테이너 기반 DI 구조”다.