안녕하세요!
지난 포스팅에서는 웹 서버와 WAS의 차이점, 그리고 효율적인 웹 시스템을 위해 왜 이 둘을 분리하여 구성하는 지에 대해 알아보았습니다.
2025.08.26 - [Spring이당] - WAS만 쓰면 안 되나요? 웹 서버와 WAS를 함께 쓰는 진짜 이유
WAS만 쓰면 안 되나요? 웹 서버와 WAS를 함께 쓰는 진짜 이유
오늘은 웹서버와 WAS; 웹 애플리케이션 서버에 대해 알아보겠습니다.각각이 무엇인지, 이 둘의 차이점과 이상적인 웹 시스템 구성에 대해 이야기해보려고 합니다.더 나아가 서블릿과 서블릿 컨
2hiidevdang.tistory.com
이번 시간에는 지난 글에 이어, WAS가 동적 컨텐츠를 만드는 과정에서 핵심적인 역할을 하는 기술인 서블릿과, 서블릿을 관리해주는 서블릿 컨테이너에 대해 깊이 알아보겠습니다.
1. 서블릿이란 무엇인가?
서블릿이란, JAVA를 사용해 웹 요청과 응답을 처리할 수 있도록 만들어진 기술 명세 또는 그로 구현된 자바 클래스 입니다.
정의를 이해하기 어려우니 상황으로 다시 알아보겠습니다.
웹 클라이언트가 서버로 요청을 보낼 때, 서버가 클라이언트에게 응답을 보낼 때, 우리는 데이터는 어떻게 보내게 될까요?

위의 이미지는 하나의 요청과 응답이 오갈 때 이뤄지는 절차입니다.
개발자가 HTTP 요청을 받으면 요청 메시지를 직접 파싱해서 원하는 정보를 꺼내고, 비즈니스 로직을 처리한 뒤, 다시 HTTP 응답 형식에 맞춰 헤더와 바디를 직접 작성해서 보내야 합니다.
이 과정은 핵심 비즈니스 로직을 제외하고는 매우 반복적이고 귀찮은 작업이겠죠.
즉, 위 이미지에서 초록색으로 감싼 부분을 제외하고는 핵심 비즈니스 로직은 아닌 것이죠.
서블릿은 바로 이 반복적이고 귀찮은 작업을 대신 수행해주는 도구입니다. 즉, HTTP 요청/응답과 관련된 저수준의 복잡한 작업을 대신 처리해줌으로써, 개발자는 오직 핵심 비즈니스 로직에만 집중할 수 있게 됩니다.
@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response){
//애플리케이션 로직
}
}
위의 코드를 살펴보면 urlPatterns(/hello)의 url 이 호출되면 서블릿 코드가 실행된다는 것을 알 수 있습니다.
HttpServletRequest: HTTP 요청 정보를 편리하게 사용할 수 있게 한다.HttpServletResponse: HTTP 응답 정보를 편리하게 제공하도록 한다.- 따라서, 개발자는 HTTP 스펙을 매우 편리하게 사용할 수 있다.
2. 서블릿 컨테이너의 역할?
이렇게 좋은 서블릿을 당장이라도 적용하고 싶지만? 안타깝게도, 서블릿은 혼자서 실행될 수 없습니다.
서블릿을 관리하고 실행해주는 특별한 환경이 필요한데, 이를 우리는 서블릿 컨테이너라고 부릅니다.
한번쯤은 들어봤을 Apache Tomcat이 바로 대표적인 서블릿 컨테이너입니다.
서블릿 컨테이너는 결국 서블릿의 전 생애를 책임지고 관리하게됩니다.
이제 서블릿이 하는 역할에 대해 하나씩 알아보겠습니다.
2.1. 서블릿의 생명주기 관리
서블릿 컨테이너는 서블릿 객체를 생성하고, 요청이 들어오면 서블릿의 메서드를 호출하고, 서블릿이 더 이상 필요 없어지면 객체를 파괴하는 모든 과정을 관리합니다.
- 서블릿 객체 생성, 초기화, 호출, 종료 관리
2.2. 싱글톤 객체 관리
서블릿 컨테이너는 서블릿 객체를 싱글톤으로 관리합니다.
즉, 특정 서블릿에 대한 최초의 요청이 있을 때 단 하나의 객체만 생성하고, 이후 모든 요청은 이 객체를 재사용하여 처리합니다. 이는 객체 생성에 드는 비용을 줄여 성능을 향상시킵니다.
모든 고객 요청은 동일한 서블릿 객체 인스턴스에 접근합니다.
따라서, 이 객체를 사용할 때에는 공유 변수 사용에 주의해야합니다.
서블릿 컨테이너가 종료되면, 이 서블릿 싱글톤 객체도 함께 종료됩니다.
정리
- 고객 요청이 올 때마다 계속 객체 생성은 비효율
- 최초 로딩 시점에 서블릿 객체를 미리 만들어두고 재활용
- 모든 고객 요청은 동일한 서블릿 객체 인스턴스에 접근
- 공유변수 사용 주의
- 서블릿 컨테이너 종료시 함께 종료
2.3. 동시 요청을 위한 멀티 스레드 처리 지원
이 부분의 내용은 먼저 간단하게 역할을 파악하고, 이후 3번에서 더 자세하게 다뤄보겠습니다.
멀티 스레드 처리는 서블릿 컨테이너의 가장 강력한 기능 중 하나입니다.
이 기능 덕분에 여러 클라이언트가 동시에 같은 서블릿에 요청을 보내더라도, 서블릿 컨테이너는 각 요청마다 별도의 스레드를 생성하여 처리합니다.
따라서, 개발자는 복잡한 스레드 관리를 신경 쓰지 않고도 안정적으로 다중 요청을 처리할 수 있습니다.
3. 서블릿 컨테이너의 멀티 스레드 지원
지금까지 서블릿과 서블릿 컨테이너을 활용해 사용자와의 요청, 응답 처리를 파악해봤습니다.
그리고 서블릿 컨테이너는 멀티 스레드를 지원합니다.
그럼 먼저 스레드가 무엇인지부터 알아보겠습니다.
3.1. 스레드란?
스레드는 프로세스 내에서 작업을 수행하는 가장 작은 단위입니다.
- 단일 스레드 : 한 번에 하나의 작업만 순차적으로 처리합니다. 마치 계산원이 한 명뿐인 마트와 같죠. 뒷사람은 앞사람계산이 끝날 때까지 무조건 기다려야합니다.
- 다중 스레드 : 여러 스레드를 사용해 여러 작업을 동시에 처리합니다. 계산원이 여러명인 마트처럼, 훨씬 빠르고 효율적인 처리가 가능합니다.
WAS는 바로 이 "멀티스레드(다중스레드)" 모델을 사용합니다.
클라이언트로부터 요청이 올 때마다 새로운 스레드를 할당하여 서블릿을 실행시킵니다. 덕분에 여러 사용자의 요청이 들어와도 막힘없이 처리할 수가 있게됩니다!
3.2. 멀티 스레드의 동작 원리?
이 3.2. 멀티 스레드 동작 원리는 앞선 내용이 이해가지 않는 경우를 위해 작성했습니다.
저는 이 부분에서 헷갈리는 부분이 존재하였기에 정리해보았습니다.
앞의 내용이 이해가 된 경우 4. 스레드 풀의 내용으로 건너가도 좋겠습니다.
이전 내용에서 "서블릿은 싱글톤 객체로 관리된다." 라는 특징이 있었습니다.
그리고 여러 클라이언트가 동시에 서블릿에 요청을 보내도 별도의 스레드(멀티 스레드)를 생성하여 처리하기 때문에 다중 요청을 처리할 수 있다고도 했습니다.
여기서 혼동될만한 부분이 있어서 먼저 짚고 넘어가겠습니다.
우리가 '싱글톤'이라는 단어를 들었을 때 떠올리는 이미지는 보통 '하나뿐인 장난감'과 비슷합니다. 놀이터에 장난감이 하나뿐이면, 한 아이가 가지고 노는 동안 다른 아이들은 그저 기다려야 하죠.
이 모델을 서블릿에 그대로 대입하면 다음과 같은 비효율적인 그림이 그려집니다.
- A 요청이 서블릿 객체에 진입해 작업을 시작한다.
- B 요청과 C 요청이 동시에 도착하지만, A가 끝날 때까지 문 앞에서 대기한다.
- A 요청이 작업을 마치고 객체에서 나온다.
- B 요청이 드디어 객체에 진입하고, C 요청은 계속 대기한다.
만약 WAS가 정말 이렇게 동작한다면, 동시 접속자가 조금만 늘어나도 서버는 금방 마비될 겁니다. 다행히도 이것은 사실이 아닙니다.
왜냐하면 객체는 공유하지만, 실행 공간(스택)은 분리되기 때문이죠.
- 공유되는 영역 (Heap) : 서블릿 객체 그 자체, 그리고 서블릿의 멤버 변수는 힙 메모리에 단 하나만 생성되어 모든 스레드가 공유합니다.
- 독립적인 영역 (Stack) : service()나 doGet()같은 메서드가 호출될 때, 그 메서드 안에서 사용된느 지역 변수들은 각 스레드별로 따로 생성되는 스택 메모리라는 독립적인 공간에 만들어집니다.
즉 서블릿 객체는 하나뿐인 장난감보다는 도서관에 하나뿐인 거대한 백과사전에 비유될 수 있습니다.
이 백과사전을 너무 크고 귀해서 아무도 자기 자리로 대출해갈 수 없습니다. 그냥 항상 제자리에 있는 존재입니다. (싱글톤의 특징이죠)
자 이제 여러 사람(요청/스레드)이 동시에 백과사전을 이용해야 한다고 상상해 봅시다.
사람들은 사전을 통째로 들어가는 것이 아닙니다. (객체를 점유하는 것이 아닙니다.)
그저 사전 앞으로 다가가, 각자 자신이 원하는 페이지(메서드 코드)를 펼쳐 읽고, 자신의 노트에 (스레드의 스택 메모리)에 필요한 내용을 적어가는 것 뿐입니다.
이 말은 결국, A라는 사람은 "요청" 파트를, B라는 사람은 "응답" 파트를 동시에 펼쳐볼 수 있다는 말입니다. 서로가 서로를 전혀 방해하는 구조가 아닙니다.
예시 상황 하나를 보겠습니다.
10개의 스레드가 동시에 MyServlet이라는 싱글톤 객체의 service() 메서드를 호출하면 다음과 같은 일이 벌어집니다.
- 10개의 스레드는 모두 동일한
MyServlet객체의service()메서드 코드를 바라봅니다. - 하지만 각 스레드는 자신만의 독립된 스택(Stack) 공간에서 메서드를 실행합니다.
- 따라서 A 스레드의
service()메서드에 있는 지역 변수와 B 스레드의service()메서드에 있는 지역 변수는 이름이 같아도 서로 전혀 다른 공간에 존재하며, 서로에게 아무런 영향을 주지 않습니다.
3.3. 공유 변수 사용에 주의하자!
앞서 '싱글톤 객체는 순차적으로 대기하며 사용된다'고 혼란이 일었던 이유는 바로 이 공유 변수 사용 때문이었을 것입니다.
제가 혼동했던 "기다린다"는 개념은 어디서 나왔던 것일까요?
바로 여러 스레드가 하나의 공유 자원을 동시에 수정하려 할 때 발생합니다.
(운영체제)
스레드 A가 count++라는 연산을 한다고 가정해 봅시다. 이 연산은 실제로는 '1. count 값을 읽는다', '2. 값에 1을 더한다', '3. 결과를 count에 다시 쓴다'는 세 단계로 이루어집니다.
만약 count가 모든 스레드가 공유하는 멤버 변수일 때, 스레드 A가 1번을 끝낸 직후 스레드 B가 끼어들어 1, 2, 3번을 모두 끝내버리면 어떻게 될까요? A는 엉뚱한 값을 읽고 저장하게 되어 데이터의 일관성이 깨집니다. 이를 경쟁 상태(Race Condition) 라고 합니다.
이러한 경쟁 상태를 방지하려면, 공유 자원에 접근하여 수정하는 코드 영역, 즉 임계 영역(Critical Section) 을 한 번에 하나의 스레드만 실행하도록 보장해야 합니다. 이 문제를 해결하는 기법을 동기화(Synchronization) 라고 합니다.
public class MyServlet extends HttpServlet {
private int sharedCounter = 0; // 공유 자원 (하나뿐인 펜)
public void doGet(HttpServletRequest request, HttpServletResponse response) {
// ...
synchronized (this) { // 임계영역 설정
// 한 번에 하나의 스레드만 이 영역을 실행할 수 있다
sharedCounter++;
}
// ...
}
}
한 스레드가 synchronized 블록에 진입하면, 다른 스레드들은 이 블록 앞에서 말 그대로 줄을 서서 기다려야 합니다. 바로 이 지점이 우리가 처음 생각했던 '기다림'이 실제로 발생하는 구간입니다.
이처럼 동기화는 데이터의 일관성을 보장하지만, 여러 스레드를 강제로 대기시켜 순차적으로 처리하게 만듦으로써 멀티스레딩의 장점을 상쇄하고 심각한 성능 저하를 야기할 수 있습니다.
따라서 서빌릿은 상태를 갖지 않도록 (stateless), 즉 공유 멤버 변수를 최대한 사용하지 않도록 설계하는 것이 매우 중요할 것입니다.
4. 스레드 풀
지금까지 계속 했던 이야기는 "요청마다 스레드를 생성해서 처리한다." 였습니다.
하지만, 이 방법은 너무 비싼 방법입니다.
멀티스레드를 적극 활용할 때의 문제는 다음과 같습니다.
- 스레드 생성 비용은 매우 비싸다.
- 고객 요청 올 때마다 스레드를 생성하면, 응답 속도가 느려진다.
- 스레드는 컨텍스트 스위칭 비용이 발생한다.
- 스레드는 생성에 제한이 없다.
- 고객 요청이 너무 많이 오면, CPU, 메모리 임계점을 넘어 서버가 죽을 수 있다.
음.. 결국 스레드를 무자비하게 마구 생성하다보면서 서버가 죽을 수 있습니다.
죽은 서버를 살리는 일은 매우 힘든 작업이기 때문에 최대한 이러한 상황이 일어나지 않도록 하는 것이 매우 중요합니다.
이런 단점을 극복하고자 나온 방식이 바로 "스레드 풀" 입니다.
스레드 풀의 핵심은 요청마다 스레드를 새로 만드는 대신, 미리 일정 개수의 스레드를 만들어두고 재활용하는 것입니다. 이는 스레드 생성 비용을 줄이고 동시 요청을 효율적으로 처리하며 서버 부하를 관리하는 데 도움이 됩니다.
스레드 풀의 장점을 하나씩 살펴보겠습니다.
4.1. 응답 속도 향상
요청이 들어왔을 때 스레드를 새로 만드는 준비 시간이 사라집니다. 이미 대기 중인 스레드를 즉시 할당하여 작업을 시작하므로, 사용자는 훨씬 빠른 응답을 경험하게 됩니다.
4.2. 자원의 효율적 사용
스레드를 미리 제한된 개수만큼만 만들어두기 때문에, 불필요한 메모리 사용을 막고 CPU의 컨텍스트 스위칭 부담을 줄여줍니다. 한정된 자원을 훨씬 효율적으로 운영할 수 있게 됩니다.
4.3. 서버의 안정성 및 제어 가능성 확보
스레드 풀은 우리 서버의 수문장 역할을 합니다.
예를 들어 최대 스레드 개수를 200개로 설정하면, 아무리 많은 요청이 밀려 들어와도 우리 서버는 정확히 200개의 요청만 동시에 처리하고 나머지는 대기시킵니다.
이는 갑작스러운 트래픽 폭주에도 서버가 자신의 처리 용량을 넘어 다운되는 최악의 상황을 원천적으로 방지해줍니다.
개발자는 이 스레드 개수를 조절함으로써 서버 부하를 능동적으로 제어할 수 있게 되는 것입니다.
다행히도 현대의 거의 모든 WAS는 내부적으로 스레드 풀을 사용하여 동작합니다. 따라서 개발자가 직접 스레드 풀을 구현할 일은 거의 없습니다.
다만, WAS의 성능을 튜닝할 때, 이 스레드 풀의 설정을 조절하는 것은 매우 중요합니다. 예를 들어 톰켓 서버에서 maxThread와 같은 파라미터로 최대 스레드 개수를 설정할 수 있습니다. 이 값을 우리 서비스의 트래픽 패턴과 서버 사양에 맞게 최적화하는 것이 안정적인 서비스 운영의 핵심이라고 할 수 있습니다.
5. 마치며
오늘은 WAS가 동적 콘텐츠를 처리하는 핵심 기술인 서블릿부터, 수많은 동시 요청을 안정적으로 처리하게 해주는 서블릿 컨테이너의 멀티스레딩과 스레드 풀 모델까지 깊이 있게 알아보았습니다.
이 글을 통해 서블릿이 싱글톤이면서도 어떻게 동시성을 확보하는지, 그리고 무분별한 스레드 생성을 막고 서버를 안정적으로 지키는 스레드 풀의 중요성을 명확히 이해할 수 있으면 좋겠습니다. 눈에 보이지 않는 서버의 뒷단에서 벌어지는 이러한 움직임을 이해한다면, 앞으로 더 효율적이고 안정적인 웹 애플리케이션을 설계하는 데 큰 도움이 될 것입니다!
파이팅~
'Spring이당' 카테고리의 다른 글
| WAS만 쓰면 안 되나요? 웹 서버와 WAS를 함께 쓰는 진짜 이유 (1) | 2025.08.26 |
|---|---|
| Stateful vs Stateless: 세션 인증과 JWT 인증의 핵심 차이점 (0) | 2025.08.20 |
| 그래서 JPA 하이버네이트가 뭔데? 라이브러리! (1) | 2025.08.03 |
| JPA를 왜 써야 할까 - 패러다임의 불일치 (2) | 2025.08.02 |
| Spring : Cannot access ~~.repository 이슈 (해결) (1) | 2025.06.02 |