Thread Pool

이종찬·2023년 2월 15일
0

📖 Thread Pool?

스레드 풀은 대기열(Queue), 작업 처리 스레드(thread)로 구성됩니다. 큐는 작업이 들어오면 대기하는 곳입니다. 스레드는 큐에 들어온 작업을 하나씩 처리하는 역할을 합니다.

여러 개의 스레드를 미리 생성하고 유지하는 것으로, 작업이 들어올 때 마다 스레드를 할당하여 작업을 처리합니다. 스레드를 생성하는 것은 시스템 자원을 많이 사용합니다. 따라서 생성과 소멸로 인한 오버헤드가 크기 때문에 해결책으로 스레드 풀을 사용합니다.

스레드 풀을 사용하면 스레드 생성과 소멸로 인한 오버헤드를 줄일 수 있으며 스레드의 재사용성을 높일 수 있습니다. 동시에 수행될 수 있는 스레드의 개수를 제어하여 너무 많은 스레드가 동시에 수행되어 시스템에 부하가 걸리는 것을 방지할 수 있습니다.


Spring에서의 Thread Pool

스프링 프레임워크에서는 ThreadPoolTaskExecutor를 사용하여 스레드 풀을 관리합니다. 해당 클래스는 Executor 인터페이스를 구현하여 스레드 풀을 생성하고, 작업 처리 스레드를 할당합니다.

스프링에서 @Async어노테이션을 사용하여 비동기 처리를 할 경우, 내부적으로 ThreadPoolTaskExecutor를 사용하여 스레드 풀을 관리합니다. 해당 클래스에는 CorePoolSize, MaxPoolSize, QueueCapacity 등의 설정을 통해 스레드 풀의 크기와 큐에 크기를 조절할 수 있습니다.

  • CorePoolSize : 스레드풀의 기본 스레드 수를 설정합니다. 해당 값을 넘지 않는 범위에서 요청된 작업은 즉시 처리됩니다.

  • QueueCapacity : Task Queue에 대기할 수 있는 최대 Task 개수를 설정합니다. 스레드풀이 다 처리하지 못한 작업들이 대기하는 곳입니다. 예를 들어 CorePoolSize = 10으로 설정되어 있는데 15개의 스레드 작업이 들어온 경우 QueueCapacity = 5가 됩니다.

  • MaxPoolSize : 스레드풀의 최대 스레드 개수를 설정합니다.

예제

@Configuration
public class AppConfig {
    //중요함
    // PoolSize를 모두 사용하는 경우 -> Queue에 내용이 들어간다.
    // Queue에 있는 것도 모두 사용하면 pool 사이즈가 지정한 만큼 늘어난다.
    // 해당 루틴이 반복되어 max값까지 늘어난다.
    @Bean("async-thread")
    public Executor asyncThread() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setMaxPoolSize(100);
        executor.setCorePoolSize(10);
        executor.setQueueCapacity(10);
        executor.setThreadNamePrefix("Async");
        return executor;
    }
}

스레드풀에 요청된 Task수가 현재 설정된 corePoolSize인 10보다 적은 경우에는 새로운 스레드를 생성하여 Task를 수행합니다.

요청된 Task수가 10이상 설정된 maxPoolSize인 100이하인 경우 새로운 스레드를 생성하여 Task를 수행할 수 있는지 확인합니다. TaskQueue에 들어갈 수 있으면 Queue에서 대기합니다. TaskQueue가 만약 가득 차있고 maxPoolSize보다 작은 상태라면 corePoolSize를 지정한 만큼 증가시킵니다. maxPoolSize보다 증가 하지 않으면 위의 루틴이 반복될 수 있습니다.

❌ 문제가 되는 경우

1. 지정한 최대 Thread를 넘어가는 경우

지정한 최대 Thread수를 넘어간다면 RejectedExecutionHandler가 정책에 따라 실행됩니다. RejectedExecutionHandler는 더이상 대기할 수 있는 Task가 없는 경우 실행이 됩니다.

스프링 부트의 ThreadPoolTaskExecutor에는 setRejectedExecutionHandler(RejectedExecutionHandler handler) 메서드를 통해 RejectedExecutionHandler를 다음과 같이 설정할 수 있습니다.

  • AbortPolicy : RejectedExecutionHandler를 발생시켜 새로운 Task의 추가를 거부합니다.
  • CallerRunsPolicy : 현재 스레드에서 Task를 처리하도록 합니다.
  • DiscardPolicy : Task를 무시합니다.
  • DiscardOldestPolicy : Task 큐에서 가장 오래된 Task를 제거하고, 새로운 Task를 추가합니다.

2. 설정에 따른 오버헤드

앞서 공부했던 것 처럼 스레드를 생성하고 소멸시키는 것은 오버헤드가 발생합니다. 스레드풀을 사용할 때 CorePoolSize, MaxPoolSize, QueueCapacity의 설정이 처리할 Task의 양과 맞지 않는다면 Task처리가 느려질 수 있습니다. 이는 곧 오버헤드 증가로 이어집니다.

3. Thread Blocking

Thread Blocking이 되면 해당 스레드는 Block된 상태로 대기하게 됩니다. 다른 Task를 처리할 수 있는 스레드가 남아있어도 해당 스레드가 Block 상태로 대기하고 있기 때문에 Task의 처리가 지연될 수 있습니다.

4. Thread Leak

Thread Leak은 스레드풀에서 사용하는 스레드가 제대로 종료되지 않아 메모리 누수가 발생하는 문제입니다. 스레드의 갯수를 늘리더라도 게속해서 Thread Leak 문제가 발생할 수 있기 때문에 적절한 조치가 필요합니다.

위와 같은 문제점들이 있기 때문에 스레드풀을 사용할 때에는 적절한 스레드풀 크기와 Task처리 방법을 고려해야 합니다.

✅ 요약

  • 스레드는 생성과 소멸에 많은 자원이 필요하기 때문에 스레드 풀을 사용한다.
  • 스레드 풀은 여러개의 스레드를 먼저 생성하고 유지하여 효율적으로 자원을 사용하는 목적을 가지고 있다.
  • 스레드 풀은 스레드가 대기하는 큐, 작업하는 스레드로 이루어져 있다.
  • ThreadPoolTaskExecutor를 사용하여 스레드풀을 관리하며 CorePoolSize, MaxPoolSize, QueueCapacity를 설정하여 스레드풀의 크기, 큐의 크기를 조절할 수 있다.
  • 스레드 풀에서 감당할 수 없는 정도에 Task가 들어오면 RejectedExecutionHandler를 통해 새로 들어온 Task의 대해 어떻게 처리할지 설정할 수 있다.
  • 스레드는 가장 중요한 역할을 하는 만큼 다루기 어렵고 문제도 많이 발생하기 때문에 문제점들을 고려하여 스레드풀 사용 및 Task 처리 방법을 고려해야 한다.
profile
왜? 라는 질문이 사라질 때까지

0개의 댓글