쓰레드 풀과 싱글톤 서블릿의 동시성 문제
WAS의 쓰레드 풀이 필요한 이유를 정리하고, 서블릿 멤버 변수에 model을 두면 왜 안 되는지 직접 부딪혀 본 기록.
시리즈 · Spring3 / 14
- SOLID 원칙과 OCP·DIP를 지키는 방법
- 의존성 주입(DI)과 스프링 컨테이너
- 쓰레드 풀과 싱글톤 서블릿의 동시성 문제
- FrontController 패턴과 핸들러 어댑터로 스프링 MVC 구조 이해하기
- @ModelAttribute와 PRG 패턴으로 폼 요청 처리하기
- System.out 대신 SLF4J로 로그 남기기
- 세션 직접 만들어 보기와 HttpSession
- 스프링 인터셉터와 예외 발생 시 오류 페이지 흐름
- 서블릿 Part와 MultipartFile로 파일 업로드 처리하기
- JDBC로 CRUD 구현하기와 커넥션 풀, DataSource
- JDBC 트랜잭션에서 스프링 트랜잭션 동기화와 트랜잭션 AOP까지
- JPA 변경 감지(dirty checking)와 병합(merge)
- 서블릿 컨테이너 초기화와 스프링 컨테이너 등록 과정
- 싱글톤 빈의 동시성 문제와 ThreadLocal
쓰레드
- 쓰레드는 코드를 순차적으로 하나씩 실행한다. 한 번에 한 줄의 코드만 수행한다.
- 동시 처리가 필요하면 쓰레드를 추가로 생성해야 한다.
요청이 올 때마다 쓰레드를 생성하는 방식에는 단점이 있다.
- 쓰레드 생성 비용이 매우 비싸다. 요청마다 생성하면 응답 속도가 늦어진다.
- 컨텍스트 스위칭 비용이 발생한다.
- 쓰레드 생성에 제한이 없으므로 CPU, 메모리 임계점을 넘어 서버가 다운될 수 있다.
쓰레드 풀
필요한 쓰레드를 미리 생성해 풀에 보관하고, 생성 가능한 쓰레드의 최대 개수를 관리한다.
- 쓰레드가 미리 생성되어 있으므로 생성 비용이 절약되고 응답 속도가 개선된다.
- 최대치가 정해져 있으므로 CPU, 메모리 임계점을 넘는 상황을 막을 수 있다.

최대 쓰레드 수 설정
최대 쓰레드(max thread) 수는 WAS의 주요 튜닝 포인트다.
- 너무 낮으면 서버 리소스는 여유로운데 응답이 지연된다.
- 너무 높으면 CPU, 메모리 임계점 초과로 서버가 다운된다.
적정 값은 로직의 복잡도와 서버 리소스에 따라 달라지므로 계산으로 구하기 어렵다. 그래서 주로 성능 테스트를 통해 어느 정도의 요청을 버틸 수 있는지 확인하며 찾는다.
멀티 쓰레드 처리는 WAS가 담당하므로 개발자는 멀티 쓰레드 관련 코드를 신경 쓰지 않아도 된다. 다만 멀티 쓰레드 환경에서 싱글톤 객체(서블릿, 스프링 빈) 는 주의해서 사용해야 한다. 아래는 이 주의점을 직접 겪은 사례다.
서블릿 멤버 변수로 model을 두면 안 되는 이유
FrontController 서블릿 하나로 모든 요청을 받는 MVC 프레임워크를 직접 만들어 보던 중 궁금한 점이 생겼다.
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String requestURI = request.getRequestURI();
ControllerV4 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>(); // 요청마다 새로 생성
String viewName = controller.process(paramMap, model); // 컨트롤러가 model.put
MyView view = viewResolver(viewName);
view.render(model, request, response);
}
흐름은 다음과 같다.
- 요청 파라미터를
paramMap에 저장한다. model객체를 생성한다.- 컨트롤러의
process에paramMap과model을 전달한다. - 컨트롤러가
paramMap으로 member를 만들고model.put("member", member)를 호출한다. render에서model의 정보를 request에setAttribute한다.
의문
model은 데이터를 전달하는 역할만 한다. 그렇다면 요청이 올 때마다 새로 만들지 말고 FrontController의 멤버 변수로 선언해 계속 재사용하면 되지 않을까? 실제로 그렇게 바꿔서 테스트해 보니 잘 동작했다.
답
서블릿 컨테이너에 등록된 서블릿은 싱글톤으로 관리된다. 따라서 멤버 변수로 선언한 model은 모든 요청 쓰레드가 공유하게 되고, 동시성 문제가 생긴다.
| 시점 | 요청 | 동작 |
|---|---|---|
| 1초 | A | model에 ("Hi", 1) 저장 |
| 2초 | B | model에 ("Hello", 2) 저장 |
| 3초 | A | setAttribute 수행 → A가 저장한 값이 아닌 B가 저장한 값이 사용된다 |
혼자 테스트할 때는 요청이 겹치지 않아 문제가 보이지 않았을 뿐이다. 요청마다 달라지는 상태는 싱글톤 객체의 필드가 아니라 지역 변수로 다뤄야 한다.
참고
- 김영한, 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 (인프런)