이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- Spring singleton은 Java singleton과 어떻게 다른가?
- prototype Bean은 왜 소멸 관리가 까다로운가?
- Lazy 초기화는 시작 시간을 줄이는 좋은 선택인가, 오류를 숨기는 위험한 선택인가?
개요
Scope는 Bean 인스턴스의 생성 범위와 공유 방식을 결정한다. Spring의 기본 Scope는 singleton이다. 이는 하나의 Spring Container 안에서 하나의 BeanDefinition이 하나의 Bean 인스턴스를 공유한다는 뜻이다.
Spring 공식 문서는 singleton, prototype, request, session, application, websocket 같은 Scope를 제공한다. 웹 Scope는 웹 애플리케이션 컨텍스트와 연결되어 HTTP 요청이나 세션 단위로 Bean을 관리한다. 일반 ApplicationContext에서 request나 session scope를 당연히 쓸 수 있다고 보면 안 된다.
Singleton
Spring singleton은 컨테이너당 하나의 객체다. Java에서 static field로 구현하는 singleton과는 다르다. 같은 JVM 안에서도 ApplicationContext가 여러 개면 같은 Bean 정의로부터 여러 singleton 인스턴스가 생길 수 있다.
singleton은 무상태 서비스에 잘 맞는다. Controller, Service, Repository가 기본적으로 singleton Bean인 이유도 대부분 요청별 상태를 객체 필드에 저장하지 않고 메서드 지역 변수나 DB, request context를 사용하기 때문이다.
Prototype
prototype Bean은 컨테이너에 요청할 때마다 새 인스턴스를 만든다. 그러나 컨테이너가 생성 이후의 전체 생명주기를 끝까지 관리하지 않는다. 초기화까지는 관여하지만 configured destruction callback은 singleton처럼 자동 호출되지 않는다.
따라서 prototype Bean이 외부 리소스를 잡는다면 정리 책임을 별도로 설계해야 한다. prototype은 “매번 새 객체”가 필요할 때 쓰는 것이지, 리소스 생명주기를 편하게 관리하려는 도구가 아니다.
Singleton 안의 Prototype
singleton Bean이 prototype Bean을 생성자 주입으로 받으면 어떤 일이 생길까? singleton이 만들어지는 시점에 prototype 인스턴스 하나가 주입되고, 이후에는 그 하나를 계속 사용한다.
매번 새 prototype이 필요하면 ObjectProvider, Provider, lookup method 같은 지연 조회 전략을 사용해야 한다. request scope 같은 짧은 생명주기의 Bean을 singleton에 주입해야 한다면 scoped proxy도 후보가 된다.
@Service
public class ReportService {
private final ObjectProvider<ReportBuilder> reportBuilderProvider;
public ReportService(ObjectProvider<ReportBuilder> reportBuilderProvider) {
this.reportBuilderProvider = reportBuilderProvider;
}
public Report createReport() {
ReportBuilder builder = reportBuilderProvider.getObject();
return builder.build();
}
}Lazy 초기화
lazy 초기화는 Bean 생성을 컨테이너 시작 시점에서 실제 사용 시점으로 미룬다. 시작 시간을 줄이는 데 도움이 될 수 있지만, 설정 오류와 의존성 문제도 실제 사용 시점까지 늦게 발견된다.
운영 안정성 관점에서는 중요한 Bean은 eager 생성으로 시작 시점에 검증되는 편이 좋다. lazy는 비용이 큰 Bean, 특정 조건에서만 쓰는 Bean, 테스트에서 필요하지 않은 Bean에 제한적으로 검토한다.
실전 팁
- 대부분의 서비스 Bean은 singleton + 무상태 설계를 기본으로 둔다.
- request별 상태를 singleton 필드에 저장하지 않는다. 동시성 문제의 시작점이 된다.
- prototype은 “DI 되는 new”처럼 쓰되, 리소스 정리는 별도로 설계한다.
- singleton에서 매번 새 prototype이 필요하면 생성자 주입이 아니라 provider 기반 조회를 사용한다.
- request/session scope를 쓰는 코드는 웹 요청 컨텍스트 밖에서 실행될 가능성도 고려한다.
- lazy는 성능 옵션이기 전에 오류 발견 시점 변경 옵션이다.
위험 신호!
- singleton Service가 요청별 사용자 정보나 누적 값을 필드에 저장한다.
- prototype Bean이 파일, 소켓, connection을 잡지만 정리 코드가 없다.
- lazy를 전역으로 켜고 부팅 성공만 확인한다.
- singleton에 prototype을 주입해 놓고 매번 새 인스턴스가 들어온다고 기대한다.
- 웹 Scope Bean을 비웹 컨텍스트나 백그라운드 스레드에서 그대로 사용할 수 있다고 가정한다.
- Scope 선택을 객체의 책임이 아니라 “에러를 피하는 설정”으로만 다룬다.
확인 질문
확인 질문
- Spring singleton은 무엇을 의미하는가?
- 하나의 Spring Container 안에서 하나의 Bean 인스턴스를 공유한다는 뜻이다.
- singleton Bean에 prototype Bean을 생성자 주입하면 매번 새 prototype이 쓰이는가?
- 아니다. singleton 생성 시점에 주입된 prototype 인스턴스 하나를 계속 사용할 수 있다.
- lazy 초기화의 가장 큰 trade-off는 무엇인가?
- 시작 시간을 줄일 수 있지만 Bean 생성 오류 발견 시점도 늦어진다.