[Spring] 비동기 프로그래밍 - ThreadPoolExcutor

비동기 프로그래밍이란?

- Async 한 통신

- 실시간성 응답을 필요로 하지 않는 상황에서 사용 (ex. Notification, Email 전송, Push 알림)

- Main Thread가 Task를 처리하는 게 아니라 Sub Thread에게 비동기로 처리 해야 되는 Task를 위임하는 행위라고 할 수 있다.

 

그러면 Main Thread는 알아서 자동으로 처리해줄텐데,

Sub Thread는 어떻게 생성하고 관리를 할 수 있을까?

 

스프링 프레임워크를 기준으로 설명하면,

스프링에서 비동기 프로그래밍을 하기 위해서는 우선, ThreadPool을 정의할 필요가 있다.

 

왜? 스레드풀을 생성해야할까?

 

비동기는 앞서 말했듯 Main Thread가 아닌 Sub Thread에서 작업이 진행된다. 

자바에서는 ThreadPool을 생성하여 Async(비동기) 작업을 처리한다. 

따라서 자바를 사용하는 스프링 프레임워크 또한 ThreadPool을 생성해야하는 것이다. 

 

 ThreadPool 생성 옵션

스레드 풀을 생성하는데 다양한 옵션이 있는데 대표적으로 아래의 옵션들이 있다.

  • CorePoolSize - 최소한의 스레드를 몇 개 가지고 있을지를 지정하는 옵션
  • MaxPoolsize - 최대 몇 개까지의 스레드를 할당할 것인지를 지정하는 옵션
  • KeepAliveTime - 얼마만큼 스레드가 작업을 하지 않으면 자원을 반납할지 시간을 지정하는 옵션
  • Unit - Long 타입인 KeepAliveTime이 시간인지, 분, 초인지 단위를 지정하는 옵션 
  • WorkQueue - 요청이 들어오면 보통은 모든 스레드가 바로 처리를 할 수 없기에 먼저 들어온 요청을 먼저 처리할 수 있는 자료구조인 큐를 사용하여 WorkQueue에 많은 요청들을 넣어둔 뒤, 현재 작업 중인 스레들이 현재 Task가 마무리되면 스레드에서 다음 작업을 할 작업들을 Task를 가져오게 된다.

ThreadPool에서 스레드가 생기는 순서에 대해 잘 알아야 한다. 

우선, 요청이 들어오면 CorePoolSize만큼 스레드가 생성되고,

그 이후 요청들은 WorkQueue의 Size만큼 넣어둔다. 

그러다가 WorkQueue가 전부 차면 그 때 MaxPoolSize만큼 새로운 스레드를 생성한다. 

 

ThreadPoolExcutor 생성자 및 사용 예시

ThreadPoolExcutor 생성자

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

 

사용 예시

ThreadPoolExecutor executor =
            new ThreadPoolExecutor(5, 10, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<>(50));

위의 코드는 스레드 풀을 생성하는 샘플 코드인데 이를 설명할 줄 알아야 한다. 

 

최소 5개의 스레드를 가지고 있고,

큐에 task가 다 쌓였다면 10개까지 스레드를 생성할 것이며,

5개 초과된 6번째부터 10번째의 스레드의 경우에는 스레드들이 3초 동안 작업을 하지 않으면 다시 스레드를 반납하고,

큐에는 최대 50개까지의 task(request)를 담고 있는 스레드 풀이다. 

 

ThreadPool 생성 시 주의해야할 부분

1. CorePoolSize 값을 너무 크게 설정할 경우 Side Effect을 고려해야 한다. 

CorePoolSize는 그 수만큼은 무조건 스레드 자원을 점유하기에 너무 큰 값을 설정해놓는다면,

많은 스레드를 점유하고 있을 수 있기에 적절한 값을 조절해서 설정할 필요가 있다.

 

2. IllegalArgumentException(+NullPointerExceptioin) 을 조심해야한다. 

IllegalArgumentException은 아래의 4가지 경우들 중 하나라도 해당될 때 발생한다. 

 

  • corePoolSize < 0  
    메인 스레드가 스레드풀에 어떤 태스크를 위임을 하는데, 해당 스레드풀에는 스레드가 없으니 당연히 작업을 하지 못하기에 예외가 발생한다. 
  • keepAliveTime < 0 
    corePoolSize에서 스레드가 더 필요해서 새로운 스레드를 생성하려고 하는데, keepAliveTime이 0보다 작기에 생성을 하기도 전에 스레드가 반납되어야 한다. 즉, 생성되자마자 죽기 때문에 예외가 발생한다.
  • maximumPoolSize <= 0
    corePoolSize가 maximumPoolSize보단 작을텐데 그러면 corePoolSizer가 0보다 작을 수 밖에 없다는 것이다.그러면 스레드풀에는 단 하나의 스레드도 없기에 예외가 발생한다.
  • maximumPoolSize < corePoolSize
    애초에 말이 안되기에 예외 발생

NullPointerException (workQueue가 null인 경우)

corePoolSize만큼 스레드를 생성하고 큐에다가 다음 요청들을 담는다고 했는데,

workQueue가 정의되어 있지 않는 null에다가 데이터를 넣게되기에 당연히 NullPointerException이 발생하게 된다.

 

ThreadPool 정리

CorePoolSize

if ( Thread 수 < CorePoolSize )
    New Thread 생성

if ( Thread 수 > CorePoolSize )
    Queue에 요청 추가

 

MaxPoolSize

if ( Queue Full && Thread 수 < MaxPoolSize )
    New Thread 생성
    
if ( Queue Full && Thread 수 > MaxPoolSize )
    요청 거절
}

더불어 요청 거절이 아닌 다른 방법을 핸들링 할 수는 있다. 


요청을 거절할건지(Exception을 터뜨릴건지) 아니면 해당 요청을 무시할 것인지
ThreadPoolExcutor 생성자에서 exception까지 핸들링하는 설정을 하면 컨트롤할 수 있다.

 

(+추가로) RejectedExcutionHandler 옵션

RejectedExecutionHandler: 스레드 경계 및 대기열 용량에 도달하여 실행이 차단될 때 사용할 핸들러를 지정해줄 수 있는 옵션이다.

 

RejectedExecutionHandler는 스레드 풀에서 허용할 수 없는 작업을 처리하는 방법을 사용자 정의하기 위한 옵션이다.

자바는 다음을 포함한 몇 가지 기본 거부 정책을 제공합니다.

  • ThreadPoolExecutor.AbortPolicy: 거부 시 RejectedExecutionException을 발생시킨다.
  • ThreadPoolExecutor.CallerRunsPolicy: 스레드 풀이 아닌 호출자의 스레드에서 작업을 실행한다.
  • ThreadPoolExecutor.DiscardPolicy: 거부된 작업을 자동으로 삭제한다.
  • ThreadPoolExecutor.DiscardOldestPolicy: 처리되지 않은 가장 오래된 요청을 삭제한 다음 현재 작업을 다시 실행시키려고 시도한다. 

더불어 위의 정책이 요구사항을 충족하지 못할 경우를 대비하여

RejectedExcutionHandler 인터페이스를 구현하여 직접 거부 동작을 구현할 수 있다.

또한 거부를 기록하거나, task를 보조 실행자에게 제출을 시도하거나, 백오프 및 재시도 메커니즘 구현이 포함될 수 있다.

 

 

백오프란?

백오프는 요청 또는 재시도 비율을 일시적으로 줄여 오류 또는 리소스 포화를 처리하는데 사용되는 전략을 의미한다.