@Async에 대한 이해

2022. 11. 21. 18:46BackEnd(Java)/Spring Boot

✅ 아래 내용들에 대해서 알아보자

- 비동기/동기, 블로킹/논블로킹 이해
- @Async 개념 및 사용법
- ThreadPoolExecutor 빈 등록 및 옵션 값 설정

 

@Aysnc를 이해하기 전에 먼저 비동기/동기, 블로킹/논블로킹 개념부터 이해를 해야한다..

혹시 헷갈리거나 잘 모르시는 분들은 아래 블로그 참고해주시면 됩니다.

 

https://inpa.tistory.com/entry/👩‍💻-동기비동기-블로킹논블로킹-개념-정리

 

👩‍💻 동기 & 비동기 / 블로킹 & 논블로킹 💯 완벽 이해하기

동기 & 비동기 vs 블로킹 & 논블록킹 개념 이 개념들을 처음 접하거나 컴퓨터 공학에 대해 잘 모르는 사람은 이 개념들이 서로 뭔가 연관이 있는 것으로 오해하기 쉽다. 아무래도 동기와 블록킹,

inpa.tistory.com

 

 

@Async

 스프링 부트에서 제공하는 손쉽게 사용할 수 있는 비동기 처리 방식

 

@Async 사용법

  1. @EnableAsync로 @Async를 사용하겟다고 선언한다
  2. 비동기로 수행되었으면 하는 메서드위에 @Async를 적용한다.
  3. 별도의 @Async 설정이 없으면 새로운 비동기 작업을 스레드 풀에서 처리하는 게 아니라 새로운 스레드를 매번 생성해서 작업을 수행시키는것이 디폴트 설정 ⇒ 이렇게 되면 많은 양의 스레드를 사용하게 되므로 비효율적이게 됨. 따라서 쓰레드 풀을 관리해줘야 한다
  4. 쓰레드 풀 관리를 위한 ThreadPoolTaskExecutor 빈 등록(옵션들 추가)

 

ThreadPoolTaskExecutor 빈 등록 예시 코드

 

package com.restudy.jpa_re;

import java.util.concurrent.Executor;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean(name = "threadPoolTaskExecutor_test")
    public Executor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setCorePoolSize(1); //thread-pool 항상 살아 있는 thread 최소 개수
        threadPoolTaskExecutor.setMaxPoolSize(10); //thread-pool에 사용할 수 있는 thread 최대 개수 
        threadPoolTaskExecutor.setQueueCapacity(2); //thread-pool에 사용할 최대 queue 크기
        threadPoolTaskExecutor.setThreadNamePrefix("test threadPool"); //thread prefix 
        
        return threadPoolTaskExecutor; 
    }
}

 

비동기 로직을 가진 클래스

 

package com.restudy.jpa_re;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class AsyncTestService {

    @Async("threadPoolTaskExecutor_test")
    public void asyncHello(int i) {
        log.info("async i ={}", i);
    }
}

 

테스트 코드

@DisplayName("asyncTest")
@Test
public void asyncTest() throws Exception {
    //given
    System.out.println("logic start");
    for(int i=0;i<100;i++){ 
        asyncTestService.asyncHello(i); 
    } 
    //when
    System.out.println("logic end");
    
    //then 
}

 

ThreadPoolTaskExecutor 옵션 값 정리

  1. corePoolSize : thread-pool에 항상 살아있는 thread 최소 개수
  2. maxPoolSize : thread-pool에서 사용할 수 있는 thread 최대 개수
  3. queueCapacity : thread-pool에서 사용할 최대 queue의 크기
  4. ThreadNamePrerfix : thread 이름의 prefix
  5. keepAliveSeconds : maxPoolSize가 모두 사용되다가 idel(유휴상태)로 돌아갔을 떄 종료하기까지 대기하는 걸리는 시간
  6. WaitForTasksToCompleteOnShutDown : 시스템 종료 시 queue에 남아있는 작업을 모두 완료한 후 종료 하도록 처리
  7. TaskDecorator : thread-pool로 작업을 시작하기 직전에 진행하는 작업을 추가할 수 있도록 하는 interface이다. 해당 interface를 구현하여 traceid를 복사하는 등의 작업을 할 수 있다(데코레이터 패턴)
  8. RejectedExecutionHandler : RejectedExecutionHandler는 ThreadPoolExecutor에서 task를 더 이상 받을 수 없을 때 호출된다. 이런 경우는 queue 허용치를 초과하거나 Executor가 종료되어 Thread 또는 큐를 사용할 수 없는 경우에 발생하게 된다.

 

@Async 튜닝 없이 사용 시 기본 값

  • corePoolSize : 1
  • maxPoolSize : Integer.MAX_VALUE
  • keepAliveSeconds : 60(second)
  • QueueCapacity : Integer.MAX_VALUE
  • AllowCoreThreadTimeOut : false
  • WaitForTasksToCompleteOnShutdown : false
  • RejectedExecutionHandler : AbortPolicy

⇒기본값을 보았을 때, corePoolSize가 1이기 때문에 1개의 스레드로 들어오는 모든 일을 처리다.

이때문에 요청이 많이 들어오게 되면 처리의 지연이 발생할 수 있습니다.

그리고 더더욱 많은 요청이 들어와서 처리할 수 없는 수준이 된다면 RejectedExecutionHandler 값이 AbortPolicy이기 때문에 Exception을 발생시키며 종료되게 됩니다.

 

for문 테스트 리뷰

 - @Async 튜닝 없이 사용하더라도 싱글 스레드로 도는것이 아니라, 멀티 스레드로 동작함

 - 스레드 개수를 2개로 설정 후 동작 시 1부터 100까지 순차적으로 실행 됨

 

@Async 사용시 주의사항

  1. private 메서드에는 적용이 안된다. public만 가능
  2. self-invocation(자가 호출)해서는 안된다 → 같은 클래스 내부의 메서드를 호출해서는 안됨(다른 클래스의 메서드 호출)

 

 

번외)

Serial(직렬) vs Concurrent(동시)

이 두 개념은 큐의 특성에 대한 개념입니다.

  • Serial

-> 시리얼큐는 큐가 받아들인 작업들을 하나의 스레드로만 보내 처리하는 큐입니다. 시리얼큐는 순서가 중요한 작업 을 처리할 때 사용

  • Concurrent

-> 컨커런트큐는 받아들인 작업을 여러 개의 스레드로 나눠서 보내 처리하는 큐입니다. 이 때 몇 개의 쓰레드로 분산할지는 위에서 언급했듯시스템(OS)이 알아서 결정합니다. 컨커런트큐는 각자 중요도나 작업 성격이 독립적이지만 유사한 여러 개의 작업을 처리할 때 사용합니다. (예를 들면 테이블 뷰 각 셀에 들어가는 이미지나 텍스트 데이터를 API에서 가지고 올 때가 있습니다.)

 

스레드의 작업 처리 방식과 큐의 작업 분산 방식 조합

Sync(동기) Async(비동기)

Serial(직렬) 작업을 다른 스레드에서 하도록 분산처리한 후 그 작업이 끝나길 기다렸다가 다음 작업을 큐에 넣습니다.이 때 다른 스레드에 작업을 분산하는 큐의 특성은 Serial입니다. 즉, 작업을 시키는 스레드(A)에서 분산처리 시킨 작업을 해당 스레드(A)가 아닌 다른 한 개의 스레드(B)에서 처리하도록 합니다(분배합니다.) 작업을 다른 스레드에서 하도록 분산처리한 후 그 작업이 끝나길 기다리지 않고 다음 작업을 큐에 넣습니다.이 때 작업을 다른 스레드에 분산하는 큐의 특성은 Serial입니다. 즉, 작업을 시키는 스레드(A)에서 분산처리 시킨 작업을 해당 스레드(A)가 아닌 다른 한 개의 스레드(B)에서 처리하도록 합니다(분배합니다.)
Concurrent(동시) 작업을 다른 스레드에서 하도록 분산처리한 후 그 작업이 끝나길 기다렸다가 다음 작업을 큐에 넣습니다.이 때 작업을 다른 스레드에 작업을 분산하는 큐의 특성은 Concurrent 입니다. 즉, 작업을 시키는 스레드(A)에서 분산처리 시킨 작업을 해당 스레드(A)가 아닌 다른 여러 개의 쓰레드(B,C,D…)에서 처리하도록 합니다(분배합니다.) 작업을 다른 쓰레드에서 하도록 분산처리한 후 그 작업이 끝나길 기다리지 않고 다음 작업을 큐에 넣습니다.이 때 다른 스레드에 작업을 분산하는 큐의 특성은 Concurrent입니다. 즉, 작업을 시키는 스레드(A)에서 분산처리 시킨 작업을 해당 스레드(A)가 아닌 다른 여러 개의 쓰레드(B,C,D…)에서 처리하도록 합니다.(분배합니다.)

 

정리

동기/비동기 개념은 작업을 보내는 시점에서 작업이 끝날때 까지 기다릴지 말지를 결정하는 것

직렬/동시 개념은 큐로 보낸 작업들이 여러 개의 스레드로 갈지 아니면 하나의 스레드로 처리할지 결정하는 것

 


참고자료

반응형