스프링 빈의 기본 스코프. 컨테이너당 인스턴스를 하나만 만들어 공유한다.
왜 기본이 싱글톤인가
요청이 초당 1000건 들어온다고 하자
프로토타입(매번 생성): 초당 1000개의 서비스 객체 생성 → GC 부담
- 싱글톤: — 1개를 공유 → 생성 비용 0
대부분의 스프링 빈(서비스, 리포지터리)은 상태가 없고 로직만 있으므로 공유해도 아무 문제가 없다. 하나만 만드는 것이 합리적이다.
싱글톤 패턴과 다른 점
// 전통적 싱글톤 패턴
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() { } // 생성자 막기
public static Singleton getInstance() { … }
}
문제가 많다 — 상속 불가, 테스트 시 교체 불가, 구체 클래스에 의존
(Singleton.getInstance() 를 호출하는 쪽이 결합된다).
스프링 싱글톤은 "컨테이너가 하나만 만들어 관리"할 뿐,
클래스 자체는 평범하다. 그래서 테스트에서는 그냥 new 해서 쓸 수 있고,
인터페이스 기반 설계도 그대로 가능하다.
가장 중요한 주의점 — 무상태로 설계해야 한다
@Service
public class BadService {
private int count; // ✗ 공유 필드!
public void process(Order o) {
count++; // 여러 스레드가 동시에 건드린다
this.currentOrder = o; // ✗ 스레드 A의 값을 B가 덮어쓴다
}
}
싱글톤은 모든 요청 스레드가 같은 인스턴스를 쓴다. 필드에 요청별 데이터를 담으면 다른 사용자의 데이터가 섞인다. 실제 서비스에서 "가끔 남의 정보가 보인다"는 사고의 전형적 원인이다.
✅ 지역 변수 사용 (스레드마다 스택이 따로)
✅ 파라미터로 전달
✅ 꼭 필요하면 ThreadLocal (단, 반드시 remove())
읽기 전용 필드는 괜찮다 — 설정값, 다른 빈에 대한 참조는 안전하다.
다른 스코프
| 스코프 | 생명주기 |
|---|---|
| singleton | 컨테이너와 함께 (기본) |
| prototype | 요청할 때마다 새로 생성. 이후 관리 안 함 |
| request | HTTP 요청마다 |
| session | HTTP 세션마다 |
| application | 서블릿 컨텍스트마다 |
프로토타입의 함정 — 컨테이너는 생성해서 주기만 하고
소멸 콜백(@PreDestroy)을 호출하지 않는다. 자원 정리는 클라이언트 책임이다.
싱글톤이 프로토타입을 주입받으면 — 고전적 문제
@Component
public class SingletonBean {
@Autowired private PrototypeBean prototype; // 주입은 딱 한 번!
}
싱글톤이 만들어질 때 프로토타입도 한 번만 주입된다. 이후 계속 같은 인스턴스를 쓰게 되어 프로토타입의 의미가 사라진다.
해결
@Autowired private ObjectProvider<PrototypeBean> provider;
// 쓸 때마다 provider.getObject() → 새 인스턴스
// 또는
@Scope(value = "prototype", proxyMode = TARGET_CLASS)
기동 시점 — 즉시 생성
싱글톤 빈은 애플리케이션 기동 시 전부 생성된다(eager). 그래서 설정 오류나 순환 참조가 기동 단계에서 드러난다. 런타임에 터지는 것보다 훨씬 낫다.
지연 생성이 필요하면 @Lazy를 쓰지만, 기동 시 검증을 포기하는 것이므로
꼭 필요할 때만 쓴다.