Spring Bean Scope 실무 정리: Singleton·Prototype·Request 동작과 함정
Spring의 @Service, @Repository, @Component는 별도 설정이 없으면 Singleton Bean으로 등록됩니다. 이 사실을 모르고 Bean 필드에 요청별 데이터를 저장하면 여러 사용자의 요청이 같은 값을 공유하는 동시성 문제가 생길 수 있습니다.
반대로 Prototype을 사용했는데도 매번 새 객체가 만들어지지 않거나, Request Scope Bean을 비동기 작업에서 호출하다가 예외가 발생하기도 합니다.
이번 글에서는 Singleton, Prototype, Request Scope의 생성 시점과 수명, 실무에서 자주 만나는 함정을 설명합니다.
Bean Scope란 무엇인가
Bean Scope는 Spring 컨테이너가 Bean 인스턴스를 몇 개 만들고 언제까지 관리할지를 결정합니다.
| Scope | 인스턴스 기준 | 주요 용도 | 주의사항 |
|---|---|---|---|
singleton |
Spring 컨테이너와 Bean 정의마다 하나 | 상태 없는 서비스·Repository | 가변 필드를 두면 요청 간 공유 |
prototype |
컨테이너에 요청할 때마다 생성 | 짧게 사용하는 상태 객체 | 소멸 과정은 직접 관리 |
request |
HTTP 요청마다 하나 | 요청 ID·요청 전용 정보 | 요청 스레드 밖에서는 사용 주의 |
session |
HTTP Session마다 하나 | 세션별 임시 상태 | 세션 수만큼 메모리 사용 |
Spring Singleton은 JVM 전체에 객체가 하나라는 뜻이 아닙니다. 하나의 Spring ApplicationContext와 하나의 Bean 정의를 기준으로 인스턴스 하나를 관리합니다.
Singleton Bean은 상태를 갖지 않게 만든다
다음 서비스는 발급 횟수를 필드에 저장합니다.
import org.springframework.stereotype.Service;
@Service
public class CouponService {
private int issuedCount;
public int issue() {
return ++issuedCount;
}
}
CouponService는 기본적으로 Singleton입니다. 여러 요청 스레드가 같은 issuedCount를 동시에 변경하므로 값이 유실되거나 예상하지 못한 결과가 나올 수 있습니다.
AtomicInteger는 한 프로세스의 단순 증가 경쟁만 줄입니다. 여러 서버에서는 인스턴스마다 값이 달라지므로 비즈니스 상태는 MariaDB나 Redis에 저장해야 합니다. Singleton 서비스는 요청 값을 매개변수로 받고 결과를 반환하는 상태 없는 구조로 작성합니다.
Prototype은 언제 새 객체를 만들까
Prototype Bean은 Spring 컨테이너에 요청할 때마다 새 인스턴스를 만듭니다.
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ReportContext {}
@Service
class ReportService {
private final ObjectProvider<ReportContext> contextProvider;
ReportService(ObjectProvider<ReportContext> contextProvider) {
this.contextProvider = contextProvider;
}
ReportContext newContext() {
return contextProvider.getObject();
}
}
Prototype을 Singleton 생성자에 직접 주입하면 한 번만 만들어집니다. 매 작업에서 새 객체가 필요하면 ObjectProvider.getObject()로 조회합니다. 단순한 객체라면 new가 더 명확할 수 있습니다.
Spring은 Prototype Bean을 생성하고 의존성을 주입한 뒤 전체 생명주기를 추적하지 않습니다. 따라서 @PreDestroy는 자동 호출되지 않으며 외부 자원은 사용하는 코드가 직접 정리해야 합니다.
Request Scope는 요청마다 인스턴스를 분리한다
요청 ID처럼 하나의 HTTP 요청 안에서는 같고 다른 요청과는 분리되어야 하는 값에는 Request Scope를 사용할 수 있습니다.
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.RequestScope;
import java.util.UUID;
@Component
@RequestScope
public class RequestTrace {
private final String requestId =
UUID.randomUUID().toString();
public String requestId() {
return requestId;
}
}
@RequestScope는 기본적으로 클래스 기반 Scoped Proxy를 사용합니다. Singleton 서비스에는 프록시가 주입되고, 메서드 호출 시 현재 HTTP 요청의 실제 RequestTrace로 연결됩니다.
Request Scope와 비동기 작업의 함정
Request Scope는 HTTP 요청 스레드에 연결됩니다. @Async나 별도 Executor에서 호출하면 ScopeNotActiveException이 발생할 수 있습니다.
비동기 작업에는 Request Scope Bean 자체를 넘기지 않습니다. 비동기 실행을 시작하기 전에 필요한 값을 꺼내 명시적으로 전달합니다.
String requestId = requestTrace.requestId();
notificationService.sendAsync(orderId, requestId);
필요한 값만 넘기면 스레드가 바뀌어도 동작이 명확합니다.
자주 하는 실수
- Singleton Bean 필드에 로그인 사용자나 요청 데이터를 저장합니다.
- Prototype Bean을 Singleton 생성자에 직접 주입하고 매번 새 객체라고 생각합니다.
- Prototype Bean의
@PreDestroy가 자동 실행된다고 기대합니다. - Request Scope Bean을
@Async나 스케줄러에서 그대로 호출합니다. - Session Scope에 큰 객체를 저장해 사용자 수만큼 메모리를 사용합니다.
핵심 요약
- Spring Bean의 기본 Scope는 Singleton이며 컨테이너와 Bean 정의마다 하나입니다.
- Singleton 서비스는 요청별 가변 상태를 필드에 저장하지 않습니다.
- Prototype을 Singleton에서 반복 생성하려면
ObjectProvider등을 사용합니다. - Prototype Bean의 소멸과 자원 정리는 클라이언트가 책임집니다.
- Request Scope는 요청마다 새 인스턴스를 만들고 Scoped Proxy로 Singleton에 주입할 수 있습니다.
- 비동기 작업에는 Request Scope Bean 대신 필요한 값만 전달합니다.
마무리
Bean Scope는 객체 개수만 정하는 설정이 아니라 상태의 공유 범위와 생명주기를 결정하는 기준입니다.
대부분의 서비스와 Repository는 상태 없는 Singleton으로 두는 것이 좋습니다. 요청 전용 상태는 Request Scope나 매개변수로 제한하고, 작업 전용 객체는 일반 객체 생성 또는 Prototype을 목적에 맞게 선택해야 합니다.
함께 읽기:
공식 자료:
최종 확인: 2026-07-29
'Programming > Spring' 카테고리의 다른 글
| Spring Framework 7 @Retryable 실전 설정: 지수 백오프·지터·재시도 예외 구분 (0) | 2026.08.03 |
|---|---|
| Spring @TransactionalEventListener AFTER_COMMIT 함정: 저장 로직이 반영되지 않는 이유 (0) | 2026.07.27 |
| @Transactional 실전 함정 정리 — 셀프 호출부터 롤백까지 (0) | 2026.07.22 |
| HikariCP 커넥션 풀 설정, 숫자의 근거 정리 (0) | 2026.07.18 |
| 스프링 MVC와 DispatcherServlet 내부 동작: 모든 요청은 여기로 집결! (0) | 2025.08.24 |