💋 쓰레드 풀의 필요성
서버는 동시에 여러 사용자가 접속할 수 있습니다.
자바에서는 스레드를 운영 체제의 자원으로 사용합니다. 우리가 스레드를 계속해서 만들면, 운영 체제의 자원이 빨리 소진될 수 있어요.
서버는 동시 접속자가 많아지면 스레드가 무한대로 생성되면서 서버가 다운될 위험이 있습니다.
애플리케이션 프로세스에서 사용되고 있는 쓰레드의 개수를 관리하기 위해서 쓰레드 풀을 도입할 수 있습니다.
💋 쓰레드 풀이란?
✔ 개념
쓰레드 풀은 미리 일정 개수의 쓰레드를 생성하여 관리하는 기법입니다.
이렇게 생성된 쓰레드들은 작업을 할당받기 위해 대기 상태에 있게 되는데, 작업이 발생하면 대기 중인 쓰레드 중 하나를 선택하여 작업을 수행합니다. 작업이 완료되면 해당 스레드는 다시 대기 상태로 돌아가고, 새로운 작업을 할당받을 준비를 합니다.
쓰레드 풀을 사용하면 스레드 생성 및 삭제에 따른 오버헤드를 줄일 수 있으며, 특정 시점에 동시에 처리할 수 있는 작업의 개수를 제한할 수 있습니다. 이를 통해 시스템의 자원을 효율적으로 관리하고 성능을 향상시킬 수 있습니다.
쓰레드 풀의 장점을 정리해보면 아래와 같습니다.
✔ 쓰레드 풀의 장점
- 자원 효율성
- 쓰레드 풀은 미리 정해진 개수의 스레드를 생성하여 관리하기 때문에, 쓰레드 생성 및 삭제에 따른 오버헤드를 줄일 수 있습니다.
- 이는 시스템의 자원을 효율적으로 관리할 수 있고, 불필요한 자원 소모를 방지합니다.
- 응답성 및 처리량 향상
- 쓰레드 풀은 작업을 대기 상태로 유지하여 작업 처리 속도를 향상시킬 수 있습니다.
- 즉, 작업이 발생하면 대기 중인 쓰레드 중 하나를 선택하여 작업을 할당하므로, 작업 처리를 병렬로 진행할 수 있습니다.
- 작업 제어
- 쓰레드 풀을 사용하면 동시에 처리할 수 있는 작업의 개수를 제한할 수 있습니다.
- 즉, 쓰레드 풀의 크기를 조절하여 시스템의 부하를 조절하고, 과도한 작업 요청으로 인한 성능 저하를 방지할 수 있습니다.
- 쓰레드 관리
- 쓰레드 풀을 사용하면 쓰레드의 생명 주기를 관리할 수 있습니다.
- 쓰레드 풀은 스레드의 생성, 재사용, 종료 등을 관리하므로, 스레드의 안전한 운영을 도와줍니다.
✔ 웹 애플리케이션에 쓰레드 풀을 도입하기 좋은 이유
쓰레드풀은 동일하고 서로 독립적인 다수의 작업을 실행 할 때 가장 효과적이다.실행 시간이 오래 걸리는 작업과 금방 끝나는 작업을 섞어서 실행하도록 하면 풀의 크기가 굉장히 크지 않은 한 작업 실행을 방해하는 것과 비슷한 상황이 발생한다. 또한 크기게 제한되어 있는 쓰레드 풀에 다른 작업의 내용에 의존성을 갖고 있는 작업을 등록하면 데드락이 발생할 가능성이 높다. 다행스럽게도 일반적인 네트웍 기반의 서버 어플리케이션 (웹서버,메일서버,파일서버등)은 작업이 서로 동일하면서 독립적이어야 한다는 조건을 대부분 만족한다. - Java concurrency in practice 책 발췌
쓰레드풀은 동시에 실행되는 작업을 관리하는 데 사용되는 메커니즘입니다.
쓰레드풀은 작업을 처리하는 쓰레드들의 집합으로 구성되며, 이 쓰레드들은 작업이 도착하면 해당 작업을 실행하고 다음 작업을 기다리는 역할을 합니다.
실행 시간이 오래 걸리는 작업과 금방 끝나는 작업을 섞어서 실행할 때, 쓰레드풀의 크기가 충분히 크지 않다면 작업 실행을 방해하는 상황이 발생할 수 있습니다. 예를 들어, 한 작업이 오랜 시간 동안 실행되는 동안 다른 작업들이 대기해야 하므로 작업 처리 속도가 저하될 수 있습니다.
또한, 크기가 제한되어 있는 쓰레드풀에 다른 작업의 내용에 의존성을 갖고 있는 작업을 등록하면 데드락이 발생할 수 있습니다. 이는 작업 간의 순서에 따라 작업이 서로 기다리는 상황이 발생하여 작업이 진행되지 않는 상태를 의미합니다.
일반적으로, 네트워크 기반의 서버 애플리케이션들은 작업이 서로 동일하면서 독립적이어야 한다는 조건을 만족합니다. 예를 들어, 웹 서버는 각 요청에 대해 별도의 쓰레드를 할당하여 처리하므로, 작업 간의 의존성이 적기 때문에 쓰레드풀을 효과적으로 사용할 수 있습니다.
각 요청은 서로 다른 사용자나 데이터를 대상으로 하며, 요청 사이에 상호작용이 없는 경우가 많습니다. 웹 서버에서 각 클라이언트 요청은 웹 페이지를 응답하는 작업이며, 각 요청은 서로 다른 웹 페이지를 요청하고 서로 독립적으로 응답을 받습니다.
이제 자바에서의 쓰레드 풀에 대해 살펴봅시다!
차차 설명해보죠.
(아래 사진 코드는 예시이니 스킵해도 됨)
Java에서는 Executor 및 ExecutorService 인터페이스를 사용하여 다른 스레드 풀 구현과 작업을 처리합니다.
일반적으로 코드를 실제 스레드 풀 구현으로부터 분리하고 애플리케이션 전체에서 이러한 인터페이스를 사용하는 것이 좋습니다.
💋 Executor 인터페이스
- execute() 메서드 딱 한 개 가지고 있음.
- 실행 가능한(Runnable) 인스턴스를 실행하기 위한 메서드
- 코드 예시
Executor executor = Executors.newSingleThreadExecutor();
executor.execute(() -> System.out.println("Hello World"));
💋 ExecutorService 인터페이스
- ExecutorService 인터페이스에는 작업의 진행 상황을 제어하고 서비스의 종료를 관리하기 위한 많은 메소드가 포함
- 작업을 실행하고 반환된 Future 인스턴스를 사용하여 실행을 제어할 수 있음.
- 코드 예시
ExecutorService executorService = Executors.newFixedThreadPool(10);
Future<String> future = executorService.submit(() -> "Hello World");
// some operations
String result = future.get();
- ExecutorService를 생성하고 작업을 submit한 다음, 반환된 Future의 get 메소드를 사용하여 제출된 작업이 완료되고 값이 반환될 때까지 기다리고 있습니다.
- 물론 실제 시나리오에서는 일반적으로 future.get()을 즉시 호출하지 않고, 실제로 계산 값을 필요로 할 때까지 호출을 연기하고 싶을 것입니다.
- 물론 실제 시나리오에서는 일반적으로 future.get()을 즉시 호출하지 않고, 실제로 계산 값을 필요로 할 때까지 호출을 연기하고 싶을 것입니다.
💋 Executors
static 메서드로 다양한 형태의 쓰레드 풀을 제공합니다.
✔ 주요 개념
Executors 클래스 내 메서드들에서 공통적으로 사용하는 매개변수 중 이해할 필요가 있는 주요한 설정 매개변수는 corePoolSize, maximumPoolSize 및 keepAliveTime입니다.
쓰레드 풀은 항상 내부에 유지되는 고정된 수의 코어 스레드와, 필요하지 않을 때 생성되고 종료될 수 있는 여분의 스레드로 구성됩니다.
(The pool consists of a fixed number of core threads that are kept inside all the time. It also consists of some excessive threads that may be spawned and then terminated when they are no longer needed.)
corePoolSize (필수)
- 쓰레드 풀의 핵심 크기를 나타내는 값입니다.
- 쓰레드 풀이 유지해야 하는 최소한의 쓰레드 개수입니다.
- 예를 들어, corePoolSize가 5라면 최소한 5개의 쓰레드가 항상 유지됩니다.
maximumPoolSize (선택)
- 쓰레드 풀의 최대 크기를 나타내는 값입니다.
- 쓰레드 풀이 생성할 수 있는 최대 쓰레드 개수입니다.
- 새로운 작업이 들어오면 모든 코어 스레드가 busy하고 내부 큐가 가득 찬 경우, 풀의 쓰레드 개수는 maximumPoolSize까지 확장될 수 있습니다.
- 쓰레드 풀이 corePoolSize까지 쓰레드를 생성한 이후에는, 작업이 도착할 때마다 새로운 쓰레드를 생성하여 작업을 처리합니다.
- maximumPoolSize를 설정하면, 쓰레드 풀의 크기가 동적으로 조정될 수 있습니다.
keepAliveTime (선택)
- 작업하지 않고 놀고 있는 쓰레드(=비활성 쓰레드)가 유지될 최대 시간을 나타내는 값입니다.
- 이 옵션은 corePoolSize를 초과하고 maxPoolSize보다 작은 수로 쓰레드가 추가적으로 만들어진 경우에만 적용됩니다.
- 쓰레드 풀의 현재 쓰레드 개수가 corePoolSize보다 크고, 작업이 도착하지 않아 비활성 상태인 쓰레드가 발생한 경우, keepAliveTime 이후에는 해당 쓰레드를 종료시킵니다.
- 만약 keepAliveTime이 0이면, 비활성 상태의 쓰레드가 즉시 종료됩니다.
위의 파라미터와 관련해서 자주 사용되는 설정들은 Executors의 정적 메서드에 이미 정의되어 있습니다.
✔ Executors.newFixedThreadPool(int nThreads)
- corePoolSize: nThreads
- 항상 설정한 파라미터 값만큼의 쓰레드 개수를 유지함
- maximumPoolSize: nThreads
- Queue에 요청이 쌓인다고 해서 추가적으로 쓰레드를 생성하지 않음.
- keepAliveTime: 0L
- 어차피 corePoolSize를 초과해서 쓰레드가 만들어지지 않기 때문에 의미가 없는 설정임.
- 고정 크기의 스레드 풀을 생성합니다.
- workingQueue에 대한 설정에 있어서, Queue에 쌓일 수 있는 요청의 개수는 사실상 무제한
- Integer.MAX_VALUE
- 쓰레드 풀에 처리해야 하는 작업이 너무 많이 몰리면, Queue에 계속 쌓일 수 있는 잠재 위험 요인이 있음.
- 풀에 고정된 수의 스레드를 유지하며, 작업이 제출되면 사용 가능한 스레드가 있을 때 실행됩니다.
- 만약 모든 스레드가 작업에 바쁘다면, 나머지 작업은 대기 상태로 남게 됩니다.
- 이 스레드 풀의 크기는 생성 시에 지정되며, 작업이 많거나 많은 프로세서를 사용하는 경우 유용합니다.
테스트 코드를 통해 더 감을 잡아봅시다.
@Test
void testNewFixedThreadPool() {
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
executor.submit(logWithSleep("hello fixed thread pools"));
executor.submit(logWithSleep("hello fixed thread pools"));
executor.submit(logWithSleep("hello fixed thread pools"));
// 올바른 값으로 바꿔서 테스트를 통과시키자.
final int expectedPoolSize = 2;
final int expectedQueueSize = 1;
assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize());
assertThat(expectedQueueSize).isEqualTo(executor.getQueue().size());
}
1. newFixedThreadPool(2)를 호출하여 크기가 2인 고정 크기의 스레드 풀을 생성했습니다. 이는 동시에 실행될 수 있는 스레드의 최대 개수를 2로 지정하는 것을 의미합니다.
2. executor.submit(logWithSleep("hello fixed thread pools"))를 3번 호출하여 "hello fixed thread pools"을 출력하는 작업을 스레드 풀에 제출했습니다.
3. 하지만, 스레드 풀의 크기가 2로 고정되어 있으므로, 첫 번째 두 작업은 즉시 실행되었습니다. 그러나 세 번째 작업은 스레드 풀의 크기를 초과하므로 대기열에 들어가게 됩니다.
4. 따라서, 예상되는 poolSize는 2이고, 첫 번째 두 작업이 실행되었으므로 queueSize는 1이 됩니다.
✔ Executors.newCachedThreadPool()
- corePoolSize: 0
- 기본적으로 가지고 있는 코어 쓰레드의 개수는 0개임.
- 즉, 요청이 없으면 그냥 쓰레드 풀이 비어있을 수 있음.
- maximumPoolSize: Integer.MAX_VALUE
- Queue에 요청이 쌓이면, 무제한으로 계속 쓰레드를 생성함.
- keepAliveTime: 60L
- 생성된 쓰레드는 60초 동안 놀게 되면, 종료됨.
Executors.newCachedThreadPool은 동적으로 크기가 조정되는 스레드 풀을 생성합니다.
이는 실행되는 작업에 따라 스레드의 수를 자동으로 증가 또는 감소시킵니다.
만약 모든 스레드가 현재 작업에 바쁘다면, 새로운 스레드를 생성하여 작업을 처리합니다.
반면에 이전에 생성된 스레드가 일정 시간 동안 작업을 처리하지 않으면, 해당 스레드는 종료됩니다.
이 스레드 풀은 작업을 큐에 쌓아두고 순차적으로 처리하는 경우 유용합니다.
테스트 코드를 통해 더 감을 잡아봅시다.
@Test
void testNewCachedThreadPool() {
final var executor = (ThreadPoolExecutor) Executors.newCachedThreadPool();
executor.submit(logWithSleep("hello cached thread pools"));
executor.submit(logWithSleep("hello cached thread pools"));
executor.submit(logWithSleep("hello cached thread pools"));
// 올바른 값으로 바꿔서 테스트를 통과시키자.
final int expectedPoolSize = 3;
final int expectedQueueSize = 0;
assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize());
assertThat(expectedQueueSize).isEqualTo(executor.getQueue().size());
}
이 코드에서 `poolsize`가 3인 이유는 `executor.submit(logWithSleep("hello cached thread pools"))`를 세 번 호출했기 때문입니다. 각 호출마다 작업이 스레드 풀에 제출되고, `newCachedThreadPool()`은 필요한 만큼 스레드를 동적으로 생성합니다. 따라서 세 번의 작업이 스레드 풀에 제출되었으므로, 스레드 풀의 크기는 3이 됩니다.
`queuesize`가 0인 이유는 `newCachedThreadPool()`이 동적으로 크기가 조절되는 스레드 풀을 생성하기 때문입니다. 이 스레드 풀은 작업을 처리하기 위해 스레드를 생성하거나, 유휴 상태로 대기하는 스레드를 종료합니다. 따라서 작업이 제출되면 스레드 풀에서 바로 처리되고, 큐에 대기 중인 작업은 없으므로 `queuesize`는 0이 됩니다.
이외에도 편의를 위한 많은 정적 메서드들이 있는데, 내가 편리하도록 찾아서 사용하면 된다.
세밀한 설정이 필요하다면, 직접 생성자를 호출해서 사용해야 할 것이다.
더 많은 정적 메서드들에 대한 설명이 필요하다면, Baeldung의 글을 이 링크에서부터 아래로 내려보면 될 것 같다.
Java Concurrent Animated에서 다운로드 받아서, 자바 동시성 처리가 어떻게 되는지 애니메이션으로 볼 수 있다.
💋 참고자료
- https://hamait.tistory.com/937
- https://velog.io/@sihyung92/how-does-springboot-handle-multiple-requests
- https://www.baeldung.com/thread-pool-java-and-guava
- https://www.youtube.com/watch?v=B4Of4UgLfWc
도움이 되었다면, 공감/댓글을 달아주면 깃짱에게 큰 힘이 됩니다!🌟
'JAVA' 카테고리의 다른 글
[JAVA] Java Reflection API에서 클래스 이름 가져오기: getName() vs getCanonicalName() (0) | 2023.09.12 |
---|---|
[JAVA] 멀티 쓰레드의 동기화(synchronization): 메서드, 메서드 내 블록에 synchronized 키워드를 붙이자! (0) | 2023.09.10 |
[JAVA] 프로세스와 스레드: 개념, Java의 쓰레드 구현, I/O Blocking (0) | 2023.09.08 |
[JAVA] 좋은 객체 지향 설계의 5가지 원칙 (SOLID) (0) | 2023.05.01 |
[Spring] Spring Core(3): IoC Container의 개념, 생명 주기 (2) | 2023.04.23 |