[JAVA] new Thread()말고 ThreadPoolTaskExecutor

무지성개발자·2023년 8월 25일
0

ThreadPoolTaskExecutor

ThreadPoolTaskExecutor란 스레드 풀을 생성하고 관리하는 역할하는 클래스로 스프링에서 제공한다. 주로 비동기 작업에 사용하고, 반복적이고 오래걸리는 작업을 여러 스레드로 나눠서 한번에 처리할 때 사용한다.

ThreadPool이란?
스레드 여러개를 미리 만들어 놓고 관리하는 곳을 말한다. 일반 자바를 사용해서 멀티스레드 처리를 하면 new Thread()로 일일히 생성해야하는데 ThreadPool을 사용하면 이미 만들어진 스레드들을 사용하고 반납해서 재활용도 가능하다.

적용 예시

다음 예시는 spring boot를 사용한다.

@Configuration
public class TaskExcutorConfig {
    @Bean
    public ThreadPoolTaskExecutor executor(){
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setThreadNamePrefix("ThreadPool Thread - ");
        return threadPoolTaskExecutor;
    }
}
public class ExcutorController {
    private final ThreadPoolTaskExecutor executor;

    @GetMapping()
    public void excutorThread() {
        log.info("executing threads...");
        Runnable r = () -> {
            try {
                log.info(Thread.currentThread().getName() + ", Now sleeping 10 sec...");
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        };

        for (int i = 0; i < 10; i++) {
            executor.execute(r);
            log.info("poolSize : " + executor.getCorePoolSize()
                    + ", activeCount : " + executor.getActiveCount()
                    + ", queueSize : " + executor.getThreadPoolExecutor().getQueue().size()
            );
        }
    }
}

TaskExcutorConfigThreadPoolTaskExecutor를 빈으로 등록하고 간단하게 컨트롤러에서 작업을 진행하였다.

코드를 잠깐 설명하자면 ThreadPoolTaskExecutor의 스레드를 사용하면 스레드 이름이 ThreadPool Thread -로 나오도록 이름을 설정했고, 컨트롤러에서 작업을 10번 하도록 했다. 결과는 아래와 같다.
로그를 보면 직접 설정한 ThreadPoolTaskExecutor에서 스레드를 사용하는걸 볼 수 있다.

여기서 눈여겨 봐야할 것은 poolSize, activeCount, queueSize인데 queueSize가 계속 올라간걸 볼 수 있다.

  • poolSize : corePoolSize의 갯수를 표시.
    • setCorePoolSize()메서드로 설정할 수 있는데 아무것도 사용안한 우리는 기본값인 1을 사용해서 1로 표기됨.
  • activeCount : corePoolSize에서 설정한 스레드 갯수 중에서 몇 개나 사용 중인지 표시.
  • queueSize : queue에 쌓인 대기중인 작업 갯수.
    • ThreadPoolTaskExecutor는 corePoolSize보다 많은 작업을 시키면 corePoolSize의 갯수만큼 동시에 일을 시키고 나머지 작업은 queue에 올려두고 작업이 끝난 스레드부터 차례대로 꺼내서 작업하도록 한다.
    • 기본 queueSize는 integer최댓값이며 setQueueCapacity()메서드로 설정 가능.
    threadPoolTaskExecutor.setCorePoolSize(3);
    threadPoolTaskExecutor.setQueueCapacity(5);

TaskExcutorConfig에 위 코드를 추가해서 다시 실행해보자.
똑같진 않아도 비슷하게 로그가 나올 텐데 뭔가 이상하다고 느꼈다면 고수다.

이상하다고 느껴야하는 부분은 PoolSize부분이다. corePoolSize를 3으로 설정했는데 Thread이름을 보니 5까지 사용 중 이다. 왜 설정한 값보다 많은 쓰레드를 사용할까?

이유는 QueueCapacity를 적게 설정해서 그렇다.
작업을 10번을 시켰는데 corePoolSize는 3으로 설정했다. 그래서 3개의 쓰레드가 동시에 작업하고 나머지 작업은 queue에 올려둔다. 그럼 queue사이즈 5 + 쓰레드 갯수 3개를 해도 8개의 작업만 가능하다. 그럼 나머지 2개의 일은 어떻게 처리할까?

  • ThreadPoolTaskExcutor는 core사이즈 만큼 일을 시키고 나머지 작업은 queue에 올린다.
  • queue에 작업이 다 찼는데, 작업이 남는다면 maxPoolSize 한도 내에서 쓰레드를 생성해서 작업을 시킨다.

이런 과정 때문에 corePoolSize를 넘은 쓰레드가 동작한 것이다.

RejectedExecutionException

만약 maxPoolSize까지 쓰레드를 생성하고 queueSize도 꽉 찾는데 추가 작업이 들어온다면 RejectedExecutionException예외가 발생한다.

RejectedExecutionException은 RejectedExecutionHandler을 사용해서 핸들링 해줘야 한다. setRejectedExecutionHandler() 메소드를 통해 할 수 있다.

  • AbortPolicy : RejectedExecutionException을 발생시킴. default임.

  • DiscardOldestPolicy : 오래된 작업을 skip. 모든 task가 무조건 처리되어야 할 필요가 없을 경우 사용.

  • DiscardPolicy : 처리하려는 작업을 skip. 모든 task가 무조건 처리되어야 할 필요가 없을 경우 사용.

  • CallerRunsPolicy : shutdown 상태가 아니라면 ThreadPoolTaskExecutor에 요청한 thread에서 직접 처리 예외와 누락 없이 최대한 처리하려면 이 옵션을 추천.

Shutdown과 Timeout

만약 프로그램이 종료되면 아직 처리되지 못한 task는 유실되게 됩니다. 유실 없이 마지막까지 다 처리하고 종료되길 원한다면 설정을 추가해야 한다.

  • setWaitForTasksToCompleteOnShutdown() : true로 설정하면 queue의 모든 작업이 끝날 때 까지 기다린다.

  • setAwaitTerminationSeconds() : 모든 작업이 처리되길 기다리기 힘든 경우라면 최대 종료 대기 시간을 설정할 수 있다.


한 줄평 : 비동기로 작업 하고 싶다면 TaskExcutorConfig에 @EnableAsync를 추가하고 비동기 작업을 할 곳에 @Async를 붙여주면 된다.

참고 -
https://kapentaz.github.io/spring/Spring-ThreadPoolTaskExecutor-%EC%84%A4%EC%A0%95/#
https://kim-jong-hyun.tistory.com/104

profile
no-intelli 개발자 입니다. 그래도 intellij는 씁니다.

0개의 댓글