본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Backend

스프링의 싱글톤 빈과 요청 스레드

작성 2026. 10. 14.추가
목차7개 항목
출처와 참고

팔만코딩경(42Seoul 커뮤니티 위키)의 「스프링의 싱글톤과 멀티 스레딩」을 바탕으로 재구성한 노트다. 「다른 빈 스코프」는 gyoogle의 「tech-interview-for-developer」 저장소의 Spring Bean Scope 문서를 바탕으로, Spring 공식 문서로 확인해 보강했다.

질문: 객체는 하나인데 요청은 동시에 수천 개

스프링 컨테이너는 @Service, @Controller, @Repository 같은 빈(bean)을 기본적으로 싱글톤 스코프로 만든다. 애플리케이션 전체에서 UserService 인스턴스는 하나다. 그런데 사용자 수천 명이 동시에 요청을 보내도 서로 섞이지 않고 처리된다. 비결은 두 가지다.

  1. 요청마다 다른 스레드가 같은 객체의 메서드를 실행한다.
  2. 요청마다 다른 데이터는 객체의 필드가 아니라 각 스레드의 스택(지역 변수, 매개변수) 에 있다.

싱글톤 패턴 자체의 장단점은 싱글톤 패턴과 서비스 패턴에, 스레드의 개념은 프로세스와 스레드에 있다.

다른 빈 스코프

싱글톤 말고도 빈마다 수명을 정할 수 있다(@Scope).

스코프인스턴스쓰는 곳
singleton(기본)컨테이너에 하나상태 없는 서비스·리포지토리
prototype요청(주입·getBean)할 때마다 새로상태를 가진 짧은 객체. 컨테이너는 만들기만 하고 소멸 콜백을 부르지 않는다
requestHTTP 요청마다 하나요청 단위 컨텍스트
sessionHTTP 세션마다 하나로그인 사용자별 장바구니
applicationServletContext마다 하나웹 애플리케이션 전역 값
websocket웹소켓 세션마다 하나연결별 상태

싱글톤 빈에 prototype이나 request 빈을 그냥 주입하면 주입 시점에 한 번만 들어가 계속 같은 객체를 쓴다. 매번 새 객체가 필요하면 ObjectProvider<T>로 꺼내거나, request·session 빈에 proxyMode = ScopedProxyMode.TARGET_CLASS를 줘 프록시를 주입한다. 옛 globalSession 스코프는 포틀릿용이라 Spring 5에서 없어졌다.

요청이 스레드를 만나는 과정

Spring Boot 웹 앱(Spring MVC)은 내장 Tomcat이 서블릿 컨테이너 겸 WAS 역할을 한다.

flowchart TB
  req["클라이언트 요청"] --> tc["Tomcat 커넥터(NIO)가 연결 수락"]
  tc --> th["요청 처리 스레드 풀에서 스레드 하나 배정<br/>(http-nio-8080-exec-N)"]
  th --> ds["DispatcherServlet"]
  ds --> ctl
  subgraph beans["모두 싱글톤 빈, 모든 스레드가 공유"]
    ctl["Controller"] --> svc["Service"]
    svc --> repo["Repository"]
  end
  repo --> done["응답을 쓰면 스레드는 풀로 돌아간다"]
구성 요소역할
서블릿(Servlet)HTTP 요청을 받아 응답을 만드는 자바 객체. 스프링 MVC에서는 DispatcherServlet 하나가 모든 요청을 받아 알맞은 컨트롤러로 보낸다
서블릿 컨테이너서블릿의 생성·초기화·소멸을 관리하고 요청을 스레드에 배정한다. Tomcat, Jetty, Undertow
스레드 풀스레드를 미리 만들어 두고 재사용한다. 스레드 생성 비용이 크기 때문에 요청마다 새로 만들지 않는다

이 구조를 요청당 스레드(thread-per-request) 모델이라 한다. 요청 하나를 처리하는 동안 그 스레드는 그 요청 전용이다.

커넥터: BIO에서 NIO로

방식동작현재
BIO(Blocking I/O)연결 하나에 스레드 하나가 연결이 끝날 때까지 붙어 있다. Keep-Alive로 놀고 있는 연결도 스레드를 잡는다Tomcat 8.5부터 제거됐다
NIO(Non-blocking I/O)소수의 Poller 스레드가 수많은 연결을 감시하다가, 데이터가 도착한 요청만 작업 스레드에 넘긴다Tomcat 8부터 기본값. Spring Boot도 NIO를 쓴다

NIO 덕분에 동시 연결 수(기본 8192)는 작업 스레드 수(기본 200)보다 훨씬 많을 수 있다. 다만 요청 하나가 처리되는 동안에는 여전히 스레드 하나를 점유한다. NIO는 연결 대기를 줄일 뿐, 스레드 안전성을 대신 지켜 주지 않는다.

핵심: 싱글톤 빈은 상태 없이 짠다

여러 스레드가 같은 빈 인스턴스를 동시에 실행하므로, 빈의 인스턴스 필드는 모든 요청이 공유하는 변수다.

@Service
public class OrderService {
    private int lastPrice;                 // ✗ 모든 요청 스레드가 공유한다
 
    private final OrderRepository repo;     // ✓ 불변 의존성(주입 후 바뀌지 않음)
 
    public int order(Long itemId, int qty) {
        int price = repo.findPrice(itemId) * qty;   // ✓ 지역 변수: 스레드마다 따로
        lastPrice = price;                          // ✗ 다른 요청이 덮어쓴다
        return lastPrice;                           // 다른 사용자의 금액이 나갈 수 있다
    }
}
둬도 되는 것두면 안 되는 것
final로 주입받은 다른 빈, 설정값요청·사용자마다 다른 값을 담는 필드
불변 객체, 스레드 안전한 객체(ConcurrentHashMap, AtomicLong)HashMap, ArrayList, SimpleDateFormat 같은 비동기화 가변 객체
지역 변수, 매개변수카운터·캐시를 일반 필드로 직접 구현한 것

요청별 상태가 꼭 필요하면 메서드 매개변수로 넘기고, 정말 필요할 때만 @RequestScope 빈이나 ThreadLocal을 쓴다. ThreadLocal은 스레드가 풀로 돌아가 재사용되므로 요청이 끝나면 반드시 remove() 해야 다음 요청에 값이 새지 않는다. 공유 자원을 꼭 바꿔야 한다면 synchronized나 락으로 보호하되, 모든 요청이 그 지점에서 줄을 서게 된다는 점을 감안한다.

스레드 풀 설정과 확장

Spring Boot에서 내장 Tomcat 설정은 application.yml로 바꾼다.

server:
  tomcat:
    threads:
      max: 200          # 작업 스레드 최대 수 (기본 200)
      min-spare: 10     # 항상 대기시켜 둘 스레드 수 (기본 10)
    max-connections: 8192   # 동시에 유지할 연결 수 (기본 8192)
    accept-count: 100       # 연결이 꽉 찼을 때 OS 대기열 길이 (기본 100)
상황대응
DB·외부 API 대기가 길어 스레드가 모자란다먼저 느린 쿼리·호출을 고친다. 스레드만 늘리면 DB 커넥션 풀이 병목이 된다
I/O 대기가 대부분인 서비스Java 21 이상, Spring Boot 3.2 이상이면 spring.threads.virtual.enabled=true로 가상 스레드를 켠다. 요청당 스레드 모델은 그대로 두고 스레드를 아주 싸게 만든다
매우 많은 동시 연결, 스트리밍Spring WebFlux(Netty) 같은 이벤트 루프 기반 논블로킹 스택을 검토한다
오래 걸리는 작업을 요청과 분리@Async와 TaskExecutor로 별도 스레드 풀에 넘기거나 메시지 큐로 뺀다

가상 스레드를 켜도 싱글톤 빈이 공유된다는 사실은 그대로다. 상태 없이 짜야 하는 규칙은 바뀌지 않는다.

정리

  • 스프링 빈은 기본이 싱글톤이고, Tomcat 스레드 풀의 스레드들이 요청마다 같은 빈을 동시에 실행한다.
  • 요청마다 다른 데이터는 스레드의 스택(지역 변수)에 있으므로 섞이지 않는다. 섞이는 것은 빈의 필드다.
  • 빈에는 불변 의존성과 설정만 두고, 요청 상태는 매개변수로 흘린다.
  • Tomcat은 NIO 커넥터로 연결과 스레드를 분리한다. 스레드가 모자라면 느린 I/O부터 고치고, 가상 스레드를 검토한다.

관련 노트: 싱글턴 패턴, Java 언어 기본(ThreadLocal), Express 싱글톤 패턴과 서비스 패턴, 웹 서버, WAS, 그리고 Nginx, NestJS Module과 Provider: 의존성 주입과 캡슐화, 프로세스와 스레드, 비동기 서버의 이벤트 루프: 다중 접속 서버에서 Reactor 패턴까지, 스프링 애플리케이션 설계

원본 기록2

댓글

GitHub 계정으로 댓글을 남길 수 있어요.