JAVA
[Java] HTTP 서버 만들기: 멀티스레딩과 네트워크 최적화하기
Stark97
2024. 1. 13. 22:31
반응형
지금까지 열심히 만든 HTTP 서버를 톰캣과 더 가깝게 만들기 위해 네트워크 연결 관련 설정을 추가했다.
📌 서론
지금까지 내가 만든 http 서버는 스레드 풀을 적용시킨 상태다. 이제 톰캣과 비슷한 방식으로 네트워크 연결 관련 세부 설정을 적용하기 위해 ServerSocket 및 Socket에 대한 설정을 추가하고자 한다. 이를 통해 서버의 안정성과 효율성을 향상해 볼 예정이다.
이전 포스트를 확인해 보자👇🏻👇🏻
이번 포스트에서 설명하는 모든 코드는 아래 깃에서 다운로드한 후 http > liketomcat 패키지에 존재합니다.
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)의 조정은 서버가 불필요한 대기 상태에 빠지지 않도록 하며, 네트워크 자원의 낭비를 방지한다. 느린 연결이나 과도한 요청으로 인한 서버의 성능 저하를 막기 위해 이러한 설정들이 꼭 필요하다.
멀티스레딩의 적용
- 멀티스레딩은 서버의 동시 처리 능력을 강화하여 성능을 향상시킨다. 스레드 풀을 활용하여 다수의 클라이언트 요청을 효율적으로 처리함으로써, 리소스 사용을 최적화하고 응답 시간을 단축한다. 이는 서버가 고부하 상황에서도 안정적으로 작동할 수 있도록 도와준다.
반응형