본문 바로가기

웹 개발/Spring Boot

SpringBoot) ThreadPoolExecutor 기본 개념

ThreadPool

서버가 어플리케이션에서 발생하는 모든 요청에 대해 매번 쓰레드를 생성하면 스레드를 생성하는 과정과 다수의 스레드를 스케줄링하느라 CPU가 바빠져 메모리 사용량이 늘어난다. 이는 전체적인 시스템 성능 저하를 초래하고 자원이 고갈되어 메모리풀로 서버가 다운 될 수 있다. 그래서 쓰레드풀을 사용한다.

쓰레드풀은 쓰레드를 미리 만들어 두고 재사용하는 방식으로 일정 수의 작업을 동시에 처리하도록 한다. 이때 사용될 수 있는 쓰레드 개수를 제한해놓고 작업 큐에 들어오는 작업들을 하나씩 쓰레드에 할당한다. 그리고 쓰레드가 한 테스크를 끝내면 다음 대기 1순위 태스크가 그 쓰레드를 재사용하는 방식으로 쓰레드의 전체 개수에는 영향을 주지 않고 쓰레드를 운영하여 시스템 성능 저하를 방지한다. 

자바는 스레드 풀을 생성하고 사용할 수 있는 클래스인 java.util.concurrent.Executors 클래스와  java.util.concurrent.ExecutorService 인터페이스를 제공한다. 

Task

  • Runnable() : 리턴값 없음, run() 메소드로 실행 
  • Callable() : 리턴값 있음, call()메소드로 실행 

이 객체를 ExecutorService의 작업큐에 넣으면 태스크 처리 요청을 하는 것이다. 

ExecutorService 

Executors는 ExecutorService의 구현체이며 쓰레드 풀 개수 및 종류를 정할 수 있으며 다음과 같은 메소드를 제공한다. 

  • newFixedThreadPool(int n) : n 만큼 고정된 크기의 쓰레드풀을 만든다. 
  • newCachedThreadPool() : 필요할 때 필요한 만큼의 쓰레드풀을 만든다. 이미 생성된 쓰레드를 재활용할 수 있어 성능이 좋다. 
  • newScheduledThreadPool(int n) : 일정 시간 뒤에 작업되거나 주기적으로 수행되는 작업을 할당할 수 있다. 
  • newSingleThreadExecutor() : 쓰레드가 1개인 ExecutorService를 반환하다. 싱글스레드에서 동작하는 작업 처리에 사용한다. 

Runnable또는 Callable 객체로 구현된 테스크를 작업 처리 요청을 하는 메소드를 제공한다. 

  • Runnable을 작업큐에 저장, 리턴값 없음, 작업 처리 도중 예외 발생 시 스레드가 종료되고 해당 스레드가 스레드풀에서 제거하고 새로운 스레드를 생성, 스레드 생성에 유발되는 오버헤드가 있음 
    • void execute(Runnable task) 
  • 작업 큐에 저장해서 반환된 작업 처리 결과를 Future로 반환, 작업 처리 도중 예외 발생 시 스레드는 종료되지 않고 다음 작업에 재사용됨. Future는 작업 완료될때까지 기다렸다가 최종 결과를 얻어오기 때문에 지연완료객체라고도 함 
    • Future<?> submit(Runnable task)  : task 처리 결과를 따로 리턴하지는 않고 스레드 작업 처리가 정상 완료됐는지 예외가 발생했는지를 알려주는데 사용함. 정상완료시 null 리턴, 예외 발생시 ExecutorException 발생 
    • Future<V> submit(Runnable task, V result)
    • Future<V> submit(Callable<V> task) : 작업 큐에 task를 저장하고 작업 결과를 Future<V>에 저장하여 리턴 

테스크는 요청 순서대로 처리 완료되지 않고 태스크의 양과 스레드 스케줄링에 따라 완료 순서가 결정된다. 이때 CompletionService.poll() 또는 CompletionService.take()를 사용하면 스레드풀에서 작업 처리가 완료된 것만 가져올 수 있다. 


ThreadPoolExecutor

자바는 쓰레드풀을 관리해주는 역할을 하는 ThreadPoolExecutor라는 클래스를 지원한다. 

ThreadPoolExecutor constructor

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) 
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) 
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) 
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
  • int corePoolSize : 기본 Pool 사이즈, 실행할 최소 쓰레드 수 
  • int maximumPoolSize : 최대 쓰레드 수 
  • long keepAliveTime : 쓰레드 미사용시 제거 대기 시간, corePoolSize 보다 쓰레드가 많아졌을 경우 maximumPoolSize까지 쓰레드가 생성될 수 있는데 이때 keepAliveTime만큼 초과된 쓰레드를 유지하고 그 이후에 다시 corePoolSize로 돌아간다. 
  • TimeUnit unit : keepAliveTime값의 시간 단위 ex. milliseconds, seconds
  • BlockingQueue <Runnable> workQueue : 요청된 작업들이 저장될 큐 (작업 대기열 관리), corePoolSize보다 작업 쓰레드가 많아져서 사용할 수 있는 쓰레드가 없는 경우 작업들은 workQueue에 대기한다. 

maximumPoolSize가 사용되는 시점

corePoolSize만큼의 쓰레드가 모두 사용되고 있는 경우, 새로운 쓰레드를 만드는 것이 아니라 workQueue로 들어가서 대시한다. 그러다가 workQueue도 가득 차면 그때 maximumPoolSize까지 확장한다. 그래서 workQueue 크기를 지정하지 않는다면 maximumPoolSize까지 확장될 일은 없게 되므로 주의해야 한다. 

예시 

  • task 10개
  • corePoolSize = 1 
  • workQueue = 10
  • maximumPoolsize = 5

위의 경우 초기에 1개의 스레드가 생성된다. 생성된 하나의 스레드가 1개의 태스크를 처리한다. 그리고 나머지 9개는 사용할 수 있는 스레드가 없으므로 작업큐에 대기한다. 큐에서 FIFO 로 처리된다. 만약에 작업큐 사이즈가 8이라면 남은 9개가 작업큐에 다 들어갈 수 없고 태스크 한 개이 대기할 곳이 없다. 이 경우에 maximumPoolSize인 5만큼 스레드 개수를 늘려 나머지 요청을 처리한다. 요청 처리 후 추가로 생성된 5개의 스레드가 사용되지 않은지 keepAliveTime(단위 : Time unit)만큼 지나면 5개를 제거하고 1개만 남겨 corePoolSize를 유지한다. 

 


References