gi_dor

Spring에서 비동기 @Async 본문

Back_End/SpringBoot

Spring에서 비동기 @Async

기돌 2024. 8. 29. 17:38

비동기 처리 작업은 멀티스레드를 사용해 작업을 분리하고 , 그 작업이 끝날 때 까지 대기하지 않고 다른작업을 처리할 수 있다

스프링부트에서는 @Async 어노테이션과 CompletableFuture 클래스를 사용해 비동기 처리를 구현할 수 있다


- @Async 는 해당 메서드를 비동기로 실행하도록 설정하고 
- CompletableFuture 비동기 처리를 위한 인터페이스 , 비동기 작업이 완료된 이후 결과값을 처리할수 있는
  메서드를 제공한다

회원가입 페이지에서 회원가입이 완료되면 이메일은 전송하교 , 완료되었다는 페이지를 보여주게 구성했다면

@Async만 사용할 경우
이메일이 성공적으로 전송되었는지 실패했는지 , 언제 끝나는지 정보가 없다
그저 비동기 작업을 하고 결과를 신경쓰지 않는다
결과적으로 이메일이 실제로 전송되었는지 알 수가 없다

CompletableFuture 를 함께 사용할 경우 
이메일 전송 작업이 실행되고 'CompletableFuture' 객체가 반환된다
성공적으로 전송되면 true , 실패시 false를  'CompletableFuture' 로 감싸서 반환된다
@Async
public void sendWelcomeEmail(String email) {
    // 이메일 전송 로직
}


@Async
public CompletableFuture<Boolean> sendWelcomeEmail(String email) {
    try {
        // 이메일 전송 로직
        return CompletableFuture.completedFuture(true);
    } catch (Exception e) {
        return CompletableFuture.completedFuture(false);
    }
}

 

1 ) Process

일반적으로 CPU에의해 자원을(메모리) 소비해 실행중인 프로그램을 뜻한다
자바 JVM은 주로 하나의 프로세스로 실행되며 , 동시에 여러작업을 수행하기 위해 멀티스레드를 지원하고있다

2 ) Thread

프로세스 (실행중인 프로그램) 안에서 실질적으로 작업을 실행하는 단위를 말하며 , JVM에 의해 관리된다
스레드를 추가 생성하게 되면 멀티 스레드 환경이 된다

3 ) ThreadPoolTaskExecutor

Spring프레임워크에서 제공하는 스레드 풀을 관리 하고 비동기 작업을 처리하는데 사용되는 클래스
Java에 ExecutorService 인터페이스를 구현 하고 Spring의 비동기 처리를 지원한다

4 ) ThreadPool

미리 일정한 갯수의 스레드를 생성해 관리하는 방법이다

생성한 스레드들은 작업을 할당받기 위해 대기 상태에있다 , 작업이 발생한다면 대기중인 스레드중 하나를 선택해
작업을 수행한다 , 작업이 완료되면 해당 스레드는 다시 대기 상태로 되며 , 새로운 작업을 할당받을 준비를 한다

스레드 풀을 사용해 스레드 생성 , 삭제에 따른 오버헤드를 줄이며 , 특정시점에 동시에 처리하는 작업에 갯수를 제한한다
이를 통해 시스템의 자원을 효율적으로 관리하고 성능을 향상 시킬수 있다

- 필요성
서버는 동시에 여러사용자가 접속한다
자바에서는 스레드를 운영체제의 자원으로 사용한다 , 스레드를 계속해서 만들면 , 운영체제의 자원이 빨리 소진된다
서버는 동시접속자가 많아지면 스레드가 무한대로 생성되어 서버다 다운될 위험이 있다

애플리케이션에서 사용되고 있는 스레드의 개수를 관리하기 위해 쓰레드풀 을 사용한다

 

ThreadPool의 필요성 ?

  • 자원의 효율성
    스레드 풀은 미리 정해진 갯수의 스레드를 생성해 관리하므로 , 오버헤드를 줄일 수 있다
    시스템의 자원을 효율적으로 관리하며 , 불필요한 자원소모를 방지한다

  • 응답성 처리량 향상
    작업을 대기상태로 유지하여 작업처리 속도를 향상시킬 수 있다
    작업이 발생하면 대기중인 스레드중 하나를 선택해 작업을 할당하므로 작업 처리를 병렬로 진행

  • 스레드 관리
    스레드 풀을 사용해 스레드 생명주기를 관리할 수 있다
    스레드의 생성 , 재사용 , 종료등을 관리하므로 안전한 운영을 도와준다 

ThreadPoolTaskExcutor 을 사용해 비동기 작업을 관리

Spring에서 제공하는 TaskExecutor 인터페이스를 구현하기 위해 ThreadPool 설정

@Configuration 어노테이션을 사용해 클래스로 작성하고 ThreadPoolTaskExcutor 사용

Spring Boot 에서 기본적으로 SimpleAsyncTaskExecutor 가 사용된다 이럴경우 스레드를 재사용하지 않고
매번 스레드를 새로 생성하기에 스레드 풀의 관리를 위해  ThreadPoolTaskExcutor 을 사용한다

@EnableAsync    // 스프링에 비동기처리 활성화 - @Async 어노테이션 비동기 작동하게 만듬
@Configuration  // 빈등록
public class AsyncConfig {

    @Bean
    public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);    // 스레드 풀의 기본크기
        executor.setMaxPoolSize(30);    // 스레드 풀의 최대 크기
        executor.setQueueCapacity(50);  // 작업 대기열의 용량 설정 , 대기열이 가득차면 추가 작업 거부
        executor.initialize();          // 작업 완료후 스레드 풀 초기화
        return executor;
    }
}

 

 

corePoolSize

  • 스레드 풀의 기본 크기를 나타내는 값 
  • 최소한의 스레드 갯수
  • 위에 작성한 코드를 기준으로 최소한 5개의 스레드가 항상 유지된다
  • default 1


maximumPoolSize

  • 스레드 풀의 최대 크기를 나타내는 값
  • 스레드 풀이 생성할수 있는 최대 스레드 갯수
  • 스레드 풀이 corePoolSize 까지 스레드를 생성한 이후 , 작업이 도착할 때 마다 새로운 스레드를 생성해 작업을 처리
  • maximumPoolSize를 설정하면 스레드 풀의 크기가 동적으로 조절된다 


QueueCapacity

  • 스레드 풀 내부에서 작업을 대기하는 용량을 나타내는 값
  • 스레드 풀이 처리하지 못한 작업들을 저장하는 대기열의 크기를 결정
  • 스레드 풀이 처리할 수 있는 작업의 양을 제한하기 위해서
  • 스레드 풀이 비정상적으로 과도한 작업을 처리하거나 메모리 부족으로인한 문제를 방지할 수 있다
  • 작게 설정되면 대기열이 가득차 추가작업이 거부 , 너무크면 메모리를 많이 사용해 성능저하가 발생


keepAliveTime 

  • 작업하지 않고 놀고있는 스레드 ( = 비활성 스레드) 가 유지되는 최대시간
  • corePoolSize 를 초과하고 maxPoolSize 보다 작은 수로 스레드가 추가적으로 만들어진 경우에만 적용
  • 스레드 풀의 현재 스레드 갯수가 corePoolSize 보다 크며 , 작업이 도착하지 않아 비활성화 상태인 스레드가 발생하면 
    keepAliveTime 이후에는 해당 스레드를 종료시킨다
  • default 60sec

 


 

스레드 풀의 크기를 적절하게 설정해야 하는 이유

스레드를 생성하는 것은 비용이 드는 작업이다
스레드가 생성될 때 요청이 처리되는 지연시간과 추가적인 처리과정에 드는 시간등 자원이 소모된다
이런 스레드 생성비용을 줄이기 위해 스레드 풀이 필요하다

스레드풀에서 미리 생성한 스레드를 재사용함으로써 자원 낭비를 막을 수 있기 때문이다

 

 

스레드 풀에 무조건 많은 스레드를 생성할까 ?

스레드를 많이 생성한다고 그 스레드를 모두 다 사용할 수는 있는 것은 아니다
쓸모없는 스레드를 많이 생성한다면 생성하는 그 자체에서 드는 자원과 비용이 낭비된다
그렇다고 스레드를 부족하게 만들면 CPU 사용률이 낮아진다

CPU 바운드 작업은 코어 스레드 수를 늘려야 하고, I/O 바운드 작업은 대기열 용량을 늘린다

 

1. CPU 바운드 작업 :  

CPU 바운드 작업은 주로 CPU 연산이 많은 작업을 뜻한다
해당 작업이 CPU의 연산 처리 속도에 영향을 받아 실행 시간이 크게 늘어나는 경우를 말한다


예를 들어, 숫자 계산, 암호화, 정렬, 압축, 이미지 처리 등의 작업이 CPU 바운드 작업에 해당
CPU 바운드 작업을 처리할 때는 코어 스레드 수를 늘려야 하며
CPU 작업을 병렬로 처리하기 위해 코어 수에 맞추어 스레드를 늘리는 것이 효율적

2. I/O 바운드 작업 : 

I/O 바운드 작업은 주로 입출력 작업이 많은 작업을 뜻한다.
해당 작업이 주로 입출력 장치(하드 디스크, 네트워크 등)와의 데이터 송수신에 시간을 소비하는 경우를 말한다


예를 들어, 파일 입출력, 네트워크 통신, 데이터베이스 조회 등의 작업이 I/O 바운드 작업에 해당.
I/O 바운드 작업을 처리할 때는 대기열 용량을 늘려야하며
입출력 작업이 완료될 때까지 대기할 수 있는 작업 대기열을 충분히 확보하는 것이 효율적

 

이상적인 스레드 풀의 적정크기

 CPU 코어의 개수

하나의 CPU 코어는 한번에 하나의 스레드를 실행할 수 있다
만약 8코어 라면 8개의 스레드를 한번에 실핼할 수 있다 , 이렇게 코어의 갯수에 따라 적정 스레드 크기를 정하게된다

서버가 많은 수의 코어를 가졌더라고 애플리케이션의 일부 코어만 사용할 수 있는 경우도 있다
이럴 때 , 애플리케이션에서 사용가능한 코어의 수가 스레드 풀의 크기를 결정하게 된다.

 

 

이상적인 스레드 풀 크기 공식

📌 서비스 시간은 , 대기시간을 제외한 실제로 작업이 동작 중인 시간을 뜻한다
대기 시간이 길다면 스레드풀의 크기를 키워야하고 , 대기시간이 짧다면 스레드풀의 크기를 줄인다

 

728x90