JAVA

[Java] HTTP 서버 만들기: 멀티스레딩과 네트워크 최적화하기

Stark97 2024. 1. 13. 22:31
반응형
 
 
 

지금까지 열심히 만든 HTTP 서버를 톰캣과 더 가깝게 만들기 위해 네트워크 연결 관련 설정을 추가했다.

📌 서론

지금까지 내가 만든 http 서버는 스레드 풀을 적용시킨 상태다. 이제 톰캣과 비슷한 방식으로 네트워크 연결 관련 세부 설정을 적용하기 위해 ServerSocket 및 Socket에 대한 설정을 추가하고자 한다. 이를 통해 서버의 안정성과 효율성을 향상해 볼 예정이다.

이전 포스트를 확인해 보자👇🏻👇🏻

 

[Java] HTTP 서버 만들기: 스레드 풀(ThreadPool) 적용

자바로 구현한 HTTP 서버에 스레드 풀을 적용시켜보자 📌 서론 이전 글에서 Java를 사용해 HTTP 요청을 처리하는 서버에 멀티스레딩 기능을 추가했다. 작성한 MultiThreadHttpServer 클래스는 각각의 요

curiousjinan.tistory.com

이번 포스트에서 설명하는 모든 코드는 아래 깃에서 다운로드한 후 http > liketomcat 패키지에 존재합니다.

 

GitHub - wlsdks/blog_Coding

Contribute to wlsdks/blog_Coding development by creating an account on GitHub.

github.com

 

1. 소켓 타임 설정

이번 코드의 핵심 변경점: 안정성과 성능 증가

 

SOCKET_TIMEOUT

  • 이 값은 클라이언트와 소켓 사이의 연결을 너무 오래 기다리지 않게 해주는 역할을 한다. 만약 클라이언트가 5초 안에 데이터를 보내지 않으면, 서버는 이 연결을 끊고 다른 작업을 계속할 수 있다. 이렇게 하면 하나의 느린 연결 때문에 전체 서버가 느려지는 일을 방지할 수 있다.

MAX_CONNECTION_REQUESTS:

  • 이 값은 서버가 한 번에 처리할 수 있는 최대 연결 요청 수를 의미한다. 코드에는 50으로 설정되어 있어서, 51번째 연결 요청부터는 서버가 거부할 것이다. 이렇게 설정하면 서버가 갑자기 많은 요청에 압도당하는 것을 막아주고, 서비스가 안정적으로 유지되도록 도와준다.
package com.study.blog.http.threadpool;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.TimeUnit;

public class ThreadPoolMultiThreadHttpServer {

    private static final int SOCKET_TIMEOUT = 5000; // 소켓 타임아웃 설정 (5초)
    private static final int MAX_CONNECTION_REQUESTS = 50; // 최대 연결 요청 대기 수

    public static void main(String[] args) {
        CustomThreadPool customThreadPool = new CustomThreadPool(
                10,  // corePoolSize
                20,  // maximumPoolSize
                5000,// keepAliveTime
                100  // queueCapacity
        );

        try (ServerSocket serverSocket = new ServerSocket(8080, MAX_CONNECTION_REQUESTS)) {
            System.out.println("Listening for connection on port 8080 ....");

            while (!serverSocket.isClosed()) {
                try {
                    Socket clientSocket = serverSocket.accept();
                    clientSocket.setSoTimeout(SOCKET_TIMEOUT); // 소켓 타임아웃 설정
                    customThreadPool.execute(new ClientHandler(clientSocket));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            customThreadPool.shutdown();
        }
    }
}

 

서버 작동 과정: 클라이언트 요청 받아들이기

  • 서버가 실행되면, 먼저 ServerSocket을 통해 8080 포트에서 클라이언트의 연결을 기다린다. 이후 클라이언트가 연결을 시도하면, accept() 메서드가 이를 감지하고 새로운 Socket 객체를 만들어 소통을 시작한다.
try (ServerSocket serverSocket = new ServerSocket(8080, MAX_CONNECTION_REQUESTS)) {
    System.out.println("Listening for connection on port 8080 ....");
}
  • 소통이 시작되면 클라이언트 소켓에 타임아웃을 설정하고, customThreadPool을 사용해 각 클라이언트 요청을 별도의 스레드에서 처리한다. 이렇게 하면 여러 클라이언트의 요청을 동시에, 효율적으로 처리할 수 있다. 만약 연결 과정이나 요청 처리 중에 문제가 발생하면, IOException이 발생해서 예외를 콘솔에 출력한다.
while (!serverSocket.isClosed()) {
    try {
        Socket clientSocket = serverSocket.accept();
        clientSocket.setSoTimeout(SOCKET_TIMEOUT); // 소켓 타임아웃 설정
        customThreadPool.execute(new ClientHandler(clientSocket));
    } catch (IOException e) {
        e.printStackTrace();
    }
}

스레드 풀 사용: 효율적인 리소스 관리

  • CustomThreadPool은 여러 클라이언트 요청을 동시에 처리하기 위해 여러 스레드를 관리한다. 이 스레드 풀은 서버의 부하를 분산시키고, 각 요청을 빠르게 처리할 수 있게 도와준다.

  • 서버가 종료될 때는 customThreadPool.shutdown()을 호출해서 스레드 풀을 정상적으로 닫아준다. 이렇게 하면 사용 중이던 리소스를 안전하게 정리할 수 있다.

2. HTTP 프로토콜 구현 개선

ClientHandler 클래스에서의 HTTP 응답 생성

  • createHttpResponse메서드는메서드는 클라이언트에게 보낼 HTTP 응답을 생성하는 데 사용된다. 이 메서드는 응답 본문(responseBody), HTTP 응답 헤더(headers), 그리고 상태 코드(statusCode)를 매개변수로 받아 완전한 HTTP 응답 문자열을 구성한다.

  • 이 코드가 호출되면 먼저 HTTP 상태 라인을 구성하고, 그 다음으로 필수 헤더(Content-Length와 Content-Type)를 추가한다. 이어서, 매개변수로 받은 headers 맵에 있는 모든 사용자 정의 헤더를 응답에 추가한다. 마지막으로 응답 본문을 추가하여 완전한 HTTP 응답을 완성하게 된다.
/**
 * 클라이언트에게 보낼 HTTP 응답을 생성한다.
 */
private String createHttpResponse(String responseBody, Map<String, String> headers, int statusCode) {
    StringBuilder responseBuilder = new StringBuilder();
    // 상태 라인 구성
    String statusLine = "HTTP/1.1 " + statusCode + " " + getReasonPhrase(statusCode) + "\r\n";
    responseBuilder.append(statusLine);

    // 필수 헤더 추가
    responseBuilder.append("Content-Length: ").append(responseBody.getBytes(StandardCharsets.UTF_8).length).append("\r\n");
    responseBuilder.append("Content-Type: text/plain; charset=UTF-8\r\n");

    // 사용자 정의 헤더 추가
    for (Map.Entry<String, String> header : headers.entrySet()) {
        responseBuilder.append(header.getKey()).append(": ").append(header.getValue()).append("\r\n");
    }

    // 응답 본문 추가
    responseBuilder.append("\r\n").append(responseBody);

    // 완성된 HTTP 응답 반환
    return responseBuilder.toString();
}

 

상태 코드 이유 구문

  • getReasonPhrase 메서드는 HTTP 상태 코드에 대한 설명을 반환한다. 예를 들어, 200은 "OK", 404는 "Not Found"라는 의미를 가지고 있다. 이 설명은 클라이언트가 HTTP 응답을 더 잘 이해하는 데 도움을 주게 된다.
// HTTP 상태 코드에 대한 이유 구문을 반환하는 메소드
private String getReasonPhrase(int statusCode) {
    switch (statusCode) {
        case 200: return "OK";
        case 404: return "Not Found";
        case 500: return "Internal Server Error";
        // 다른 상태 코드 처리
        default: return "";
    }
}

 

3. HTTP요청 결과 비교 (개선 전/후)

프로토콜 개선 이전 헤더

프로토콜 개선 전 결과
프로토콜 개선 전 결과

프로토콜 개선 이후 헤더

프로토콜 개선 후 결과
프로토콜 개선 후 결과

프로토콜을 개선한 이후 헤더에 훨씬 풍부한 정보가 들어가는 것을 알 수 있다.

 

4. 멀티스레딩 및 리소스 관리

스레드 풀 초기화와 작업 거부 정책

  • 내가 만든 서버에서는 ThreadPoolExecutor를 사용하여 스레드 풀을 초기화한다. 이때 중요한 것은 스레드 풀의 크기와 파라미터 설정이다. 여기서 사용하는 CallerRunsPolicy는 스레드 풀이 바쁠 때 추가 작업을 처리하는 방식을 정의해 준다.

  • 만약 모든 스레드가 사용 중이고 대기열도 가득 차 있다면, 이 정책은 새로운 작업을 스레드 풀이 아닌 작업을 제출한 스레드에서 실행하게 한다. 이런 방법을 통해 시스템이 과부하 상태일 때 부하를 분산시킬 수도 있다.
// 스레드 풀 초기화 (예시)
public CustomThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, int queueCapacity) {
    this.threadPoolExecutor = new ThreadPoolExecutor(
            corePoolSize,
            maximumPoolSize,
            keepAliveTime,
            TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<>(queueCapacity),
            new ThreadPoolExecutor.CallerRunsPolicy() // 작업 거부 정책
    );
}

 

5. 정리

네트워크 최적화

  • 네트워크 최적화는 서버의 안정성과 효율성을 높이는 데 중요한 요소다. 특히, 소켓 타임 설정(SOCKET_TIMEOUT)과 최대 연결 요청 수(MAX_CONNECTION_REQUESTS)의 조정은 서버가 불필요한 대기 상태에 빠지지 않도록 하며, 네트워크 자원의 낭비를 방지한다. 느린 연결이나 과도한 요청으로 인한 서버의 성능 저하를 막기 위해 이러한 설정들이 꼭 필요하다.

멀티스레딩의 적용

  • 멀티스레딩은 서버의 동시 처리 능력을 강화하여 성능을 향상시킨다. 스레드 풀을 활용하여 다수의 클라이언트 요청을 효율적으로 처리함으로써, 리소스 사용을 최적화하고 응답 시간을 단축한다. 이는 서버가 고부하 상황에서도 안정적으로 작동할 수 있도록 도와준다.
반응형