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 작업이 긴 시간 소요 되는 작업에 대해 별도의 스레드를 수행하며, 메인 스레드가 블로킹되지 않도록 합니다.
1. 멀티 스레드(Multi-Thread)
💡 멀티 스레드(Multi-Thread)
- 여러 스레드를 동시에 실행하여 작업을 ‘병렬로 처리’하는 기술을 의미합니다. 이는 CPU의 활용도를 높이고, 응답 시간을 줄이며, 서버의 처리 능력을 향상합니다.
- 이러한 비동기 작업은 별도의 스레드에서 실행이 되며, 메인 스레드가 블로킹되지 않도록 합니다. 즉, 메인 스레드가 특정 작업을 기다리느라 멈추지 않고, 다른 작업을 계속해서 수행할 수 있습니다.
[ 더 알아보기 ]
💡 병렬 처리
- 여러 작업을 동시에 수행하는 기술을 의미합니다. 이는 여러 개의 프로세서나 스레드를 사용하여 작업을 병렬로 실행함으로써, 작업 처리 시간을 줄이고 시스템 성능을 향상하는 방법입니다.
- 주로 대규모 연산 데이터 처리, 시뮬레이션 등에 사용되며, 이를 통해 더욱 빠르고 효율적인 작업처리가 가능합니다.
💡 그럼 일반적으로 사용하는 API 서버는 동기적 처리라고 알고 있는데, 이는 멀티스레드를 사용하지 않는 것일까?
- 일반적인 동기 처리에서도 멀티스레드를 사용할 수 있습니다. 동기 처리는 작업이 완료될 때까지 호출한 메서드가 반환되지 않는 것을 의미하며, 멀티스레드는 여러 스레드를 동시에 실행하여 병렬로 작업을 처리하는 기술입니다.
- 따라서 동기 처리에서도 멀티스레드를 활용하여 작업을 병렬로 실행할 수 있습니다. 단, 동기 처리에서는 작업 간의 순서가 보장되며, 비동기 처리와는 다르게 호출한 메서드가 완료될 때까지 기다리는 특성이 있습니다.
💡 [참고] 스레드에 대해 상세히 궁금하시면 아래의 글이 도움이 됩니다.
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에 대한 이해가 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
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) 프록시가 생성될지 여부를 나타냅니다. |
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 | - | 실행할 비동기 작업의 스레드 이름을 지정합니다. |
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!!!!"가 출력됨을 확인할 수 있습니다.
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를 출력과 동시에 리턴 값을 바로 출력해 주는 결과를 확인할 수 있습니다.
💡 [참고] 아래와 같은 호출을 하였을 경우, 비동기 처리로 수행되는 코드에 대해서도 응답 값을 받아서 동기와 같이 처리가 됨을 확인할 수도 있습니다. (* 해당 내용은 뒤에 내용에서 이어집니다)
/**
* 비동기 호출 수행 테스트
*/
@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 메서드를 호출할 때는 주의가 필요합니다.
- 주로 결과를 필요로 하는 복잡한 작업이나 시간이 오래 걸리는 작업에 사용됩니다.
- 예를 들어, 데이터베이스 쿼리나 파일 처리 작업 등이 이에 해당합니다.
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;
}
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로 변환합니다. |
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;
}
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는 명시적으로 완료 상태로 설정할 수 있어, 외부에서 작업의 성공 또는 실패를 제어할 수 있습니다.
- 주로 비동기 작업의 결과를 조합하거나, 체이닝 하여 연속적인 비동기 작업을 수행할 때 사용됩니다. 예를 들어, 여러 비동기 작업을 순차적으로 실행하거나 결과를 조합하는 데 유용합니다.
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를 반환합니다. |
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 !!!!";
});
}
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;
}
4.5. CompletableFuture 사용 결과 -2
[ 더 알아보기 ]
💡 메인 스레드가 종료하지 않고 대기하는 이유는 무엇인가?
- 비동기 작업이 완료될 때까지 메인 스레드가 살아있도록 하기 위해서입니다. 만약 메인 스레드가 먼저 종료되면, 비동기 작업이 완료되기 전에 프로그램이 종료될 수 있습니다.
- 따라서 비동기 작업이 완료되는 것을 보장하기 위해 메인 스레드를 일시적으로 대기 상태로 만들어 주는 것입니다.
💡 그러면 메인 스레드는 항상 대기하도록 sleep()을 해주어야 하는가?
- 그렇지 않습니다. 일반적으로는 비동기 작업이 완료될 때까지 메인 스레드가 대기할 필요는 없습니다.
- 비동기 작업이 완료되면, 해당 작업의 콜백 또는 후속 작업이 자동으로 실행됩니다.
- 다만, 예제 코드에서처럼 프로그램이 종료되지 않도록 하기 위해 일시적으로 대기 상태로 만들어 주는 경우가 있을 수 있습니다. 하지만 실제 애플리케이션에서는 비동기 작업을 처리하는 다른 방법을 사용할 수 있습니다.
💡Thread.currentThread(). getName()로 조회되는 것은 메인 스레드인가?
- Thread.currentThread().getName()로 조회되는 것은 메인 스레드가 아닙니다.
- @Async 어노테이션을 사용하여 비동기 메서드를 정의하면, 해당 메서드는 별도의 스레드에서 실행됩니다.
- 따라서, Thread.currentThread().getName()를 호출하면 현재 실행 중인 비동기 스레드의 이름이 반환됩니다.
💡 [참고] 해당 글에서 테스트한 코드는 아래의 Repository 내에서 확인이 가능합니다
💡 다음 글에서는 Spring Boot 환경에서 Async를 이용할 때, 이를 실행하는 Executor의 종류 및 스레드 풀, 예외처리에 대해 알아봅니다.
오늘도 감사합니다. 😀
반응형