본문으로 건너뛰기

ServerSocket으로 이해하는 1요청 1스레드

· 약 2분
개발자

톰캣은 단순한 컨테이너가 아니라, 'ServerSocket'을 멀티 스레드로 구현한 거대한 서버 시스템이다. 직접 코드로 구현하며 그 민낯을 파헤쳐보자.

1. 톰캣의 심장: ServerSocket​

우리가 쓰는 모든 자바 서버의 대문은 ServerSocket이다. 특정 포트(8080)를 열고 손님이 올 때까지 accept()에서 대기(Blocking)하는 것이 시작이다.

2. 1요청 1스레드 (Thread-per-Request)​

톰캣은 손님이 올 때마다 새로운 '알바생(Thread)'을 고용한다. 문지기(Main 스레드)가 손님을 받으면, 실제 응답은 알바생이 처리하게 하여 동시 접속을 가능하게 한다.


import java.io.*;
import java.net.*;

public class Main {
public static void main(String[] args) throws IOException {
// 8080 포트 오픈
ServerSocket server = new ServerSocket(8080);
System.out.println("🚀 8080 포트에서 서버가 대기 중입니다...");
int cnt = 0;
while (true) {
// 유저가 들어올 때까지 대기 (Blocking)
cnt++;
Socket socket = server.accept();

System.out.println("새로운 연결 요청 발생!" + cnt);

// 톰캣 방식: 새 스레드 생성
new Thread(() -> {
try (OutputStream out = socket.getOutputStream()) {
String threadName = Thread.currentThread().getName();
String msg = "Hello from " + threadName;

// HTTP 헤더 붙이기
String response = "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/plain; charset=UTF-8\r\n" +
"Content-Length: " + msg.getBytes().length + "\r\n\r\n" +
msg;

out.write(response.getBytes());
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}

3. 더 효율적인 관리: 스레드 풀 (Thread Pool)​

요청마다 스레드를 새로 만드는 것은 리소스 낭비다. 그래서 실제 톰캣은 미리 일정 수의 일꾼을 뽑아 대기시키는 '스레드 풀' 기법을 사용한다.

생성 비용 절감: 스레드 생성/소멸은 CPU 자원을 많이 소모한다. 미리 만든 스레드를 재사용하여 응답성을 높인다.

서버 과부하 방지: 무한정 스레드가 늘어나는 것을 막아, 유저가 몰려도 서버가 한꺼번에 뻗는 사태를 방지한다.

4. 그럼에도 존재하는 'Blocking'의 벽​

스레드 풀을 써도 결국 1요청 = 1스레드 공식은 유지된다. 만약 200명의 일꾼이 모두 긴 작업(DB 조회 등)을 하느라 붙잡혀 있다면, 201번째 유저는 앞의 작업이 끝날 때까지 무한 대기에 빠진다. 이것이 톰캣 같은 블로킹 서버의 구조적 한계다.

5. 다음 예고: 네티(Netty)와 이벤트 기반 모델​

이러한 '대기(Blocking)' 문제를 해결하기 위해 등장한 것이 바로 이벤트 기반(Event-driven) 통신이다. 다음 글에서는 단 몇 개의 스레드만으로 수만 명의 요청을 쳐내는 **네티(Netty)**의 원리를 파헤쳐보겠다.

???

Q. 그럼 왜 모든 곳에서 네티를 쓰지 않고 톰캣을 기본으로 사용할까?

톰캣은 자바 웹 표준(Servlet)을 완벽하게 구현한 '풀 패키지' 서버로, 개발자가 로직에만 집중할 수 있는 생산성을 제공한다. 반면 네티는 성능은 극강이지만 비동기 설계의 복잡도와 디버깅의 어려움이 커서, 초고성능이 필요한 특수 케이스가 아니라면 안정성과 생산성 측면에서 톰캣이 훨씬 유리하기 때문이다.