[Java] http 요청 멀티스레딩 적용하기
📌 서론
서버가 동시에 여러 클라이언트의 요청을 효율적으로 처리할 수 있게 하기 위해 멀티스레딩을 적용하는 건 매우 중요하다. 저번 포스트에서 Java를 사용하여 HTTP 요청을 처리하는 간단한 서버를 구축했다. 이번 포스트에서는 만들었던 서버에 멀티스레딩 기능을 추가해 보도록 하자
이 글을 쉽게 이해하기 위해서 우리는 먼저 각 클라이언트 요청을 처리하는 핵심 클래스인 ClientHandler를 살펴볼 것이다. 이 클래스를 먼저 보는 것이 서버의 기본 작동 원리와 요청 처리 메커니즘을 이해하는 데 필수적이기 때문이다.
그 후에, ClientHandler 클래스가 어떻게 멀티스레딩 환경에서 동작하는지 보여주는 MultiThreadHttpServer 클래스를 살펴보자
1. ClientHandler 클래스
ClientHandler 클래스의 역할
- ClientHandler 클래스는 HTTP 요청을 처리하는 핵심적인 역할을 수행한다. 이 클래스는 Java의 Runnable 인터페이스를 구현하고 있으며, run 메서드를 통해 각 클라이언트 연결에서 실행될 작업을 정의한다. 이는 서버가 각 클라이언트 요청을 개별적으로 처리할 수 있도록 해주는 중요한 구조적 요소다.
- 이 클래스의 주된 기능은 클라이언트로부터 받은 HTTP 요청을 분석하고 처리하는 것이다. HTTP 메서드, 요청 경로, 헤더 등의 정보를 바탕으로 요청을 분석하여 적절한 응답을 생성하고, 이를 클라이언트에게 다시 전송한다. 예를 들어, GET, POST, PUT 같은 HTTP 메서드에 따라 다른 처리를 수행한다.
❗️중요❗️ 이 클래스를 작성한 것만으로는 멀티스레딩이 구현된 것이 아니다. 멀티스레딩의 실제 구현은 main함수를 실행시키는 MultiThreadHttpServer 클래스에서 이뤄진다.
package com.study.blog.http.multithread;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* ClientHandler 클래스 자체는 멀티스레딩을 구현한 것이 아니다.
* 이 클래스는 Runnable 인터페이스를 구현하고 있고, 이 인터페이스의 run 메소드는 스레드가 실행할 작업을 정의하고 있다.
* 멀티스레딩의 핵심은 이 클래스가 아니라 main 메서드 내부의 new Thread(clientHandler).start(); 이 부분에서 나타난다.
*/public class ClientHandler implements Runnable {
private Socket clientSocket;
public ClientHandler(Socket clientSocket) {
this.clientSocket = clientSocket;
}
@Override
public void run() {
try (
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))
) {
String line = reader.readLine();
if (line == null) {
return;
}
String[] requestLineParts = line.split(" ");
String httpMethod = requestLineParts[0]; // http 종류
String requestPath = requestLineParts[1]; // 요청 경로
// 헤더값을 Map으로 만든 다음 세팅해 준다.
HashMap<String, String> headerMap = new HashMap<>();
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) {
break;
}
String[] header = line.split(": ");
headerMap.put(header[0], header[1]);
}
String responseBody = handleRequest(httpMethod, requestPath, reader, headerMap);
String httpResponse = createHttpResponse(responseBody, headerMap);
writer.write(httpResponse);
writer.flush();
if ("close".equalsIgnoreCase(headerMap.get("Connection"))) {
clientSocket.close();
}
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
// 각 요청을 처리해둘 핸들러 메서드
private String handleRequest(String httpMethod, String path, BufferedReader reader, Map<String, String> headers) throws IOException {
// 요청 처리 로직
if ("GET".equals(httpMethod)) {
return "GET 요청에 대한 응답 : " + path;
} else if ("POST".equals(httpMethod) || "PUT".equals(httpMethod)) {
return handlePostOrPutRequest(reader, headers);
}
return "Unsupported Method";
}
// post나 put 요청일때는 이 메서드를 사용한다.
private String handlePostOrPutRequest(BufferedReader reader, Map<String, String> headerMap) throws IOException {
// content의 length를 추출한다.
int contentLength = Integer.parseInt(headerMap.getOrDefault("Content-Length", "0"));
StringBuilder requestBody = new StringBuilder();
// contentLength만큼 반복하여 body에 데이터를 쓴다.
for (int i = 0; i < contentLength; i++) {
requestBody.append((char) reader.read());
}
return "Received data: " + requestBody.toString();
}
// 인코딩을 utf-8로 설정한다.
private String createHttpResponse(String responseBody, Map<String, String> headers) {
// HTTP 응답 생성
return "HTTP/1.1 200 OK\r\n" +
"Content-Length: " + responseBody.getBytes(StandardCharsets.UTF_8).length + "\r\n" +
"Content-Type: text/plain; charset=UTF-8\r\n" +
"\r\n" +
responseBody;
}
}
2. ClientHandler 클래스 코드 분석
클래스 및 필드 정의
- ClientHandler 클래스는 Runnable 인터페이스를 구현하며 이 클래스는 클라이언트의 HTTP 요청을 처리한다. clientSocket 필드는 클라이언트와의 연결을 나타낸다.
public class ClientHandler implements Runnable {
private Socket clientSocket;
public ClientHandler(Socket clientSocket) {
this.clientSocket = clientSocket;
}
}
run 메서드
- run 메서드는 스레드가 시작될 때 호출된다. 여기서는 클라이언트로부터의 입력 스트림을 읽기 위해 BufferedReader를, 응답을 클라이언트로 보내기 위해 BufferedWriter를 사용한다. 이 내부에서 HTTP 요청 처리 로직이 실행된다.
@Override
public void run() {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {
// HTTP 요청 처리 로직
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
HTTP 요청 처리
- 클라이언트로부터 첫 번째 줄을 읽어 HTTP 메서드와 요청 경로를 파악한다. 이는 HTTP 요청의 첫 줄이고, 여기에는 요청 메서드와 경로 정보가 포함되어 있다.
String line = reader.readLine();
if (line == null) {
return;
}
String[] requestLineParts = line.split(" ");
String httpMethod = requestLineParts[0]; // HTTP 메소드
String requestPath = requestLineParts[1]; // 요청 경로
헤더 값 설정
- 클라이언트 요청의 헤더를 읽어 HashMap에 저장한다. 요청 헤더에는 클라이언트와 요청에 대한 다양한 메타데이터가 포함되어 있다.
HashMap<String, String> headerMap = new HashMap<>();
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) {
break;
}
String[] header = line.split(": ");
headerMap.put(header[0], header[1]);
}
응답 처리 및 전송
- HTTP 메서드와 경로(requestPath)를 기반으로 요청을 처리한 후, 응답을 생성한다. 이 응답은 클라이언트에게 전송된다. 만약 Connection 헤더가 "close"로 설정되어 있으면 소켓 연결을 종료한다.
String responseBody = handleRequest(httpMethod, requestPath, reader, headerMap);
String httpResponse = createHttpResponse(responseBody, headerMap);
writer.write(httpResponse);
writer.flush();
if ("close".equalsIgnoreCase(headerMap.get("Connection"))) {
clientSocket.close();
}
📌 내용 요약
이 코드의 핵심은 클라이언트로부터의 HTTP 요청을 받아 처리하고, 적절한 응답을 생성하여 다시 클라이언트에게 전송하는 과정이다. ClientHandler는 이러한 처리를 위해 스레드에서 실행되며, 멀티스레딩 환경에서 서버의 각 클라이언트 요청을 독립적으로 처리할 수 있게 해 준다.
3. MultiThreadHttpServer 클래스
MultiThreadHttpServer 클래스의 멀티스레딩 구현
- MultiThreadHttpServer 클래스는 Java의 Thread 클래스를 활용하여, 서버가 각 클라이언트 연결마다 새로운 스레드를 생성하고 시작하도록 한다. 이 클래스는 ServerSocket을 통해 클라이언트 연결을 기다리고 있다 연결이 성립되면 ClientHandler 객체를 생성한다.
- 이후, 이 객체는 새로운 Thread에 할당되어 시작된다. 그러면 각 연결은 독립적인 스레드에서 처리되므로, 한 클라이언트의 요청이 다른 클라이언트의 요청 처리에 영향을 주지 않는다.
package com.study.blog.http.multithread;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class MultiThreadHttpServer {
/**
* ServerSocket은 IOException을 발생시킬 수 있다. 이는 네트워크 문제, 포트 충돌, 서버 소켓이 예상치 못하게 닫히는 경우 등 다양한 이유로 발생한다.
* 그래서 accept 메소드 호출을 try-catch 블록으로 감싸서, 발생 가능한 IOException을 적절히 처리해야 한다.
* */ public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(8080)) {
System.out.println("Listening for connection on port 8080 ....");
while (!serverSocket.isClosed()) {
try {
Socket clientSocket = serverSocket.accept();
ClientHandler clientHandler = new ClientHandler(clientSocket);
new Thread(clientHandler).start();
} catch (IOException e) {
// 여기서 IOException을 처리
System.err.println("Error accepting client connection: " + e.getMessage());
// 필요한 경우 루프를 끝내거나 서버를 재시작할 수 있어.
}
}
} catch (IOException e) {
// ServerSocket 생성 중 발생한 예외 처리
System.err.println("Cannot start server: " + e.getMessage());
}
}
}
4. MultiThreadHttpServer 클래스 코드 분석
클래스 및 메인 메서드
- MultiThreadHttpServer 클래스는 서버의 진입점인 main 메서드를 포함한다. 이 메서드에서 서버 소켓을 설정하고 클라이언트 연결을 수락하는 작업이 이루어진다.
public class MultiThreadHttpServer {
public static void main(String[] args) {
// 서버 소켓 설정 및 연결 수락 로직
}
}
서버 소켓 생성
- ServerSocket 객체가 생성되며, 8080 포트에서 클라이언트의 연결을 기다린다. ServerSocket 생성 중 발생할 수 있는 IOException을 처리하기 위한 try-catch 블록이 포함되어 있다.
try (ServerSocket serverSocket = new ServerSocket(8080)) {
System.out.println("Listening for connection on port 8080 ....");
// 클라이언트 연결 수락 루프
} catch (IOException e) {
System.err.println("Cannot start server: " + e.getMessage());
}
클라이언트 연결 수락 및 처리
- 이 루프는 서버 소켓이 닫히지 않은 동안 계속 실행된다. serverSocket.accept() 메서드를 통해 클라이언트 연결을 수락하고, 각 연결에 대해 ClientHandler 객체를 생성한 다음, 이를 새로운 스레드에서 시작한다. 클라이언트 연결 수락 중 발생하는 IOException을 처리하기 위한 내부 try-catch 블록이 있다.
while (!serverSocket.isClosed()) {
try {
Socket clientSocket = serverSocket.accept();
ClientHandler clientHandler = new ClientHandler(clientSocket);
new Thread(clientHandler).start();
} catch (IOException e) {
System.err.println("Error accepting client connection: " + e.getMessage());
}
}
이 클래스는 기본적으로 HTTP 서버의 핵심 기능을 구현한다. 클라이언트 연결을 수락하고, 각 연결에 대한 요청을 별도의 스레드에서 처리함으로써 멀티스레딩을 통한 동시성을 제공한다.
📌 내용 요약
ClientHandler 클래스는 각각의 HTTP 요청을 개별적으로 처리하는 기능을 정의하고, MultiThreadHttpServer 클래스는 이러한 요청을 병렬적으로 처리할 수 있는 환경을 제공한다.
MultiThreadHttpServer 클래스의 new Thread(clientHandler).start();를 통한 멀티스레딩 접근 방식은 서버의 처리 능력을 대폭 향상시키며, 이는 서버가 동시에 다수의 클라이언트 요청을 효과적으로 처리할 수 있게 만들어준다.
멀티 스레딩 코드의 테스트 결과가 궁금하다면?👇🏻👇🏻
작성한 코드는 아래 git에서 코드를 받은 후 "study/blog/http/multithread" 패키지에서 확인할 수 있다.
'JAVA' 카테고리의 다른 글
[JMeter] 멀티스레딩 vs 스레드 풀: Java로 만든 HTTP 서버 성능 테스트 (27) | 2024.01.03 |
---|---|
[Java] HTTP 서버 만들기: 스레드 풀(ThreadPool) 적용 (1) | 2024.01.02 |
[JMeter] MacOS(M1)에서 JMeter를 이용한 부하 테스트 (26) | 2024.01.01 |
[Java] HTTP 서버 만들기: GET, POST, PUT 요청별 처리 (28) | 2023.12.30 |
[Java] HTTP 서버 구현: postman과 자바 HttpClient를 사용한 요청 (23) | 2023.12.30 |