빈이 몇 개 만들어지고 얼마나 사는지를 정하는 설정. 기본값이 만드는 결과가 크다.
종류
- singleton — 컨테이너당 하나 (기본값)
- prototype — 요청할 때마다 새 인스턴스
- request — HTTP 요청당 하나 (웹)
- session — HTTP 세션당 하나 (웹)
기본이 싱글톤이라서 따라오는 것 — 무상태여야 한다
// ❌ 싱글톤 빈에 상태를 뒀다 — 모든 요청이 이 필드 하나를 공유한다
@Service
class OrderService {
private Long currentUserId; // 재앙
public void process(Long userId) {
this.currentUserId = userId; // 다른 스레드가 곧바로 덮어쓴다
// ... A 사용자의 요청이 B 사용자의 데이터를 본다
}
}
// ✅ 상태는 지역 변수나 파라미터로
@Service
class OrderService {
public void process(Long userId) { /* userId 를 넘겨 쓴다 */ }
}
"싱글톤 빈에 가변 필드를 두지 않는다" 는 Spring에서 가장 값비싼 사고를 막는 한 줄이다. 재현이 어렵고(동시 요청에서만 발생) 데이터가 섞여 나가므로 피해가 크다.
싱글톤에 프로토타입을 주입하면
@Service
class A {
private final B b; // B 가 prototype 이어도
A(B b) { this.b = b; } // A 가 만들어질 때 '한 번' 주입된다
}
// → 이후 계속 같은 B 인스턴스를 쓴다. 매번 새로 생기지 않는다
// 매번 새로 필요하면 조회 시점을 미룬다
@Service
class A {
private final ObjectProvider<B> provider;
void run() { B b = provider.getObject(); } // 호출할 때마다 새 인스턴스
}
@Lookup 메서드 주입도 같은 목적이다.
프로토타입의 소멸 콜백은 호출되지 않는다
컨테이너는 prototype 빈을 '생성·주입' 까지만 관리한다
- @PreDestroy 가 불리지 않는다
- 자원을 쥐는 빈을 prototype 으로 두면 누수가 된다
GoF 싱글톤과 다르다
- GoF 싱글톤 — JVM 당 하나. private 생성자 + static 인스턴스
- Spring 싱글톤 — 컨테이너당 하나. 평범한 클래스이고 컨테이너가 하나만 만들 뿐
- 테스트에서 new 로 여러 개 만들 수 있다
면접 함정
- ❌ "프로토타입을 주입하면 매번 새 객체" → 싱글톤이 들고 있으면 고정된다.
- ❌ "싱글톤이라 스레드 안전" → 반대다. 공유되므로 상태를 두면 위험하다.
웹 스코프와 프록시
// request 스코프 빈을 싱글톤에 주입하려면 프록시가 필요하다
@Component
@RequestScope // = @Scope(value="request", proxyMode=TARGET_CLASS)
class RequestContext { private String traceId; }
@Service
class OrderService {
private final RequestContext ctx; // 프록시가 주입된다
// 실제 호출 시점에 '그 요청의' 인스턴스로 위임된다
}
프록시가 없으면 싱글톤이 만들어지는 기동 시점에 요청이 없어서 주입 자체가 실패한다.
상태가 필요하면 어디에 두나
요청 범위 값 (traceId · 로그인 사용자)
- @RequestScope · ThreadLocal · Scoped Values (Java 25)
캐시 · 설정처럼 공유해도 되는 불변 값
- 싱글톤 필드에 final 로 두면 안전하다
가변 상태
- 싱글톤에 두지 않는다. 파라미터로 넘기거나 외부 저장소로
"불변이면 싱글톤 필드에 둬도 된다" 가 정확한 기준이다. 금지되는 건 가변 상태다.