1편에서 만든 HttpServerV1은 요청을 순차적으로 처리했다. 첫 번째 요청이 5초 걸리면 두 번째 요청은 5초를 기다려야 했다. 실제 서비스라면 절대 용납할 수 없는 구조다. 해법은 단순하다. 요청마다 별도의 스레드를 할당하면 된다
구조 변경
V1의 구조는 메인 스레드가 연결 수락과 요청 처리를 모두 담당했다
main 스레드: accept() → process() → accept() → process() → ...
V2의 구조는 역할을 명확히 분리한다
main 스레드: accept() → accept() → accept() → ...
↓ ↓ ↓
pool 스레드1: process()
pool 스레드2: process()
pool 스레드3: process()
메인 스레드는 연결만 수락하고, 실제 처리는 스레드 풀의 스레드들이 병렬로 담당한다
HttpRequestHandlerV2 – 요청 처리 단위
public class HttpRequestHandlerV2 implements Runnable {
private final Socket socket;
public HttpRequestHandlerV2(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
process();
} catch (Exception e) {
log(e); // 스레드 풀 스레드에서 실행되므로 예외를 직접 처리
}
}
private void process() throws IOException {
try (socket;
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream(), UTF_8));
PrintWriter writer = new PrintWriter(socket.getOutputStream(), false, UTF_8)) {
String requestString = requestToString(reader);
if (requestString.contains("/favicon.ico")) {
log("favicon 요청");
return;
}
log("HTTP 요청 정보 출력");
System.out.println(requestString);
log("HTTP 응답 생성중...");
sleep(5000);
responseToClient(writer);
log("HTTP 응답 전달 완료");
}
}
private String requestToString(BufferedReader reader) throws IOException {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) {
break;
}
sb.append(line).append("\n");
}
return sb.toString();
}
private void responseToClient(PrintWriter writer) {
String body = "<h1>Hello World</h1>";
int length = body.getBytes(UTF_8).length;
StringBuilder sb = new StringBuilder();
sb.append("HTTP/1.1 200 OK\r\n");
sb.append("Content-Type: text/html\r\n");
sb.append("Content-Length: ").append(length).append("\r\n");
sb.append("\r\n");
sb.append(body);
log("HTTP 응답 정보 출력");
System.out.println(sb);
writer.println(sb);
writer.flush();
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
V1의 process()와 거의 동일하다. 달라진 것은 클래스가 Runnable을 구현해 스레드 풀에서 실행될 수 있는 작업 단위가 됐다는 점이다
run()에서 예외를 직접 잡는 이유: Runnable.run()은 체크 예외를 던질 수 없다. 또한 스레드 풀의 스레드에서 예외가 전파되면 해당 스레드가 종료될 수 있다. 스레드를 보호하기 위해 run() 내부에서 Exception을 잡아 로그를 남기고 조용히 처리한다
HttpServerV2 — 스레드 풀 도입
public class HttpServerV2 {
private final ExecutorService es = Executors.newFixedThreadPool(10);
private final int port;
public HttpServerV2(int port) {
this.port = port;
}
public void start() throws IOException {
ServerSocket serverSocket = new ServerSocket(port);
log("서버 시작 port: " + port);
while (true) {
Socket socket = serverSocket.accept();
es.submit(new HttpRequestHandlerV2(socket)); // 스레드 풀에 작업 제출
}
}
}
public class ServerMainV2 {
private static final int PORT = 12345;
public static void main(String[] args) throws IOException {
HttpServerV2 server = new HttpServerV2(PORT);
server.start();
}
}
V1과 비교해 start() 내부에서 바뀐 것은 단 한 줄이다
// V1 process(socket); // 메인 스레드가 직접 처리 → 블로킹 // V2 es.submit(new HttpRequestHandlerV2(socket)); // 스레드 풀에 위임 → 즉시 반환
es.submit()은 작업을 큐에 넣고 즉시 반환한다. 스레드 풀의 유휴 스레드가 꺼내서 실행한다. 메인 스레드는 즉시 다음 accept()로 돌아간다
스레드 풀 크기 선택
여기서는 newFixedThreadPool(10)으로 최대 10개의 스레드를 사용한다. 동시에 10개의 요청을 처리할 수 있다는 뜻이다. 실무에서는 보통 수백 개 수준으로 설정한다. 스레드 수를 너무 적게 잡으면 대기 요청이 쌓이고, 너무 크게 잡으면 컨텍스트 스위칭 비용과 메모리 사용량이 증가한다. 적절한 값은 CPU 코어 수, 작업의 I/O 비율, 예상 동시 요청 수를 종합적으로 고려해 결정한다
실행 결과 확인
[ main] 서버 시작 port: 12345 [pool-1-thread-1] HTTP 요청 정보 출력 GET / HTTP/1.1 Host: localhost:12345 [pool-1-thread-1] HTTP 응답 생성중... [pool-1-thread-3] HTTP 요청 정보 출력 ← thread-1이 완료되기 전에 thread-3 시작 GET / HTTP/1.1 Host: localhost:12345 [pool-1-thread-3] HTTP 응답 생성중... [pool-1-thread-1] HTTP 응답 전달 완료 [pool-1-thread-2] favicon 요청 [pool-1-thread-4] favicon 요청 [pool-1-thread-3] HTTP 응답 전달 완료
pool-1-thread-1이 응답을 완료하기 전에pool-1-thread-3이 이미 요청을 처리하기 시작했다. 두 요청이 병렬로 처리된다는 증거다. V1이라면thread-1이 완료된 후에야 다음 요청이 처리됐을 것이다.thread-2와thread-4가 파비콘 요청을 처리하느라 중간에 끼어들었다. 브라우저가 페이지 요청과 별개로 파비콘을 자동 요청하기 때문이다
두 버전 비교 요약
| V1 | V2 | |
| 처리 방식 | 순차(단일 스레드) | 병렬(스레드 풀) |
| 2개 요청 소요 시간 | 10초 | ~5초 |
| 스레드 수 | 1개 | 최대 10개 |
| 구조 | accept → process → accept | accept → submit → accept |
HTTP 서버가 어떻게 동시 요청을 처리하는지 이해했다. 실제로 Tomcat, Jetty 같은 서블릿 컨테이너도 이 구조를 기반으로 동작한다. 물론 훨씬 정교하게 최적화되어 있지만, 핵심 아이디어는 같다