λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°
  • μž₯원읡 κΈ°μˆ λΈ”λ‘œκ·Έ
πŸ’Š Java & Kotlin & Spring/- spring framework +

Spring μ—μ„œ @Async λ₯Ό μ‚¬μš©ν•  λ•ŒλŠ” ThreadPoolTaskExecutor λ₯Ό λ“±λ‘ν•΄μ£Όμž

by Wonit 2024. 4. 17.

TL;DR

 

이번 κΈ€μ˜ 핡심을 μš”μ•½ν•˜λ©΄ λ‹€μŒκ³Ό κ°™λ‹€

 

  • @Async 은?
    • Spring μ—μ„œ 비동기 μž‘μ—…μ„ μ‰½κ²Œ μ‹€ν–‰ν•  수 μžˆλ„λ‘ λ„μ™€μ£ΌλŠ” κΈ°λŠ₯
  • μ™œ ThreadPoolTaskExecutor λ₯Ό 등둝해야 ν• κΉŒ?
    • Spring 의 κΈ°λ³Έ 비동기 처리 Executor λŠ” 맀번 μƒˆλ‘œμš΄ thread λ₯Ό 생성
    • ThreadPool 을 μ΄μš©ν•  ν•„μš”κ°€ 있음
  • 사전지식
    • Executor 와 ThreadPoolExecutor
    • Spring 의 TaskExecutor
  • ThreadPoolExecutor μ΄ν•΄ν•˜κΈ°
    • μ •ν™•νžˆλŠ” ThreadPoolExecutor μ•ˆμ—μ„œ μ–΄λ–€ λ‘œμ§μ— μ˜ν•΄ μƒˆλ‘œμš΄ thread λ₯Ό λ§Œλ“œλŠ”μ§€ μ•Œμ•„μ•Ό 함
    • 섀정에 따라 single thread 처럼 λ™μž‘ν•  수 있음
  • μ μ ˆν•œ ThreadPoolTaskExecutor 의 μ„€μ • κ°’ μ°ΎκΈ°
    • μ—°μ‚°μ˜ μ’…λ₯˜μ— 따라 CPU bound / IO bound 둜 ꡬ뢄
    • Java Concurrency in Practice μ—μ„œ μ œμ•ˆν•˜λŠ” 계산법
    • μ€‘μš”ν•œ 건 직접 μ„±λŠ₯ ν…ŒμŠ€νŠΈλ₯Ό 톡해 ν™•μΈν•˜κΈ°

 

Spring μ—μ„œ @Async μ–΄λ…Έν…Œμ΄μ…˜

 

Spring 으둜 Application 을 κ°œλ°œν•˜λ‹€ 보면 비동기 task λ₯Ό μ‹€ν–‰ν•΄μ•Ό ν•  일이 μ’…μ’… μžˆλ‹€.

 

λ‹€μŒκ³Ό 같이 @EnableAsync 와 @Async λ₯Ό μ΄μš©ν•΄μ„œ Spring μ—μ„œ 비동기 task λ₯Ό 많이 처리 ν•  것이닀.

 

 

λ‹€λ₯Έ λ³΄ν†΅μ˜ AoP λ‚˜ Spring abstraction 의 μ‚¬μš©λ²•κ³Ό λΉ„μŠ·ν•˜κ²Œ, bean 으둜 λ“±λ‘λœ 클래슀의 μ–΄λ–€ λ©”μ„œλ“œκ°€ @Async μ–΄λ…Έν…Œμ΄μ…˜μœΌλ‘œ decorate λ˜μ—ˆλ‹€λ©΄ Spring 은 ν•΄λ‹Ή method λ₯Ό proxy λ₯Ό μ΄μš©ν•˜μ—¬ μš”μ²­ thread 와 λ³„κ°œμ˜ thread μ—μ„œ ν•΄λ‹Ή λ©”μ„œλ“œλ₯Ό μˆ˜ν–‰ν•  수 있게 ν•œλ‹€.

 

μ™œ ThreadPoolTaskExecutor bean 을 등둝해야 ν• κΉŒ?

 

그럼 본둠으둜 λ“€μ–΄κ°€μ„œ, @Async λ₯Ό μ‚¬μš©ν•  λ•Œμ—λŠ” μ™œ ThreadPoolTaskExecutor λ₯Ό μ΄μš©ν•΄μ•Ό ν• κΉŒ?

 

κ·Έ μ΄μœ λŠ” @EnableAsync μ–΄λ…Έν…Œμ΄μ…˜μ—μ„œ μ‰½κ²Œ 확인 ν•  수 μžˆλ‹€.

 

 

  • 기본적으둜 @EnableAsync μ–΄λ…Έν…Œμ΄μ…˜μ„ μ‚¬μš©ν•  경우 TaskExecutor νƒ€μž…μ˜ bean 을 찾음
  • λ§Œμ•½ user κ°€ λ“±λ‘ν•œ custom bean 이 μ‘΄μž¬ν•˜μ§€ μ•Šμ„ 경우 SimpleAsyncTaskExecutor λ₯Ό μ‚¬μš©ν•¨
  • SimpleAsyncTaskExecutor μ—μ„œλŠ” thread λ₯Ό reuse ν•˜μ§€ μ•ŠμŒ

 

즉, 비동기 task κ°€ λ°œμƒν•  λ•Œλ§ˆλ‹€ κ³„μ†ν•΄μ„œ thread λ₯Ό μƒμ„±ν•˜κΈ° λ•Œλ¬Έμ΄λ‹€.

 

thread λ₯Ό μƒμ„±ν•˜λŠ” μž‘μ—…μ€ 맀우 λΉ„μ‹Ό μž‘μ—…μ΄λΌλŠ” 사싀은 맀우 유λͺ…ν•œλ°, μš°λ¦¬κ°€ ThreadPool 을 μ‚¬μš©ν•˜κ² λ‹€κ³  λͺ…μ‹œν•˜μ§€ μ•ŠμœΌλ©΄ Spring 은 기본적으둜 SimpleAsyncTaskExecutor λ₯Ό 비동기 task executor 의 κΈ°λ³Έ κ΅¬ν˜„μ²΄λ‘œ 등둝 ν•˜κΈ° λ•Œλ¬Έμ— μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ μ„±λŠ₯ ν•˜λ½μ΄ λ°œμƒ ν•  수 μžˆλ‹€.

 

μ™œ ThreadPool 을 μ‚¬μš©ν•΄μ•Ό ν•˜λŠ”κ°€μ— λŒ€ν•œ μ΄μ•ΌκΈ°λŠ” ν•˜μ§€ μ•Šκ² λ‹€. μ΄μœ κ°€ κΆκΈˆν•˜λ‹€λ©΄ 10 λΆ„ ν…Œμ½”ν†‘ ThreadPool - μš°ν…Œμ½” youtube μ—μ„œ 확인할 수 μžˆλ‹€.

 

λͺ¨λ“  μž‘μ—…μ—μ„œ thraedPool 을 μ‚¬μš©ν•˜λŠ” 것이 항상 쒋은 μ„±λŠ₯을 λ‚΄μ§€λŠ” μ•Šμ§€λ§Œ (CPU bound μž‘μ—…κ³Ό 같은) μš°λ¦¬κ°€ λ§Œλ“œλŠ” λŒ€λΆ€λΆ„μ˜ application 은 IO bound κ°€ 많기 λ•Œλ¬Έμ— 비동기 μ²˜λ¦¬λŠ” threadPool 을 μ΄μš©ν•˜λŠ” 편이 μ’‹λ‹€

 

ThreadPoolTaskExecutor 이야기 ν•˜κΈ° 전에 사전지식

 

κ·Έ 전에, 짧은 사전 지식을 이야기 해보며 java 의 Executor 와 ExecutorService 그리고 Spring 의 TaskExecutor 에 λŒ€ν•΄μ„œ μ•Œμ•„λ³΄μž

 

사전지식 1. java 의 Executor 와 ExecutorService

 

java5 버전이 μΆœμ‹œλ  λ•Œ java.util.concurrent νŒ¨ν‚€μ§€ threading, scheduling κ³Ό 같은 비동기 μž‘μ—…μ„ 더 μ‰½κ²Œ ν•  수 μžˆλ„λ‘ Executor λΌλŠ” μΈν„°νŽ˜μ΄μŠ€κ°€ μΆ”κ°€λ˜μ—ˆλ‹€.

 

  • κ°œλ°œμžλŠ” λͺ…μ‹œμ μœΌλ‘œ new Thread λ₯Ό 톡해 μŠ€λ ˆλ“œλ₯Ό μƒμ„±ν•˜μ§€ μ•Šμ•„λ„ 됨
  • runnable μž‘μ—…μ„ μ‰½κ²Œ μ‹€ν–‰ν•  수 μžˆλ„λ‘ λ„μ™€μ€Œ
  • μŠ€λ ˆλ“œ 생λͺ…μ£ΌκΈ°λ₯Ό μ• ν”Œλ¦¬μΌ€μ΄μ…˜ κ°œλ°œμžλ“€μ΄ μ‰½κ²Œ 관리할 수 μžˆλ„λ‘ 함

 

ExecutorService λŠ” Executor μΈν„°νŽ˜μ΄μŠ€λ₯Ό ν™•μž₯ν•˜μ—¬ 비동기 task 의 싀행을 더 쉽고 ν™•μž₯μ„± μžˆλ„λ‘ κ΄€λ¦¬ν•˜λŠ” ν™•μž₯ μΈν„°νŽ˜μ΄μŠ€μ΄λ‹€.

 

  • task 싀행이 λ”μš± κ°„νŽΈν•΄μ§
  • Executors λΌλŠ” static factory class λ₯Ό 톡해 μ—¬λŸ¬ νƒ€μž…μ˜ ExecutorService μΈμŠ€ν„΄μŠ€λ₯Ό 생성할 수 μžˆλ„λ‘ 함
    • fixedThreadPool, singleThread λ“±λ“± μ‰½κ²Œ executor λ₯Ό 생성할 수 있음

 

이 두 클래슀λ₯Ό 잘 μ΄μš©ν•œλ‹€λ©΄ λ‹€μŒκ³Ό 같이 μ‰½κ²Œ threading 을 ν•  수 있게 λœλ‹€

 

public class ExecutorExample {
  public static void main(String[] args) {
    ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.execute(() -> {
        System.out.println("비동기 μž‘μ—… μ‹€ν–‰");
    });

    executor.shutdown();
  }
}

 

사전지식 2. Spring 의 TaskExecutor

 

Spring Core docs 의 TaskExecution μ—μ„œ μ•Œ 수 μžˆμ§€λ§Œ Spring 은 TaskExecutor λ₯Ό 톡해 비동기 μž‘μ—…μ— λŒ€ν•œ 좔상화λ₯Ό μ œκ³΅ν•œλ‹€.

 

TaskExecutor λŠ” μ•žμ„œ μ„€λͺ…ν•œ java 의 Executor λ₯Ό wrapping ν•œ μΈν„°νŽ˜μ΄μŠ€λ‘œ java.util.concurrent.Executor λ₯Ό μƒμ†ν•œλ‹€

 

Spring μ—μ„œλ„ μ—¬λŸ¬ TaskExecutor 의 κ΅¬ν˜„μ²΄λ₯Ό μ œκ³΅ν•˜λŠ”λ° λŒ€ν‘œμ μœΌλ‘œ μš°λ¦¬κ°€ 봀던 SimpleAsyncTaskExecutor 와 μ•žμœΌλ‘œ 이야기 ν•  ThreadPoolTaskExecutor κ°€ μžˆλ‹€.

 

ThreadPoolTaskExecutor λž€?

 

ThreadPoolTaskExecutor λŠ” java 의 ThreadPoolExecutor.java λ₯Ό wrapping ν•œ Spring 의 Executor 이닀.

 

ThreadPoolTaskExecutor λ₯Ό Async task λ₯Ό μ²˜λ¦¬ν•  Bean 으둜 λ“±λ‘ν•˜κΈ° μœ„ν•΄μ„œλŠ” λ™μž‘ 방식을 μ •ν™•νžˆ 이해해야 ν•œλ‹€

 

ThreadPoolTaskExecutor λŠ” μ–΄λ–»κ²Œ μŠ€λ ˆλ“œλ₯Ό μƒμ„±ν•˜λŠ”κ°€

 

생성에 λŒ€ν•œ λ™μž‘ 방식을 이해해야 ν•˜λŠ” μ΄μœ λŠ” μš°λ¦¬κ°€ λ“±λ‘ν•˜λŠ” bean 섀정이 μ§μ ‘μ μœΌλ‘œ μ„±λŠ₯에 영ν–₯을 미치기 λ•Œλ¬Έμ΄λ‹€.

 

μ΄μœ λŠ” ThreadPoolTaskExecutor κ°€ Thread λ₯Ό μƒμ„±ν•˜λŠ” 방식에 μžˆλŠ”λ°, 이λ₯Ό μ΄ν•΄ν•˜κΈ° μœ„ν•΄μ„œλŠ” λ‹€μŒ 4가지 κ°œλ…λ§Œ κΈ°μ–΅ν•˜μž

 

  1. core pool size
  2. max pool size
  3. queue capacity (waiting queue)
  4. activeThreadCount

 

ThreadPoolTaskExecutor λŠ” μœ„μ˜ 3가지 정보λ₯Ό 가지고 bound 에 따라 μžλ™μœΌλ‘œ ν’€ 크기(μŠ€λ ˆλ“œμ˜ 수)λ₯Ό μ‘°μ ˆν•œλ‹€.

threadPoolTaskExecutor 의 핡심 3가지

 

μ–΄λ–€ μˆœμ„œλ‘œ 생성이 λ˜λŠ”μ§€ μ•Œμ•„λ³΄μž.

 

  1. μ΄ˆκΈ°μ—λŠ” corePoolSize 만큼 μŠ€λ ˆλ“œλ₯Ό μƒμ„±ν•œλ‹€.
  2. μ‹ κ·œλ‘œ μš”μ²­μ΄ λ“€μ–΄ 왔을 λ•Œ, corePoolSize 만큼 μŠ€λ ˆλ“œκ°€ ν• λ‹Ήλ˜λ©΄ queue 에 μš”μ²­μ„ λŒ€κΈ°μ‹œν‚¨λ‹€
  3. queue κ°€ 꽉 μ°¨λ©΄ μ‹ κ·œλ‘œ μŠ€λ ˆλ“œλ₯Ό μƒμ„±ν•œλ‹€.
  4. 3번 과정을 maxPoolSize 만큼 μŠ€λ ˆλ“œλ₯Ό 생성될 λ•Œ κΉŒμ§€ λ°˜λ³΅ν•œλ‹€
  5. λ§Œμ•½ maxPoolSize 만큼 μŠ€λ ˆλ“œλ₯Ό μƒμ„±ν–ˆκ³  corePoolSize κ°€ 꽉 μ°Όλ‹€λ©΄ μš”μ²­μ„ reject ν•œλ‹€

 

μœ„μ˜ μˆœμ„œλ₯Ό 그림으둜 도식화 ν•œλ‹€λ©΄ λ‹€μŒκ³Ό κ°™λ‹€.

 

ThreadPoolTaskExecutor κ°€ μŠ€λ ˆλ“œλ₯Ό μƒμ„±ν•˜λŠ” 둜직

 

μ—¬κΈ°μ„œ μ€‘μš”ν•œ 사싀은 activeThreadCount κ°€ maxPoolSize 만큼 ν• λ‹Ήλ˜κ³  waiting queue κ°€ 꽉 μ°¬λ‹€λ©΄ λͺ¨λ“  μš”μ²­μ„ Reject ν•œλ‹€λŠ” 것이닀.

 

ThreadPoolTaskExecutor κ°€ μš”μ²­μ„ Reject ν•  λ•Œ?

 

μ•žμ„  λ¬Έμž₯을 μ•½μ‹μœΌλ‘œ ν‘œν˜„ν•œλ‹€λ©΄ activeThreadCount == maxPoolSize && queue is full 라면 task μ‹€ν–‰ μš”μ²­μ΄ κ±°λΆ€λœλ‹€.

 

μ΄λ•Œ, κ±°λΆ€λœ μš”μ²­μ€ RejectExecutionHandler.java 에 μ˜ν•΄μ„œ handling 될 수 μžˆλ‹€. μ—¬κΈ°μ„œ RejectionHandlingPolicy λ₯Ό μ–΄λ–»κ²Œ κ°€μ Έκ°€μ•Ό ν•˜λŠ”μ§€λ„ 맀우 μ€‘μš”ν•˜λ―€λ‘œ μžμ„Έν•œ λ‚΄μš©μ€ μ•žμ„  docs λ₯Ό 톡해 ν™•μΈν•˜μž.

 

μ•žμ„  μŠ€λ ˆλ“œ 생성 방식을 μ΄ν•΄ν•˜μ§€ λͺ»ν•œ 채 ThreadPool 만 μ‚¬μš©ν•˜κ² λ‹€κ³  ThreadPoolTaskExecutor bean 을 λ‹€μŒκ³Ό 같이 μ„€μ •ν•˜μ—¬ bean 을 λ“±λ‘ν•œ μ½”λ“œλ₯Ό ν•„μžλŠ” 많이 봐 μ™”λ‹€.

 

@Primary
public TaskExecutor taskExecutor() {
  ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

  executor.setCorePoolSize(1);
  executor.setQueueCapacity(1024); // waiting pool size
  executor.setMaxPoolSize(10);

  return executor;
}

 

이런 섀정을 μΆ”κ°€ν•œ 개발자의 μ˜λ„λŠ” μ•„λ§ˆλ„ μš”μ²­μ΄ reject λ˜λŠ” 것 보닀 queue μ—μ„œ waiting ν•˜λŠ” 편이 λ‚«λ‹€ 라고 μƒκ°ν–ˆμ„ 텐데,

 

μ‹€μ œλ‘œλŠ” single thread 둜 처리될 것이고 multi thread κ°€ λ™μž‘ν•˜λ €λ©΄ 1024 개 이상이 와야 두 번째 μŠ€λ ˆλ“œκ°€ 생성될 것이닀.

 

그럼 μ μ ˆν•œ ThreadPoolTaskExecutor 의 μ„€μ • 값은?

 

μ μ ˆν•œ ThreadPool 의 섀정값은 λ»”ν•œ μ΄μ•ΌκΈ°μ§€λ§Œ ν™˜κ²½κ³Ό μ»΄ν“¨νŒ… 엔진에 따라 λ‹€λ₯΄λ‹€.

 

ν•˜μ§€λ§Œ λͺ‡κ°€μ§€ 팁이 μ‘΄μž¬ν•˜λŠ”λ°, λ°”λ‘œ λ‹€μŒ 2가지λ₯Ό κ³ λ €ν•˜λ©΄ μ‰½κ²Œ μ ‘κ·Όν•  수 μžˆλ‹€.

 

  1. μ—°μ‚°μ˜ μ’…λ₯˜μ— 따라
  2. CPU 코어에 따라

 

적정 μŠ€λ ˆλ“œ κ΅¬ν•˜κΈ° - 1. μ—°μ‚° μ’…λ₯˜ νŒŒμ•…ν•˜κΈ°

 

μ—°μ‚°μ˜ μ’…λ₯˜μ— 따라 μŠ€λ ˆλ“œ ν’€μ˜ 크기가 컀야 ν•˜λŠ”μ§€ μž‘μ•„μ•Ό ν•˜λŠ”μ§€ νŒλ‹¨ ν•  수 μžˆλ‹€.

 

  • CPU Bouond μ—°μ‚°
    • CPU λ₯Ό ν™œμš©ν•œ μ—°μ‚° μž‘μ—…
    • λŒ€κΈ°μ‹œκ°„μ΄ 거의 0에 μˆ˜λ ΄ν•˜λ―€λ‘œ μŠ€λ ˆλ“œ 풀이 μž‘λ‹€λ©΄ Context Switching 이 자주 λ°œμƒν•΄ λΉ„νš¨μœ¨ λ°œμƒ
  • IO Bound μ—°μ‚°
    • HTTP, File κ³Ό 같은 IO μ—°μ‚° μž‘μ—…
    • 보톡 WAITING TIME 이 κΈΈκΈ° λ•Œλ¬Έμ— μŠ€λ ˆλ“œ ν’€μ˜ 크기가 클 λ•Œ νš¨κ³Όμ μž„

 

적정 μŠ€λ ˆλ“œ κ΅¬ν•˜κΈ° - 2. CPU μ½”μ–΄ 수 νŒŒμ•…ν•˜κΈ°

 

효율적인 연산을 μœ„ν•΄μ„œλŠ” CPU 의 Free time 에 적절히 일을 μ‹œν‚€λŠ” 것이 μ€‘μš”ν•˜λ‹€.

 

ν•˜λ‚˜μ˜ μ½”μ–΄μ—μ„œ 보톡 ν•˜λ‚˜μ˜ μŠ€λ ˆλ“œλ₯Ό μ‹€ν–‰ μ‹œν‚€λ―€λ‘œ 적정 μŠ€λ ˆλ“œμ˜ μˆ˜λŠ” μ½”μ–΄μ˜ μˆ˜μ— λΉ„λ‘€ν•œλ‹€.

 

Java Concurrency in Practice μ—μ„œ μ œμ•ˆν•˜λŠ” 계산법

 

μ΄λŸ¬ν•œ 곡식이 μžˆμ§€λ§Œ λ©€ν‹° μŠ€λ ˆλ“œ 풀이 κ³ λ €λ˜μ§€ μ•Šμ•˜κ³ , 이λ₯Ό κ³ λ €ν•œ μ—¬λŸ¬ μˆ˜μ‹μ΄ μ‘΄μž¬ν•˜λ‚˜ 적정 μŠ€λ ˆλ“œμ˜ 개수λ₯Ό κ΅¬ν•˜λŠ” κ°€μž₯ 효과적인 방법은 stress test 등을 톡해 직접 수λ₯Ό μ‘°μ ˆν•˜λŠ” 것을 λͺ…μ‹¬ν•˜μž.

 

μ μ ˆν•œ ThreadPool 의 μ„€μ • 값은 이상적인 μŠ€λ ˆλ“œ ν’€ 크기에 λŒ€ν•˜μ—¬ - code-lab1.tistory.com μ—μ„œ μ„€λͺ…을 잘 ν•΄μ£Όκ³  μžˆμœΌλ‹ˆ μ°Έκ³ ν•˜λŠ” 것도 쒋을 것 κ°™λ‹€.

λŒ“κΈ€