Skip to content
Park Hyoin PARKHYO.IN
Go back

Spring Boot #5 — 서블릿과 내장 톰캣 · 스레드풀, 그리고 WAS 라는 층

Edit page

Spring Boot #4 (트랜잭션 · 연관관계 · N+1) 까지는 내가 짠 코드 안쪽을 봤다. 이번 #5 는 한 층 아래 — 내 코드를 실제로 실행시켜주는 톰캣을 들여다봤다.

Table of contents

Open Table of contents

서블릿과 톰캣 — 규격과 실행기

먼저 용어 두 개를 갈랐다.

  • 서블릿(Servlet) — 자바에서 웹 요청을 처리하는 규격
  • 톰캣(Tomcat) — 그 서블릿을 실행해주는 프로그램

규격과 실행기의 관계다. 서블릿이 “웹 요청은 이런 모양으로 처리한다”는 약속이면, 톰캣은 그 약속대로 실제로 돌려주는 쪽이다.

curl 을 날릴 때마다 스레드가 하나씩

직접 확인해봤다. 아래 요청을 실행할 때마다 스레드가 하나씩 생겼다.

curl http://localhost:8080/was/thread

톰캣의 방식은 이랬다. 일꾼(스레드)을 미리 뽑아두고 있다가, 요청이 오면 그 일꾼에게 일을 배정한다. 요청마다 매번 새 일꾼을 고용하는 게 아니라, 대기 중인 일꾼 중 하나를 붙여주는 식이다. 이게 스레드풀이다.

스레드 수를 내가 정한다

일꾼을 몇 명이나 둘지는 application.properties 에서 정한다.

server.tomcat.threads.max=200          # 최대 몇 명까지 뽑을지
server.tomcat.threads.min-spare=10     # 놀고 있어도 최소 몇 명은 대기시킬지
  • threads.max — 일꾼의 상한
  • threads.min-spare — 놀더라도 항상 준비시켜둘 최소 인원

생각해볼 것 — 동시 접속자가 몰리면

스레드 개념 자체는 이미 알고 있어서 오늘 내용이 어렵지는 않았다. 그런데 생각해볼 지점이 있었다.

갑자기 동시 접속자가 몰리면 톰캣 스레드풀이 막힌다. 준비한 일꾼보다 손님이 더 많이 오면, 나머지 손님은 대기한다.

그렇다고 일꾼을 마냥 늘릴 수도 없다. 스레드 하나하나가 메모리를 꽤 먹기 때문이다. 무작정 threads.max 를 키우면 이번엔 메모리가 터진다. 결국 이건 “많으면 좋다”가 아니라 균형의 문제다.

풀이 하나가 아니다 — 스레드풀 vs DB 커넥션 풀

그리고 풀(pool)이 톰캣 스레드풀 하나만 있는 게 아니었다. DB 커넥션 풀이 따로 있다.

스레드풀DB 커넥션 풀
다루는 것요청을 처리할 일꾼(스레드)DB 연결
관련 층컨트롤러 · 서비스DB
목적요청 동시 처리DB 쪽 병목 줄이기

스레드풀이 컨트롤러·서비스 쪽 이야기라면, DB 커넥션 풀은 DB 관련 병목을 줄이기 위한 풀이다. 그리고 이 두 풀의 균형을 적절히 맞춰서 설정해야 한다. (일꾼을 200명 뽑아놨는데 DB 연결은 10개뿐이면, 190명은 DB 앞에서 줄 서게 된다.)

WAS · 웹 서버 · 그리고 헷갈리는 “아파치”

오늘 알아본 톰캣의 정체는 WAS(Web Application Server) 다.

여기서 이름 때문에 헷갈리는 지점이 있었다. 톰캣의 정식 명칭이 Apache Tomcat 이다 보니, “아파치”라는 말이 맥락에 따라 다른 걸 가리킨다.

  • Apache Tomcat — WAS. 서블릿을 실행하는 쪽 (백엔드 맥락에서 “아파치”라고 하면 이걸 가리키는 경우를 봤다)
  • Apache HTTP Server웹 서버. 인프라 맥락에서 “아파치”라고 하면 보통 이쪽

둘 다 아파치 재단 프로젝트라 이름이 겹칠 뿐, 전혀 다른 물건이다. 그래서 “아파치”라는 단어가 나오면 지금 어느 층 이야기인지부터 확인해야 한다.

사용자 → 웹 서버 → WAS → DB

백엔드는 대략 이 흐름으로 동작한다.

사용자에서 웹 서버, WAS, DB 로 이어지는 백엔드 요청 흐름도

그리고 요즘 웹 서버는 Nginx 를 많이 쓴다고 한다. Apache HTTP Server 보다 더 가볍고 동시 접속에 유리하기 때문이다.

회고

지금까지 컨트롤러 · 서비스 · 리포지토리를 짜면서도, 그게 어디서 어떻게 실행되는지는 생각해본 적이 없었다. 오늘 그 아래층을 열어보니 톰캣이 일꾼을 뽑아두고 요청을 배정하고 있었다.

재밌었던 건, 오늘 배운 게 전부 “편하게 쓰던 것의 한계” 라는 점이다. 스레드풀은 편하지만 몰리면 막히고, 늘리면 메모리를 먹는다. #4 에서 본 N+1 · 더티 체킹 도 결국 같은 결이었다 — 편한 추상 뒤에 뭐가 돌고 있는지 봐야 한다는 것.

더 공부해볼 것

  • 예제에 있던 slow() 메서드의 정체 — 오늘 본 예제 코드에 slow() 같은 메서드가 있었는데 이게 정확히 뭔지 아직 확인을 못 했다. (요청을 일부러 지연시켜서 스레드가 붙잡히는 걸 보여주는 테스트용이 아닐까 짐작만 하는 중 — 확인 필요.)
  • threads.max 를 실제로 몇으로 잡아야 하나 — 기본값 200 이 어디서 나온 숫자인지, 서버 사양·요청 특성(CPU 바운드 vs I/O 바운드)에 따라 어떻게 달라지는지. → Spring Boot: Server Properties
  • DB 커넥션 풀 크기 정하기 — Spring Boot 기본 커넥션 풀인 HikariCP 의 maximum-pool-size 를 스레드풀과 어떻게 맞추는지. “커넥션은 스레드보다 적게” 라는 감은 잡혔는데, 근거 있는 공식이 있는지. → HikariCP: About Pool Sizing
  • 스레드 하나가 실제로 메모리를 얼마나 먹나 — “많이 먹는다”는 감각을 숫자로. JVM 스레드 스택 크기(-Xss) 와 연결해서.
  • 왜 Nginx 가 동시 접속에 유리한가 — “가볍다” 를 넘어서, 이벤트 기반 구조와 프로세스/스레드 기반 구조의 차이로 설명할 수 있게.
  • 스레드풀이 막히는 걸 넘어서는 방법 — 비동기 · 논블로킹(WebFlux) 같은 다른 접근이 있다고 들었는데, 그게 이 “일꾼 배정” 모델을 어떻게 바꾸는지.

Edit page