Java/Spring Boot

[Java] Spring Boot Async 비동기 처리 이해하기 -1 : 주요 어노테이션 및 비동기 반환 유형

adjh54 2024. 8. 3. 13:58
반응형
해당 글에서는 Spring Boot 환경에서 비동기 처리를 위한 방법에 대해 알아봅니다.






1) Spring Boot Async


💡 Spring Boot Async

- Spring Boot 환경에서 ‘비동기 프로그래밍’을 지원하는 기능을 의미합니다. 이를 통해서 메서드 호출이 즉시 반환되고 실제 작업은 별도의 스레드에서 비 동기적으로 실행이 됩니다.

- 비동기 메서드를 사용하면 서버의 성능을 향상하고 응답 시간을 줄일 수 있습니다.
- 주로 I/O 작업이 긴 시간 소요 되는 작업에 대해 별도의 스레드를 수행하며, 메인 스레드가 블로킹되지 않도록 합니다.

https://levelup.gitconnected.com/8-things-you-need-to-know-when-you-want-to-use-spring-async-really-well-e5af4af259c5

 

1. 멀티 스레드(Multi-Thread)


💡 멀티 스레드(Multi-Thread)

- 여러 스레드를 동시에 실행하여 작업을 ‘병렬로 처리’하는 기술을 의미합니다. 이는 CPU의 활용도를 높이고, 응답 시간을 줄이며, 서버의 처리 능력을 향상합니다.

- 이러한 비동기 작업은 별도의 스레드에서 실행이 되며, 메인 스레드가 블로킹되지 않도록 합니다. 즉, 메인 스레드가 특정 작업을 기다리느라 멈추지 않고, 다른 작업을 계속해서 수행할 수 있습니다.

 
 
 

[ 더 알아보기 ]

💡 병렬 처리

- 여러 작업을 동시에 수행하는 기술을 의미합니다. 이는 여러 개의 프로세서나 스레드를 사용하여 작업을 병렬로 실행함으로써, 작업 처리 시간을 줄이고 시스템 성능을 향상하는 방법입니다.
- 주로 대규모 연산 데이터 처리, 시뮬레이션 등에 사용되며, 이를 통해 더욱 빠르고 효율적인 작업처리가 가능합니다.


💡 그럼 일반적으로 사용하는 API 서버는 동기적 처리라고 알고 있는데, 이는 멀티스레드를 사용하지 않는 것일까?

- 일반적인 동기 처리에서도 멀티스레드를 사용할 수 있습니다. 동기 처리는 작업이 완료될 때까지 호출한 메서드가 반환되지 않는 것을 의미하며, 멀티스레드는 여러 스레드를 동시에 실행하여 병렬로 작업을 처리하는 기술입니다.
- 따라서 동기 처리에서도 멀티스레드를 활용하여 작업을 병렬로 실행할 수 있습니다. 단, 동기 처리에서는 작업 간의 순서가 보장되며, 비동기 처리와는 다르게 호출한 메서드가 완료될 때까지 기다리는 특성이 있습니다.

 
 

💡 [참고] 스레드에 대해 상세히 궁금하시면 아래의 글이 도움이 됩니다.
 

[Java] 스레드(Thread) 이해하기 -1 : 구조, 상태, 예시

해당 글에서는 스레드에 대한 정의 구조, 상태, 예시와 단일 스레드, 멀티 스레드에 대한 이해를 돕기 위한 목적으로 작성한 글입니다. 1) 스레드(Thread) 1. 스레드(Thread) 💡 스레드(Thread)란? - 하나

adjh54.tistory.com

 
 
 

2. 스레드 풀(Thread Pool)


💡 스레드 풀(Thread Pool)

- 여러 개의 스레드들을 ‘미리 생성’해 두고 작업 큐에서 작업을 가져와서 실행하는 방식으로, 스레드 생성과 소멸에 따른 오버헤드를 줄이고, 시스템 자원을 효율적으로 활용할 수 있게 합니다.

- 이를 사용하면 스레드 생성 비용을 절감하고, 응답 시간을 단축시키며, 시스템의 안정성을 높일 수 있습니다.

 

💡 스레드 풀 생성 과정

- 애플리케이션이 시작될 때, 사전에 미리 지정한 스레드의 개수에 따라서 스레드 풀 내에 스레드가 생성이 됩니다.

💡 스레드 풀 처리 과정

- 생성된 스레드를 기반으로 이를 이용한 처리과정을 이해합니다

1. 애플리케이션(Application)

- 비동기 처리의 하나의 새로운 작업(New Task)을 발생시켰고, 이를 작업 큐로 제출(Submit)을 하게 됩니다.

2. 작업 큐(Task Queue)

- 전달받은 작업(Task)은 큐에서 스레드의 사용이 가능할 때까지 보관을 합니다.
- 또한 큐에서는 FIFO 형태로 먼저 들어온 데이터가 먼저 처리하는 구조를 가지며 순차적으로 데이터가 쌓이고 들어온 순서대로 처리가 수행됩니다.

3. 스레드(Thread)

- 스레드 풀에 있는 스레드 중 하나가 사용이 가능해지면, 작업 큐에서 작업을 가져와서 실행을 하게 됩니다.
- 작업이 완료되면, 스레드는 유휴 상태가 되며, 작업이 발생하면 다시 큐에서 다음 작업을 가져와서 처리를 합니다.

 
 
 
 
 

💡 Java 내에서 스레드 풀 생성 및 처리 과정

1. 스레드 풀 생성 : ExecutorService

- 스레드 풀을 생성할 때, 미리 정해진 수의 스레드가 생성됩니다. Java에서는 ExecutorService 인터페이스를 통해 스레드 풀을 생성할 수 있습니다

2. 작업 제출 : submit()

- 스레드 풀에 작업을 제출하면, 작업은 작업 큐에 추가됩니다. 큐에 추가된 작업은 스레드 풀 내의 스레드에 의해 실행됩니다.

3. 작업 실행

- 스레드 풀 내의 스레드가 작업 큐에서 작업을 가져와 실행합니다. 스레드가 유휴 상태일 때, 다음 작업을 가져와 실행합니다.

4. 작업 완료

- 각 스레드는 작업을 완료하면 다시 유휴 상태로 돌아가며, 다음 작업을 대기합니다.

5. 스레드 풀 종료 : shutdown()

- 더 이상 작업을 제출하지 않을 때, 스레드 풀을 종료해야 합니다. 이를 위해 shutdown() 또는 shutdownNow() 메서드를 호출합니다.
- shutdown()은 이미 제출된 작업을 완료한 후 스레드 풀을 종료하고, shutdownNow()는 실행 중인 작업을 중단하고 즉시 종료합니다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 스레드 풀 생성
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        // 작업 제출
        for (int i = 0; i < 10; i++) {
            executorService.submit(() -> {
                System.out.println("Thread Name: " + Thread.currentThread().getName());
            });
        }

        // 스레드 풀 종료
        executorService.shutdown();
    }
}

 
 

💡 스레드 풀 생성 및 처리 과정 결과

 
 
 

[ 더 알아보기 ]

💡 스레드 풀을 사용하지 않으면 어떻게 될까?

- 스레드 풀을 사용하지 않으면, 각 작업이 요청될 때마다 새로운 스레드가 생성되고 작업이 완료되면 해당 스레드가 소멸됩니다.
- 이는 스레드 생성과 소멸에 따른 오버헤드가 발생하여 시스템 자원의 비효율적인 사용을 초래할 수 있습니다.
- 또한, 스레드 수가 무제한으로 증가할 수 있어 시스템 성능이 저하되거나 심지어 시스템이 다운될 위험이 있습니다.


💡 스레드 풀이랑 커넥션 풀이랑 비슷한 개념인 거 같은데?

- 스레드 풀과 커넥션 풀은 개념적으로 유사합니다. 두 경우 모두 자원을 미리 할당하여 필요할 때 빠르게 사용할 수 있도록 하여, 자원 생성 및 소멸에 따른 오버헤드를 줄이고 시스템 성능을 향상하는 역할을 합니다.
- 스레드 풀은 스레드를 미리 생성해 두고 작업을 처리하며, 커넥션 풀은 데이터베이스 연결을 미리 생성해 두고 필요할 때 재사용합니다.

 
 

💡 [참고] Connection Pool을 이용한 DBCP에 대한 이해가 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
 

[Java/Library] HikariCP 이해하고 적용하기 (with. MyBatis)

해당 글에서는 HikariCP에 대해 이해하고 영속성 프레임워크(Persistence Framework)인 MyBatis와 연동을 하는 적용 방법에 대해서 공유 목적으로 작성한 글입니다.      💡 [참고] 이전에 구성하였던

adjh54.tistory.com

 
 
 

2) Spring Boot Async 주요 어노테이션


1. @EnableAsync 어노테이션


💡 @EnableAsync 어노테이션

- Spring 애플리케이션에서 '비동기 처리를 활성화'하는 어노테이션입니다.

- 주로 @Configuration을 선언한 클래스에서 사용됩니다. 해당 어노테이션을 선언한 클래스 내에서는 메서드 단위로 비동기 처리 활성화 설정을 하거나 @Bean 어노테이션과 함께 Executor의 스레드 풀 설정이나 비동기 작업에 대한 설정을 정의하는 데 사용이 됩니다.

 

1.1. @EnableAsync 어노테이션 속성


💡 @EnableAsync 어노테이션 속성
속성 명 리턴 타입 default  설명
annotation Class<? extends Annotation>   클래스 또는 메소드 수준에서 감지할 'async' 어노테이션 타입을 나타냅니다.
mode AdviceMode AdviceMode.PROXY 비동기 어드바이스가 적용될 방식을 나타냅니다. AdviceMode.PROXY AdviceMode.ASPECTJ을 제공합니다.
order int - AsyncAnnotationBeanPostProcessor가 적용될 순서를 나타냅니다.
proxyTargetClass boolean false 표준 Java 인터페이스 기반 프록시 대신 서브클래스 기반(CGLIB) 프록시가 생성될지 여부를 나타냅니다.

 

 

EnableAsync (Spring Framework 6.1.11 API)

Indicate how async advice should be applied. The default is AdviceMode.PROXY. Please note that proxy mode allows for interception of calls through the proxy only. Local calls within the same class cannot get intercepted that way; an Async annotation on suc

docs.spring.io

 
 

1.2. @EnableAsync 어노테이션 사용예시


💡 @EnableAsync 어노테이션 사용예시

- @Configuration 어노테이션과 함께 @EnableAsync 어노테이션은 사용이 됩니다.
- @Bean 어노테이션 내에 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;
import java.util.concurrent.Executor;

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("Async-");
        executor.initialize();
        return executor;
    }
}

 
 

2. @Async 어노테이션


💡 @Async 어노테이션

- 메서드를 ‘비동기 메서드’로 실행하도록 지정하는 어노테이션입니다. 즉, 메서드 호출은 즉시 반환되고, 실제 작업은 별도의 스레드에서 비동기적으로 수행이 됩니다.

- 해당 어노테이션이 지정된 메서드는 반드시 접근 제한자로 public을 선언해야 합니다. 이는 메서드가 public 이어야 프록시 될 수 있기 때문입니다.

 

[ 더 알아보기]

💡 프록시 될 수 있다는 말은 뭘까?

- 메서드가 호출될 때, 실제 메서드 호출을 가로채서 별도의 로직을 추가하거나 대체할 수 있는 프록시 객체가 사용된다는 의미입니다.
- Spring에서는 주로 AOP(Aspect-Oriented Programming)를 통해 이런 프록시 패턴을 사용하여 비동기 처리, 트랜잭션 관리, 로깅 등의 기능을 구현합니다.

 

2.1. @Async 어노테이션 속성


💡 @Async 어노테이션 속성
속성 명 리턴 타입 default 설명
name String - 실행할 비동기 작업의 스레드 이름을 지정합니다.

 

 

Async (Spring Framework 6.1.11 API)

Annotation that marks a method as a candidate for asynchronous execution. Can also be used at the type level, in which case all the type's methods are considered as asynchronous. Note, however, that @Async is not supported on methods declared within a @Con

docs.spring.io

 

2.2. @Async 어노테이션 사용예시


💡 @Async 어노테이션 사용예시

- 기본적으로 @Async 어노테이션으로 사용하면 SimpleAsyncTaskExecutor로 실행이 되며, 이름으로 지정한 taskExecutor는 AsyncConfig 내에서 재정의한 Executor의 이름으로 매핑이 됩니다.
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class MyAsyncService {

    @Async("taskExecutor")
    public void asyncMethod() {
        // 비동기 작업 수행
    }
}

 
 
 

3) 비동기 반환 유형(Async Return Type) : 반환 유형이 존재하지 않는 경우


1. 비동기 반환 유형이 존재하면 동기 처리 방식과 같은 것이지 않을까?


💡 비동기 반환 유형이 존재하면 동기 처리 방식이지 않을까?

- 결과적으로, 반환 유형이 존재한다고 해서 동기 처리방식과 같은 것은 아닙니다.

- 동기 처리 방식의 경우, ‘호출되는 작업’이 완료가 될 때까지 블로킹이 됩니다. 즉, 호출한 메서드는 작업이 완료될 때까지 기다려야 하며, 그동안 다른 작업이 수행할 수 없습니다.

- 비동기 처리 방식의 경우, ‘호출되는 작업’이 즉시 반환되며 작업은 별도의 스레드에서 비동기적으로 실행됩니다. 즉, 호출한 메서드는 작업이 완료될 때까지 블로킹되지 않으며, 작업이 완료된 후 콜백 또는 Future 객체를 통해 결과를 받을 수 있습니다.

 
 

1.1. 동기(Synchronous) 처리 과정


💡 동기 처리 과정

- 해당 과정에서는 '메인 스레드'는 블로킹이 되며, 5초간의 중지가 되는 동안을 기다린 뒤 수행하는 동기 처리가 수행이 됩니다.

- 해당 synchronousMethod() 메서드를 호출하게 되면, 작업 중인 스레드의 이름이 콘솔로 출력이 되고, Thread.sleep(5000)를 통해서 5초간 스레드가 중지되어 블로킹이 됩니다. 5초가 지난 후에 리턴 값을 반환하고 종료됩니다.
public String synchronousMethod() {
    // 1. 현재 스레드의 이름을 출력합니다.
    System.out.println("Execute method synchronously " + Thread.currentThread().getName());

    try {
        // 2. 스레드를 5초간 일시 중지 시킵니다.
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return "Error: " + e.getMessage();
    }

    // 3. 작업이 완료된 후 결과를 반환합니다.
    return "hello world !!!!";
}

 
 
 

💡 동기 처리 과정 결과

- 아래의 처리 과정의 결과에서는 콘솔로 출력되는 Execute method synchronously 부분이 수행되고, 멈춘 상태가 같은 대기 상태로 이어지다 결과값인 "hello world!!!!"가 출력됨을 확인할 수 있습니다.

 

blog-codes/spring-boot-async/src/test/java/com/adjh/springbootasync/service/SyncAsyncDiffServiceTest.java at main · adjh54ir/bl

Contributor9 티스토리 블로그 내에서 활용한 내용들을 담은 레포지토리입니다. Contribute to adjh54ir/blog-codes development by creating an account on GitHub.

github.com

 
 
 

1.2. 비동기 처리 과정


💡 비동기 처리 과정

- 해당 과정에서는 5초간의 스레드의 일시 중지를 수행하였지만 '메인 스레드'는 블로킹되지 않고 별도의 스레드에서 처리하기에 즉시 값을 반환합니다.

- 해당 asyncMethod() 메서드는 호출하게 되면, 작업 중인 스레드의 이름이 콘솔로 출력이 되고, CompletableFuture.supplyAsync()를 통해 비동기 작업을 수행합니다. 
@Async
public CompletableFuture<String> asyncMethod() {
    // 1. 현재 스레드의 이름을 출력합니다.
    System.out.println("Execute method asynchronously " + Thread.currentThread().getName());

    return CompletableFuture.supplyAsync(() -> {
        try {
            // 2. 스레드를 5초간 일시 중지 시킵니다.
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return "Error: " + e.getMessage();
        }

        // 3. 비동기 결과를 반환합니다.
        return "hello world !!!!";
    });
}

 
 
 

💡 동기 처리 과정 결과

- 해당 처리 과정에서는 Execute method asynchronously를 출력과 동시에 리턴 값을 바로 출력해 주는 결과를 확인할 수 있습니다.

 

blog-codes/spring-boot-async/src/test/java/com/adjh/springbootasync/service/SyncAsyncDiffServiceTest.java at main · adjh54ir/bl

Contributor9 티스토리 블로그 내에서 활용한 내용들을 담은 레포지토리입니다. Contribute to adjh54ir/blog-codes development by creating an account on GitHub.

github.com

 
 
 

💡 [참고] 아래와 같은 호출을 하였을 경우, 비동기 처리로 수행되는 코드에 대해서도 응답 값을 받아서 동기와 같이 처리가 됨을 확인할 수도 있습니다. (* 해당 내용은 뒤에 내용에서 이어집니다)
/**
 * 비동기 호출 수행 테스트
 */
@Test
void asyncMethod() {

    // 비동기 메서드 호출
    CompletableFuture<String> future = syncAsyncDiffService.asyncMethod();

    // 다른 작업을 수행
    System.out.println("Doing other tasks...");

    try {
        // 비동기 작업의 결과를 기다림
        String result = future.get();
        System.out.println("Result: " + result);
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
}

 
 

2. 반환 유형이 존재하지 않는 경우 : void


💡 반환 유형이 존재하지 않는 경우 : void

- @Async 어노테이션을 사용하는 메서드 내에서 비동기적으로 실행되며 호출자는 즉시 반환이 됩니다. 이때 메서드는 처리 결과를 반환하지 않습니다.
- 반환 유형이 존재하지 않는 경우는 단순 작업이나 I/O 작업에서 주로 사용이 됩니다.
- 예를 들어, 로그 기록이나 알림 전송과 같은 작업들이 이에 해당합니다.

 
 

💡 반환 유형이 존재하지 않는 경우 사용예시

- @Async 어노테이션을 통해서 메서드 호출이 비동기적으로 처리되며, 메서드의 리턴타입이 void로 반환되지 않으며 비동기 처리가 되는 메서드를 의미합니다.
- 해당 메서드 내에서는 별도의 리턴값을 받지 않고 처리가 됩니다
/**
 * [Async] 반환 유형이 존재하지 않는 경우 : void
 */
@Async
@Override
public void asyncVoidType() {
    System.out.println("Execute method asynchronously. :: " + Thread.currentThread().getName());
}

 
 
 
 

4) 비동기 반환 유형(Async Return Type) : 반환 유형이 존재하는 경우


 

1. Future, ListenableFuture, CompletableFuture 요약


💡 Future, ListenableFuture, CompletableFuture 요약

- 해당 경우에서는 비동기 처리가 수행된 이후에 이에 따르는 응답값을 전달받습니다.
클래스 설명 주요 기능  단점
Future 비동기 작업의 결과를 나타내는 객체입니다. get 메서드를 통해 결과를 얻을 수 있습니다. 작업 완료 전까지 get 호출 시 블로킹됩니다. get 메서드 호출 시 블로킹될 수 있으며, 콜백 기능이 없습니다.
ListenableFuture Future의 확장으로, 비동기 작업 완료 시 콜백을 등록할 수 있는 기능을 제공합니다. 콜백을 통해 비동기 작업 완료 시 특정 작업을 수행할 수 있으며, 효율적인 비동기 프로그래밍이 가능합니다. 콜백 등록 시 주의가 필요하며, Future 객체의 다른 제한사항도 동일하게 적용됩니다.
CompletableFuture Future의 확장으로, 복잡한 비동기 흐름 처리를 위한 다양한 유틸리티 메서드를 제공합니다. 콜백을 통해 비동기 작업 완료 시 특정 작업을 수행할 수 있으며, 여러 비동기 작업을 조합하거나 체이닝할 수 있습니다. 복잡한 비동기 작업의 경우 코드 가독성이 떨어질 수 있으며, 예외 처리를 적절히 다뤄야 합니다.

 
 

[ 더 알아보기 ]

💡 블로킹이 된다는 말은 무슨 말일까?

- 특정 작업이 완료될 때까지 해당 스레드가 멈추거나 대기 상태가 되는 것을 의미합니다.
- 예를 들어, 어떤 메서드에서 블로킹 호출을 하면 그 메서드가 반환되기 전까지 다른 작업을 수행할 수 없습니다. 따라서 응답 시간이 길어지거나 성능이 저하될 수 있습니다.

 
 

2. Future


💡 Future

- Java의 비동기 프로그래밍에서 사용되는 인터페이스로 비동기 작업의 결과를 나타내는 클래스입니다. 이는 작업이 완료되었는지 여부를 확인하고, 완료된 작업의 결과를 가져오거나, 작업이 완료될 때까지 기다리는 등의 기능을 제공합니다.

- Future 객체의 get 메서드를 호출하면 비동기 작업이 완료될 때까지 블로킹됩니다. 따라서 get 메서드를 호출할 때는 주의가 필요합니다.
- 주로 결과를 필요로 하는 복잡한 작업이나 시간이 오래 걸리는 작업에 사용됩니다.
- 예를 들어, 데이터베이스 쿼리나 파일 처리 작업 등이 이에 해당합니다.
 

Future (Java SE 17 & JDK 17)

Type Parameters: V - The result type returned by this Future's get method All Known Subinterfaces: RunnableFuture , RunnableScheduledFuture , ScheduledFuture All Known Implementing Classes: CompletableFuture, CountedCompleter, ForkJoinTask, FutureTask, Rec

docs.oracle.com

 
 
 

https://danielme.com/2023/01/23/spring-framework-asynchronous-methods-with-async-future-and-taskexecutor-spring_boot-spring_data/

 
 
 

2.1. Future 메서드


💡 Future 메서드
메서드 리턴 타입 설명
cancel(boolean mayInterruptIfRunning) boolean 이 작업의 실행을 취소하려고 시도합니다.
get() V 필요한 경우 계산이 완료될 때까지 대기한 후 결과를 가져옵니다.
get(long timeout, TimeUnit unit) V 필요한 경우 최대 지정된 시간 동안 대기한 후 결과를 가져옵니다.
isCancelled() boolean 이 작업이 정상적으로 완료되기 전에 취소되었는지 여부를 반환합니다.
isDone() boolean 이 작업이 완료되었는지 여부를 반환합니다.

 
 

2.2. Future 사용예시


💡 반환 유형이 존재하지 존재하는 경우 : Future 사용예시

- 해당 예시에서는 @Async 어노테이션을 통해, 비동기 처리임을 지정하고 Feture <String> 객체 값으로 반환을 받는 메서드를 구성하였습니다.

1. 현재 스레드의 이름을 출력합니다.

- 스레드가 생성되고 반환되는 과정을 확인하기 위해 로그를 작성하였습니다.

2. 스레드를 5초간 일시 중지 시킵니다.

- 비동기 메서드의 실행 중에 일정 시간을 기다리게 하기 위해서입니다. 5초 동안 스레드를 일시 중지하고 그 후 비동기 작업의 결과를 반환합니다.

3. 비동기 작업의 결과를 반환합니다.

- Future <String> 리턴타입에 맞는 형태로 임의의 값을 두어서 결과를 반환합니다.
/**
 * [Async] 반환 유형이 존재하는 경우 : Future
 *
 * @return
 */
@Async
@Override
public Future<String> asyncFutureType() {
    System.out.println("Execute method asynchronously - " + Thread.currentThread().getName());
    try {
        Thread.sleep(5000);
        return new AsyncResult<String>("hello world !!!!");
    } catch (InterruptedException e) {
        //
    }
    return null;
}

 

 

blog-codes/spring-boot-async/src/test/java/com/adjh/springbootasync/service/AsyncReturnTypeServiceTest.java at main · adjh54ir/

Contributor9 티스토리 블로그 내에서 활용한 내용들을 담은 레포지토리입니다. Contribute to adjh54ir/blog-codes development by creating an account on GitHub.

github.com

 
 
 

2.3. Future 사용예시 결과


💡 Future 사용예시 결과

- 테스트를 5번 반복한 결과입니다. 최초 Async가 실행이 되고 결과 값이 반환이 되었습니다.

 
 

3. ListenableFuture


💡 ListenableFuture

- Future의 확장으로 비동기 작업의 완료를 기다리는 동안 '콜백'을 등록하여 작업이 완료될 때 특정 동작을 실행할 수 있는 기능을 제공합니다.

- 해당 객체를 통해 비동기 작업이 완료되었을 때, 특정 작업을 수행할 수 있도록 콜백을 설정할 수 있습니다. 이는 비동기 작업의 완료를 기다리지 않고도 후속 작업을 설정할 수 있어서 효율적인 비동기 프로그래밍을 가능하게 합니다.
- Spring Framework 6.x 이상 버전에서 해당 클래스는 Deprecated 되었습니다.
- 주로 결과를 필요로 하는 복잡한 작업이나 시간이 오래 걸리는 작업에 사용됩니다.

 

[ 더 알아보기 ] 

💡 콜백이란 무엇일까?

- 콜백은 특정 작업이 완료된 후 자동으로 호출되는 함수 또는 메서드를 의미합니다. 비동기 작업은 메인 스레드와 별도로 실행되기 때문에, 작업이 완료되었을 때 어떤 작업을 수행해야 하는지 지정할 필요가 있습니다. 이때 콜백 메서드를 사용하여 작업 완료 후의 처리될 내용에 대해서 정의합니다.
- 주로 비동기 프로그래밍에서 사용되며, 비동기 작업이 완료된 후 후속 작업을 수행하는 데 유용합니다.

 

3.1. ListenableFuture 메서드


 💡 ListenableFuture 메서드

- 해당 객체는 interface java.util.concurrent.Future로부터의 데이터를 상속받아서 모두 이용이 가능하면서 ListenableFuture 객체만의 메서드 이용이 가능합니다.
- 해당 메서드는 모두 Spring 6.x 버전에서는 Deprecated 되었고 CompletableFuture를 사용하기를 권장하고 있습니다.

 

메서드 리턴 타입 설명
addCallback(ListenableFutureCallback<? super T> callback) void 비동기 작업이 완료되었을 때 실행될 콜백을 등록합니다.
addCallback(SuccessCallback<? super T> successCallback, FailureCallback failureCallback) void 비동기 작업이 성공적으로 완료되었을 때와 실패했을 때 각각 실행될 콜백을 등록합니다.
completable() default CompletableFuture<T> ListenableFuture를 CompletableFuture로 변환합니다.

 

 

ListenableFuture (Spring Framework 6.1.11 API)

Extend Future with the capability to accept completion callbacks. If the future has completed when the callback is added, the callback is triggered immediately. Inspired by com.google.common.util.concurrent.ListenableFuture.

docs.spring.io

 

3.2. ListenableFuture 사용 예시


💡 ListenableFuture 사용 예시

- asyncListenableFuture() 메서드를 구성하였습니다. 해당 메서드는 @Async를 통해서 비동기 처리를 수행하는 메서드로 선
언하였고, ListenableFuture <String> 객체의 리턴타입을 가진 형태입니다.

1. 현재 스레드의 이름을 출력합니다.
- 스레드가 생성되고 반환되는 과정을 확인하기 위해 로그를 작성하였습니다.

2. 스레드를 5초간 일시 중지 시킵니다.
- 비동기 메서드의 실행 중에 일정 시간을 기다리게 하기 위해서입니다. 5초 동안 스레드를 일시 중지하고 그 후 비동기 작업의 결과를 반환합니다.

3. 비동기 작업의 결과를 반환합니다.
- ListenableFuture <String> 리턴타입에 맞는 형태로 객체를 생성하였습니다.

4. 콜백을 등록합니다.
- addCallback() 메서드를 통해서 성공 시, 실패 시에 대한 콜백함수를 받습니다. 각각 성공에 따르는 로그와 실패에 따르는 로그를 작성하였습니다.
import org.springframework.util.concurrent.ListenableFutureCallback;
import org.springframework.util.concurrent.ListenableFuture;

// @Async 어노테이션으로 비동기 메서드를 정의합니다.
@Async
@Override
public ListenableFuture<String> asyncListenableFuture() {

  // 1. 현재 스레드의 이름을 출력합니다.
  System.out.println("Execute method asynchronously - " + Thread.currentThread().getName());

  try {
      // 2. 스레드를 5초간 일시 중지 시킵니다.
      Thread.sleep(5000);

      // 3. 비동기 작업의 결과를 반환합니다.
      ListenableFuture<String> future = new AsyncResult<>("hello world !!!!");

      // 4. 콜백을 등록합니다.
      future.addCallback(new ListenableFutureCallback<String>() {
          @Override
          public void onSuccess(String result) {
              // 비동기 작업이 성공적으로 완료되었을 때 실행되는 콜백
              System.out.println("Success with result: " + result);
          }

          @Override
          public void onFailure(Throwable t) {
              // 비동기 작업이 실패했을 때 실행되는 콜백
              System.out.println("Failure: " + t.getMessage());
          }
      });

      return future;
  } catch (InterruptedException e) {
      System.out.println("error :: " + e.getMessage());
  }

  return null;
}

 

 

blog-codes/spring-boot-async/src/test/java/com/adjh/springbootasync/service/AsyncReturnTypeServiceTest.java at main · adjh54ir/

Contributor9 티스토리 블로그 내에서 활용한 내용들을 담은 레포지토리입니다. Contribute to adjh54ir/blog-codes development by creating an account on GitHub.

github.com

 
 
 

3.3. ListenableFuture 사용 예시 결과


💡 ListenableFuture 사용 예시 결과

- 최초 메서드가 실행이 되었고, 5초가 지난 뒤 결과값을 반환해 줍니다.

 

💡 [참고] 'org.springframework.util.concurrent.ListenableFuture' is deprecated since version 6.0

- Spring 버전 6.0부터 이 클래스는 더 이상 사용되지 않도록(deprecated) 표시되었습니다.
- 이는 해당 클래스가 더 이상 권장되지 않으며, 향후 버전에서는 제거될 수 있음을 의미합니다. 대신 CompletableFuture와 같은 대체 클래스를 사용하는 것이 권장됩니다. CompletableFuture는 Java 8에서 도입되었으며, 비동기 프로그래밍을 더 쉽고 유연하게 할 수 있는 다양한 기능을 제공합니다.

 
 
 

4. CompletableFuture


💡 반환 유형이 존재하는 경우 : CompletableFuture

- Java 8에서 도입된 Future의 구현체이자 확장 기능을 제공하는 클래스입니다. 이전 Future 보다 비동기 작업을 더 쉽게 작성하고 관리할 수 있도록 도와줍니다.

- 주요 기능으로는 작업을 체인 방식으로 연결할 수 있는 thenApply, thenAccept, thenCompose 등의 메서드와, 작업 완료 후 특정 동작을 실행할 수 있는 whenComplete, handle 등의 메서드가 있습니다.
- 또한, CompletableFuture는 명시적으로 완료 상태로 설정할 수 있어, 외부에서 작업의 성공 또는 실패를 제어할 수 있습니다.
- 주로 비동기 작업의 결과를 조합하거나, 체이닝 하여 연속적인 비동기 작업을 수행할 때 사용됩니다. 예를 들어, 여러 비동기 작업을 순차적으로 실행하거나 결과를 조합하는 데 유용합니다.

https://danielme.com/2023/01/23/spring-framework-asynchronous-methods-with-async-future-and-taskexecutor-spring_boot-spring_data/

 
 

4.1. CompletableFuture 메서드


메서드 리턴 타입 Description
acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action) CompletableFuture<Void> 이 단계 또는 주어진 다른 단계가 정상적으로 완료되면, 제공된 action을 인수로하여 실행되는 새 CompletionStage를 반환합니다.
acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action) CompletableFuture<Void> 이 단계 또는 주어진 다른 단계가 정상적으로 완료되면, 기본 비동기 실행 시설을 사용하여 제공된 action을 인수로하여 실행되는 새 CompletionStage를 반환합니다.
acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action, Executor executor) CompletableFuture<Void> 이 단계 또는 주어진 다른 단계가 정상적으로 완료되면, 제공된 실행기를 사용하여 제공된 action을 인수로하여 실행되는 새 CompletionStage를 반환합니다.
allOf(CompletableFuture<?>... cfs) static CompletableFuture<Void> 주어진 모든 CompletableFuture가 완료되면 완료되는 새 CompletableFuture를 반환합니다.
anyOf(CompletableFuture<?>... cfs) static CompletableFuture<Object> 주어진 CompletableFuture 중 하나가 완료되면 동일한 결과로 완료되는 새 CompletableFuture를 반환합니다.
applyToEither(CompletionStage<? extends T> other, Function<? super T, U> fn) <U> CompletableFuture<U> 이 단계 또는 주어진 다른 단계가 정상적으로 완료되면, 제공된 기능을 인수로하여 실행되는 새 CompletionStage를 반환합니다.
applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn) <U> CompletableFuture<U> 이 단계 또는 주어진 다른 단계가 정상적으로 완료되면, 기본 비동기 실행 시설을 사용하여 제공된 기능을 인수로하여 실행되는 새 CompletionStage를 반환합니다.
applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn, Executor executor) <U> CompletableFuture<U> 이 단계 또는 주어진 다른 단계가 정상적으로 완료되면, 제공된 실행기를 사용하여 제공된 기능을 인수로하여 실행되는 새 CompletionStage를 반환합니다.
cancel(boolean mayInterruptIfRunning) boolean 아직 완료되지 않은 경우, 이 CompletableFuture를 CancellationException로 완료합니다.
complete(T value) boolean 아직 완료되지 않은 경우, 주어진 값을 get() 및 관련 메서드가 반환하는 값으로 설정합니다.
completeAsync(Supplier<? extends T> supplier) CompletableFuture<T> 기본 실행자를 사용하여 비동기 작업에서 호출된 주어진 공급자 함수의 결과로 이 CompletableFuture를 완료합니다.
completeAsync(Supplier<? extends T> supplier, Executor executor) CompletableFuture<T> 제공된 실행자를 사용하여 비동기 작업에서 호출된 주어진 공급자 함수의 결과로 이 CompletableFuture를 완료합니다.
completedFuture(U value) static <U> CompletableFuture<U> 주어진 값으로 이미 완료된 새 CompletableFuture를 반환합니다.
completedStage(U value) static <U> CompletionStage<U> 주어진 값으로 이미 완료된 새 CompletionStage를 반환하며, CompletionStage 인터페이스의 메서드만 지원합니다.
completeExceptionally(Throwable ex) boolean 아직 완료되지 않은 경우, 주어진 예외로 get() 및 관련 메서드의 호출이 예외를 throw 하도록 합니다.
completeOnTimeout(T value, long timeout, TimeUnit unit) CompletableFuture<T> 주어진 시간 초과 전에 완료되지 않은 경우, 주어진 값으로 이 CompletableFuture를 완료합니다.
copy() CompletableFuture<T> 이 CompletableFuture가 정상적으로 완료되면 동일한 값으로 정상적으로 완료되는 새 CompletableFuture를 반환합니다.
defaultExecutor() Executor 실행자를 지정하지 않은 비동기 메서드에 사용되는 기본 실행자를 반환합니다.
delayedExecutor(long delay, TimeUnit unit) static Executor 주어진 지연 후(또는 지연이 음수일 경우 지연 없이) 기본 실행자에 작업을 제출하는 새 실행자를 반환합니다.
delayedExecutor(long delay, TimeUnit unit, Executor executor) static Executor 주어진 지연 후(또는 지연이 음수일 경우 지연 없이) 기본 실행자에 작업을 제출하는 새 실행자를 반환합니다.
exceptionally(Function<Throwable, ? extends T> fn) CompletableFuture<T> 이 단계가 예외적으로 완료되면, 예외를 인수로 하여 제공된 기능을 실행하는 새 CompletionStage를 반환합니다.
failedFuture(Throwable ex) static <U> CompletableFuture<U> 주어진 예외로 이미 예외적으로 완료된 새 CompletableFuture를 반환합니다.
failedStage(Throwable ex) static <U> CompletionStage<U> 주어진 예외로 이미 예외적으로 완료된 새 CompletionStage를 반환하며, CompletionStage 인터페이스의 메서드만 지원합니다.
get() T 필요한 경우 이 미래가 완료될 때까지 기다렸다가 결과를 반환합니다.
get(long timeout, TimeUnit unit) T 필요한 경우 최대 주어진 시간 동안 이 미래가 완료될 때까지 기다렸다가 결과를 반환합니다.
getNow(T valueIfAbsent) T 완료된 경우 결과 값을 반환하고(또는 발생한 예외를 throw) 그렇지 않은 경우 주어진 valueIfAbsent를 반환합니다.
getNumberOfDependents() int 이 CompletableFuture의 완료를 기다리는 CompletableFuture의 추정 개수를 반환합니다.
handle(BiFunction<? super T, Throwable, ? extends U> fn) <U> CompletableFuture<U> 이 단계가 정상적으로 또는 예외적으로 완료되면, 결과와 예외를 인수로 하여 제공된 기능을 실행하는 새 CompletionStage를 반환합니다.

 

 

CompletableFuture (Java SE 17 & JDK 17)

Type Parameters: T - The result type returned by this future's join and get methods All Implemented Interfaces: CompletionStage , Future A Future that may be explicitly completed (setting its value and status), and may be used as a CompletionStage, support

docs.oracle.com

 
 

4.2. CompletableFuture 사용예시 -1


💡 CompletableFuture 사용예시 -1

- 해당 예시에서는 콜백으로 성공/실패에 대한 후 처리가 존재하지 않는 형태의 사용예시입니다.

1. 현재 스레드의 이름을 출력합니다.
- 스레드가 생성되고 반환되는 과정을 확인하기 위해 로그를 작성하였습니다.

2. 비동기 작업을 수행할 CompletableFuture 객체를 생성합니다.
- supplyAsync()를 통해서 결과값을 반환합니다.

3. 스레드를 5초간 일시 중지 시킵니다.
- 비동기 메서드의 실행 중에 일정 시간을 기다리게 하기 위해서입니다. 5초 동안 스레드를 일시 중지하고 그 후 비동기 작업의 결과를 반환합니다.

4. 비동기 결과가 성공하였을 때 반환되는 값입니다.
/**
 * [Async] 반환 유형이 존재하는 경우 : CompletableFuture
 *
 * @return CompletableFuture<String>
 */
@Async
@Override
public CompletableFuture<String> asyncCompletableFuture() {
    // 1. 현재 스레드의 이름을 출력합니다.
    System.out.println("Execute method asynchronously " + Thread.currentThread().getName());

    // 2. 비동기 작업을 수행할 CompletableFuture 객체를 생성합니다.
    return CompletableFuture.supplyAsync(() -> {
        try {
            // 3. 스레드를 5초간 일시 중지 시킵니다.
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return "Error: " + e.getMessage();
        }
        // 4. 비동기 결과가 성공하였을때 반환되는 값입니다.
        return "hello world !!!!";
    });
}

 

 

blog-codes/spring-boot-async/src/test/java/com/adjh/springbootasync/service/AsyncReturnTypeServiceTest.java at main · adjh54ir/

Contributor9 티스토리 블로그 내에서 활용한 내용들을 담은 레포지토리입니다. Contribute to adjh54ir/blog-codes development by creating an account on GitHub.

github.com

 
 
 
 

4.3. CompletableFuture 사용 결과 -1


💡 CompletableFuture 사용 결과 -1

 
 

4.4. CompletableFuture 사용 예시 -2


💡 CompletableFuture 사용 예시 -2

- 해당 예시에서는 콜백으로 성공/실패에 대한 후처리가 포함된 예시입니다.

1. CompletableFuture 객체를 생성하고, 비동기 작업을 수행하여 결과값을 반환받습니다.

2. 비동기 작업이 완료되었을 때 수행할 동작을 정의합니다.

3. 비동기 작업의 성공과 실패에 따라 다르게 처리합니다.

4. 메인 스레드가 종료되지 않도록 2초간 대기합니다.

5. 결과적으로 CompletableFuture 객체를 반환합니다.
@Override
public CompletableFuture<String> asyncCompletableFuture2() {
    // 1. CompletableFuture 객체를 생성하고, 비동기 작업을 수행하여 결과값을 반환받습니다.
    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello");

    // 2. 비동기 작업이 완료되었을 때 수행할 동작을 정의합니다.
    future.whenComplete((result, exception) -> {
        if (exception == null) {
            // 3. 비동기 작업이 성공적으로 완료되었을 때 실행되는 블록
            System.out.println("Completed successfully with result: " + result);
        } else {
            // 4. 비동기 작업이 실패했을 때 실행되는 블록
            System.out.println("Completed with error: " + exception.getMessage());
        }
    });

    // 5. 메인 스레드가 종료되지 않도록 2초간 대기합니다.
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        System.out.println("error :: " + e.getMessage());
    }

    // 6. CompletableFuture 객체를 반환합니다.
    return future;
}

 
https://github.com/adjh54ir/blog-codes/blob/main/spring-boot-async/src/test/java/com/adjh/springbootasync/service/AsyncReturnTypeServiceTest.java

 

blog-codes/spring-boot-async/src/test/java/com/adjh/springbootasync/service/AsyncReturnTypeServiceTest.java at main · adjh54ir/

Contributor9 티스토리 블로그 내에서 활용한 내용들을 담은 레포지토리입니다. Contribute to adjh54ir/blog-codes development by creating an account on GitHub.

github.com

 
 

4.5. CompletableFuture 사용 결과 -2


 
 

 [ 더 알아보기 ]

💡 메인 스레드가 종료하지 않고 대기하는 이유는 무엇인가?

- 비동기 작업이 완료될 때까지 메인 스레드가 살아있도록 하기 위해서입니다. 만약 메인 스레드가 먼저 종료되면, 비동기 작업이 완료되기 전에 프로그램이 종료될 수 있습니다.
- 따라서 비동기 작업이 완료되는 것을 보장하기 위해 메인 스레드를 일시적으로 대기 상태로 만들어 주는 것입니다.


💡 그러면 메인 스레드는 항상 대기하도록 sleep()을 해주어야 하는가?

- 그렇지 않습니다. 일반적으로는 비동기 작업이 완료될 때까지 메인 스레드가 대기할 필요는 없습니다.
- 비동기 작업이 완료되면, 해당 작업의 콜백 또는 후속 작업이 자동으로 실행됩니다.
- 다만, 예제 코드에서처럼 프로그램이 종료되지 않도록 하기 위해 일시적으로 대기 상태로 만들어 주는 경우가 있을 수 있습니다. 하지만 실제 애플리케이션에서는 비동기 작업을 처리하는 다른 방법을 사용할 수 있습니다.


💡Thread.currentThread(). getName()로 조회되는 것은 메인 스레드인가?

- Thread.currentThread().getName()로 조회되는 것은 메인 스레드가 아닙니다.
- @Async 어노테이션을 사용하여 비동기 메서드를 정의하면, 해당 메서드는 별도의 스레드에서 실행됩니다.
- 따라서, Thread.currentThread().getName()를 호출하면 현재 실행 중인 비동기 스레드의 이름이 반환됩니다.

 
 
 

💡 [참고] 해당 글에서 테스트한 코드는 아래의 Repository 내에서 확인이 가능합니다
 

blog-codes/spring-boot-async at main · adjh54ir/blog-codes

Contributor9 티스토리 블로그 내에서 활용한 내용들을 담은 레포지토리입니다. Contribute to adjh54ir/blog-codes development by creating an account on GitHub.

github.com

 
 

💡 다음 글에서는 Spring Boot 환경에서 Async를 이용할 때, 이를 실행하는 Executor의 종류 및 스레드 풀, 예외처리에 대해 알아봅니다. 

 
 
 
오늘도 감사합니다. 😀

 

반응형