자바로 구현한 HTTP 서버에 스레드 풀을 적용시켜보자
📌 서론
이전 글에서 Java를 사용해 HTTP 요청을 처리하는 서버에 멀티스레딩 기능을 추가했다. 작성한 MultiThreadHttpServer 클래스는 각각의 요청에 대해 새로운 스레드를 생성하고 소멸시키는 방식을 채택하고 있다. 이 방법은 작은 규모의 서버에는 충분히 효과적일 수 있지만, 대규모 서버나 높은 트래픽을 처리하는 환경에서는 다수의 문제점을 야기할 수 있다. 예를 들어, 자원 관리의 복잡성 증가, 성능 저하, 그리고 스레드 생성과 소멸에 따른 오버헤드가 있다. 이에, 이번 글에서는 이러한 문제점을 해결하기 위해 '스레드 풀'을 적용하는 방법을 소개하려고 한다. 스레드 풀을 사용하면, 서버의 성능을 크게 향상시키고 자원 사용을 최적화할 수 있으며, 높은 트래픽에도 더욱 효율적으로 대응할 수 있다.
만약 이전 포스트를 읽지 않았다면 멀티스레딩을 적용한 서버 코드를 한번 보고 오는 것을 추천한다.👇🏻👇🏻
1. 멀티스레딩을 적용했던 MultiThreadHttpServer 클래스의 동작
MultiThreadHttpServer 클래스의 동작
- 클라이언트 연결 수락
- ServerSocket을 통해 클라이언트의 연결을 기다린다.
- ServerSocket을 통해 클라이언트의 연결을 기다린다.
- 새 스레드 생성 및 실행
- 연결이 성립되면 ClientHandler 객체를 생성하고, 이 객체를 새로 생성된 Thread에 할당하여 실행한다.
- 연결이 성립되면 ClientHandler 객체를 생성하고, 이 객체를 새로 생성된 Thread에 할당하여 실행한다.
- 병렬 처리
- 각 클라이언트 연결은 별도의 스레드에서 처리되므로, 다른 연결과 독립적으로 작업을 수행한다.
- 각 클라이언트 연결은 별도의 스레드에서 처리되므로, 다른 연결과 독립적으로 작업을 수행한다.
이 방식의 단점
- 스레드 오버헤드
- 각 요청마다 새로운 스레드를 생성하고 소멸시키는 과정은 시스템 자원을 상당히 소모하게 된다. 특히 많은 수의 클라이언트 요청을 동시에 처리해야 할 경우, 이러한 오버헤드는 성능 저하로 이어질 수 있다.
- 각 요청마다 새로운 스레드를 생성하고 소멸시키는 과정은 시스템 자원을 상당히 소모하게 된다. 특히 많은 수의 클라이언트 요청을 동시에 처리해야 할 경우, 이러한 오버헤드는 성능 저하로 이어질 수 있다.
- 메모리 사용량 증가
- 매 요청마다 새로운 스레드를 생성하면 메모리 사용량이 증가한다. 이는 서버의 메모리 용량에 부담을 주고, 시스템의 안정성을 위협할 수 있다.
- 매 요청마다 새로운 스레드를 생성하면 메모리 사용량이 증가한다. 이는 서버의 메모리 용량에 부담을 주고, 시스템의 안정성을 위협할 수 있다.
- 응답 시간 불안정
- 스레드 생성 및 소멸에는 시간이 걸리므로, 고객에게 일관된 응답 시간을 제공하는 데 어려움을 겪을 수 있다.
📌 결론
따라서 대규모 서버 또는 높은 트래픽 환경에서는 스레드 풀을 사용하는 것이 좀 더 효율적이다. 스레드 풀을 사용하면 미리 생성된 스레드 세트를 재사용하여 스레드 생성 및 소멸에 따른 오버헤드를 줄일 수 있다. 이러한 접근 방식은 서버의 성능을 개선하고 자원 사용을 최적화하는 데 도움이 된다.
2. 코드 작성: CustomThreadPool 클래스 작성
CustomThreadPool 클래스는 Java의 ThreadPoolExecutor를 활용하여 사용자 정의 스레드 풀을 구현한다. 이 클래스의 설정은 서버의 효율성과 성능에 직접적인 영향을 미친다.
corePoolSize (핵심 스레드 수)
- 이 값은 스레드 풀이 항상 유지하는 스레드의 수를 나타낸다. 이는 서버가 비교적 낮은 부하에서도 빠르게 응답할 수 있게 하는 기본 처리 능력을 보장한다. 너무 낮으면 서버가 요청을 즉시 처리하는 데 어려움을 겪을 수 있고, 너무 높으면 불필요한 리소스를 소모할 수 있다.
maximumPoolSize (최대 스레드 수)
- 스레드 풀이 운영할 수 있는 최대 스레드 수를 지정한다. 이 값은 서버가 고부하 상황에서도 요청을 처리할 수 있는 최대 능력을 정의한다. 너무 낮게 설정하면 피크 시간에 서버가 요청을 충분히 처리하지 못할 수 있고, 너무 높게 설정하면 시스템에 과부하가 걸릴 수 있다.
keepAliveTime (유휴 스레드 유지 시간)
- 이것은 추가적인 스레드들이 유휴 상태로 얼마나 오래 유지될 수 있는지를 결정한다. 이 시간이 지나면 더 이상 필요하지 않은 스레드는 종료된다.
queueCapacity (대기열 크기)
- 스레드가 모두 바쁠 때 대기하는 작업의 수를 정의한다. 큐가 가득 차면 새로운 작업은 거부되거나 대기 상태가 된다. 이 설정은 서버의 부하 관리와 관련이 있으며, 적절한 크기 설정은 서버의 안정성을 보장하는 데 중요하다.
CustomThreadPool 클래스 작성
- 이 클래스의 execute 메서드는 작업을 스레드 풀에서 실행하도록 하며, shutdown 메서드는 스레드 풀을 안전하게 종료한다.
package com.study.blog.http.threadpool;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Java의 ThreadPoolExecutor를 사용하여 커스텀 스레드 풀을 구현했다.
* 여기서 스레드 풀의 핵심 매개변수들(핵심 스레드 수, 최대 스레드 수, 유휴 시간, 큐 용량)을 설정한다.
*/public class CustomThreadPool {
private ThreadPoolExecutor threadPoolExecutor;
public CustomThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, int queueCapacity) {
this.threadPoolExecutor = new ThreadPoolExecutor(
corePoolSize, // 핵심 스레드 수: 이 값은 스레드 풀이 항상 유지하는 스레드 수를 결정한다.
maximumPoolSize, // 최대 스레드 수: 스레드 풀이 동시에 실행할 수 있는 최대 스레드 수를 지정한다.
keepAliveTime, // 유휴 스레드 유지 시간: 추가 스레드(핵심 스레드를 제외한 스레드)가 유휴 상태로 유지될 수 있는 시간을 결정한다.
TimeUnit.MILLISECONDS,// 시간 단위 : 스레드 풀에 들어오는 작업이 바로 처리될 수 없을 때 대기하는 큐의 크기를 결정한다.
new ArrayBlockingQueue<>(queueCapacity) // 작업 대기열
);
}
public void execute(Runnable task) {
threadPoolExecutor.execute(task);
}
public void shutdown() {
threadPoolExecutor.shutdown();
}
// 필요한 경우 다른 ThreadPoolExecutor 메서드 추가
}
3. 코드 작성: ClientHandler 클래스 작성 (이전과 멀티스레딩 적용 코드와 동일)
아래 포스트의 1~2번 내용을 확인하고 오도록 하자 (이전 글과 중복되는 내용이라 여기에는 적지 않았다.)
4. ThreadPoolMultiThreadHttpServer 클래스 작성 (스레드 풀 사용)
ThreadPoolMultiThreadHttpServer 클래스는 스레드 풀을 활용하여 서버의 처리 능력을 향상시키는 핵심 구성 요소다. 스레드 풀의 사용은 서버의 자원 관리와 성능 최적화에 큰 장점을 제공한다.
스레드 풀 사용의 장점
- 효율적인 자원 관리
- 스레드 풀을 사용하면 미리 정의된 수의 스레드를 재사용함으로써 스레드 생성 및 소멸에 따른 오버헤드를 크게 줄일 수 있다. 이는 특히 고부하 상황에서 서버의 성능을 보장한다.
- 스레드 풀을 사용하면 미리 정의된 수의 스레드를 재사용함으로써 스레드 생성 및 소멸에 따른 오버헤드를 크게 줄일 수 있다. 이는 특히 고부하 상황에서 서버의 성능을 보장한다.
- 메모리 오버헤드 감소
- 스레드 풀은 불필요한 스레드 생성을 방지함으로써 메모리 사용량을 최적화한다. 각 스레드는 일정량의 메모리를 차지하기 때문에, 스레드 수를 제어하는 것은 메모리 효율성을 높이는 데 중요하다.
- 스레드 풀은 불필요한 스레드 생성을 방지함으로써 메모리 사용량을 최적화한다. 각 스레드는 일정량의 메모리를 차지하기 때문에, 스레드 수를 제어하는 것은 메모리 효율성을 높이는 데 중요하다.
- 부하 분산
- 스레드 풀은 작업 부하를 스레드 간 균등하게 분산시켜 처리한다. 이는 서버가 갑작스러운 트래픽 증가에도 안정적으로 대응할 수 있게 한다.
package com.study.blog.http.threadpool;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolMultiThreadHttpServer {
public static void main(String[] args) {
CustomThreadPool customThreadPool = new CustomThreadPool(
10, // corePoolSize
20, // maximumPoolSize
5000,// keepAliveTime
100 // queueCapacity
);
try (ServerSocket serverSocket = new ServerSocket(8080)) {
System.out.println("Listening for connection on port 8080 ....");
while (!serverSocket.isClosed()) {
try {
Socket clientSocket = serverSocket.accept();
customThreadPool.execute(new ClientHandler(clientSocket));
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
customThreadPool.shutdown();
}
}
}
만약 스레드 풀을 사용하지 않을 경우, 서버는 각 요청마다 새로운 스레드를 생성하고 소멸시켜야 한다. 이는 성능 저하, 메모리 낭비 및 불안정한 응답 시간을 초래할 수 있다. 따라서 스레드 풀을 적절히 관리하는 것은 고성능 서버 구축에 있어 필수적인 요소가 된다.
5. ThreadPoolMultiThreadHttpServer 클래스 분석
메인 메서드 및 스레드 풀 설정
- main 메서드는 프로그램 실행의 시작점이다. 이 안에서 CustomThreadPool 객체를 생성한다. 이 객체는 스레드 풀의 크기, 최대 스레드 수, 스레드 유휴 시간 및 큐 용량을 설정한다.
public static void main(String[] args) {
CustomThreadPool customThreadPool = new CustomThreadPool(
10, // corePoolSize
20, // maximumPoolSize
5000,// keepAliveTime
100 // queueCapacity
);
// 서버 소켓 생성 및 클라이언트 연결 수락 로직
}
서버 소켓 생성 및 클라이언트 연결 수락
- 8080 포트에서 클라이언트 연결을 기다리는 ServerSocket을 생성한다. 클라이언트 연결이 수락되면, 각 클라이언트 연결을 CustomThreadPool을 통해 생성된 스레드로 처리한다. 이는 스레드를 효율적으로 관리하고, 서버의 자원 사용을 최적화한다.
try (ServerSocket serverSocket = new ServerSocket(8080)) {
System.out.println("Listening for connection on port 8080 ....");
while (!serverSocket.isClosed()) {
try {
Socket clientSocket = serverSocket.accept();
customThreadPool.execute(new ClientHandler(clientSocket));
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
customThreadPool.shutdown();
}
📌 내용 요약
이 코드는 스레드 풀을 활용하여 효율적으로 스레드를 관리하는 멀티스레딩 HTTP 서버를 구현한다.
CustomThreadPool을 사용하여 서버의 성능을 최적화하고, 자원을 효율적으로 사용한다.
서버 소켓은 클라이언트의 연결을 수락하고, customThreadPool을 통해 각 요청을 스레드 풀에 할당한다.
📌 마무리
스레드 풀 사용의 핵심 이점은 효율적인 자원 관리와 성능 최적화다. 스레드 풀을 통해 미리 정의된 수의 스레드를 재사용함으로써, 스레드 생성 및 소멸에 따른 오버헤드를 크게 줄일 수 있다. 이는 특히 고부하 상황에서 서버의 성능을 보장한다.
메모리 오버헤드 감소와 부하 분산 역시 스레드 풀의 주요 이점으로, 서버가 갑작스러운 트래픽 증가에도 안정적으로 대응할 수 있게 한다.
결론적으로, 스레드 풀은 고성능 서버 구축에 있어 필수적인 요소로, 성능과 안정성을 극대화하는 데 중요한 역할을 한다.
스레드 풀을 적용한 java로 만든 http 서버의 성능이 궁금하다면?👇🏻👇🏻
지금까지 작성한 코드는 "blog/http/threadpool" 패키지에 들어가 보시면 확인하실 수 있습니다.
'JAVA' 카테고리의 다른 글
[Java] HTTP 서버 만들기: 멀티스레딩과 네트워크 최적화하기 (39) | 2024.01.13 |
---|---|
[JMeter] 멀티스레딩 vs 스레드 풀: Java로 만든 HTTP 서버 성능 테스트 (27) | 2024.01.03 |
[Java] HTTP 서버 만들기: 멀티스레딩 적용 (1) | 2024.01.02 |
[JMeter] MacOS(M1)에서 JMeter를 이용한 부하 테스트 (26) | 2024.01.01 |
[Java] HTTP 서버 만들기: GET, POST, PUT 요청별 처리 (28) | 2023.12.30 |