Springboot Java - 비동기와 멀티스레팅
비동는 무엇이고, 비동기 처리를 위해 효과적으로 멀티스레딩을 사용하는 방법은?
비동기와 논블로킹
비동기와 Non-blocking의 차이가 뭔가요?라는 질문에 대답할 수 있는가?
멀티스레딩에 대해 얘기하면서 비동기와 동기, Blocking과 Non-blocking에대해 얘기한 적이 있다. 두 개의 개념에 대해 약간은 애매하게 이해하고 있어서 그랬는지, 비동기와 Non-blocking이 같은 개념이라고 생각하고 있었고, 그래서 다시 정리하며 확실하게 어떤 개념인지 알아보고자 한다.
사실 한글과 영어의 차이라고 생각한것도 있었는데, 생각해보니 동기는 synchronous, 비동기는 asynchronous 였다는게 기억나, 아차 싶었다.
동기(synchronous)와 비동기(asynchronous)
- 동기(synchronous)
- 작업이 순차적으로(=직렬적) 실행되는 것을 의미한다.
- 이전 작업이 완료되어야 다음 작업을 수행할 수 있고, 그래서 결과를 기다리는 동안 다른 작업을 할 수 없다.
- 비동기(asynchronous)
- 작업이 동시에(=병렬적) 실행될 수 있다.
- 이전 작업이 끝날 때까지 기다리지 않고, 다음 작업을 진행할 수 있다.
- 완료 시점이 불확실해, 보통은 완료가 되면 실행되는 콜백 등과 같이 사용된다.
- 이전 작업이 끝날 때까지 기다리지 않고, 다음 작업을 진행할 수 있다.
Blocking, Non-Blocking
그렇다면 Blocking과 Non-Blocking은 어떤 개념일까?
- 블로킹(Blocking)
- 특정 작업이 끝날 때까지 대기하며, 다른 작업을 수행하지 못한다.
- 논블로킹(Non-Blocking)
- 작업을 수행하는 동안 대기하지 않고, 바로 다음 작업을 수행할 수 있다.
여기까지 읽으면, 결국 같은 얘기인거아냐? 라고 생각할 수 있다.
관점의 차이
사실 개념들은 서로 반대되거나 하는 개념이 아니라, 작업에 대해 관점의 차이다.
이 작업의 처리 순서가 순차적이냐, 순차적이지 않냐 와 같은 순서에 대한 관점이라면 동기와 비동기 이고, 요청을 보내고 응답이 올 때까지 기다려야하냐, 아니면 기다리지 않아도 되냐와 같이 대기에 대한 관점이라면 블로킹과 논 블로킹인 것이다. 그래서 비동기라고 무조건 논 블로킹이 아니라 블로킹이 발생할 수 있다.
조금만 검색해 보아도 이 주제에 대해 자세히 적어 놓은 글이 많은것을 보면, 동기+블로킹 / 비동기+논블로킹 이렇게 개념을 섞어서 알고 있는 경우가 많은것같다. (나처럼..)
어떻게 보면, 비동기나 논블로킹이나 추구하는 결과는 동시에 다른 작업을 수행한다는 것이고, 목표는 효율적인 실행에 있다는 것을 보면 비슷하게 느껴질 수 있다. 또, 실무에서는 비동기만 사용하거나 논블로킹만 사용하는 경우도 적고, 대부분이 비동기+논블로킹을 같이 이용해 효율을 높이고 있기 때문에 더 그럴것이라고 생각한다. 하지만 그래도 정확한 개념을 알아야, 비동기인데 왜 효율이 안오르는지, 이건 논블로킹이니까 되는거 아닌가? 이런 오류에서 벗어날것이라고 생각한다.
예제
그럼 개념으로는 알았으니 조금만 더 확실히 알기위해 각 상황에 대한 예제까지 보며 공부해보자.
동기 + 블로킹
요청을 하면, 결과가 올 때까지 멈춘다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public String blockingMethod() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "완료!";
}
public void doTask() {
System.out.println("작업 시작");
String result = blockingMethod();
System.out.println(result);
}
- 3 블로킹3초 동안 블로킹 되는 로직이 있다.
- 12
doTask()
는 동기로 동작하고,blockingMethod()
에서는 블로킹되기 때문에 3초동안 아무것도 하지 못하고 기다리게 된다. - 13 3초 이후에 출력된다.
결과 동기로 실행하며, 블로킹 되는 로직이 있어 blockingMethod()
의 응답이 올 때 까지 아무것도 하지 못하고 기다리며 블로킹 된다.
비동기 + 블로킹
비동기로 실행은 하지만, 결과를 기다려야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void doTask() {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
Thread.sleep(3000);
return "완료!";
});
System.out.println("작업 시작");
try {
String result = future.get();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
executor.shutdown();
}
- 5 블로킹3초 동안 블로킹 되는 로직이 있다.
- 12
doTask()
는future
를 이용해 비동기로 동작하지만,future.get()
의 결과를 받기위해 블로킹되기 때문에 3초동안 아무것도 하지 못하고 기다리게 된다. - 13 3초 이후에 출력된다.
결과 비동기로 실행하지만, future.get()
의 응답을 기다리기 때문에 결국 블로킹 발생한다. 비동기라도 블로킹이 발생하면 의미가 없다.
비동기 + 논 블로킹
1
2
3
4
5
6
7
8
9
10
11
12
13
public void doTask() {
CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "완료!";
})
.thenAccept(result -> System.out.println(result));
System.out.println("작업 요청 완료!");
}
- 1~10
doTask()
는CompletableFuture.supplyAsync
를 이용해 비동기로 작업을 실행한다. - 10
doTask()
는CompletableFuture.supplyAsync
의 작업 결과를 기다리지 않는 논 블로킹으로 동작하며,CompletableFuture.supplyAsync
는 작업 이후 다음 작업이 있다. - 12
doTask()
는 비동기로 작업 실행 이후, (대기없이) 바로 자신의 작업을 이어서 한다.
결과 doTask()
는 내부의 작업을 비동기로 실행하며, 블로킹 없이 바로 다음 코드를 실행한다. 비동기에 논 블로킹 방식이라 효율적이다.
동기 + 논 블로킹
특정 작업이 완료될 때까지 대기하지 않지만, 반복적으로 상태를 체크하는 방식
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class SyncNonBlockingExample {
private static boolean isDone = false;
private static String result = "";
public static void main(String[] args) {
System.out.println("작업 시작");
new Thread(() -> {
try {
Thread.sleep(3000);
result = "작업 완료!";
isDone = true;
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
while (!isDone) {
System.out.println("다른 작업 수행 중...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(result);
}
}
- 8~16
main()
는Thread
를 이용해 논 블로킹(=블로킹 되지 않도록) 방식으로 작업을 실행한다. - 18~25
Thread
를 이용한 작업 외에도, 다른 작업을 하긴 하지만(=블로킹 되지 않음) 계속해서Thread
의 작업을 체크한다. - 21 CPU 사용량을 줄이기 위해 잠시 대기
- 12
main()
는Thread
의 작업이 끝나야 자신의 작업도 끝나는 작업의 순서(=동기)를 가진다.
결과 main()
의 작업은 동기식
(코드가 순차적으로 실행)으로 동작하지만, Thread
의 작업이 끝날 때까지 멈춰 기다리지는 않는 논블로킹
방식이다.
또 다른 시각으로 본 블로킹/논블로킹
지금까지 글로도 잘 이해가 되면 좋겠지만 혹시나 좀 더 명확하게 하기위해 정보를 찾던 중 제어권이라는 키워드를 이용해 정리한 글을 찾아 소개해보려 한다.
여기서 말하는 제어권이란 전체 흐름을 제어할 권리로 이해하면 좋다.
블로킹
블로킹에서는 제어권의 소유가 변경된다.
main()
이 제어권을 가지고 코드를 실행하던 중 job()
을 호출했을 때, 제어권이 job()
으로 넘어가고, main()
은 job()
이 제어권을 가져갔기 때문에 main()
의 실행이 멈춰지고 job()
을 기다리게 된다. 그리고 job()
이 실행을 마치고 응답과 함께 main()
에게 제어권을 넘겨주면 main()
이 멈췄던 부분부터 다시 실행되는 것이다.
- 제어권
main()
main()
이 제어권을 가지고 실행중이다.
- 제어권
- 제어권
main()
->job()
main()
이job()
을 호출하며 제어권을 넘겨준다.
- 제어권
- 제어권
job()
main()
은 제어권이 없어 실행이 멈춰지고job()
을 기다리게 된다.(=블로킹)
- 제어권
- 제어권
job()
->main()
job()
이 실행을 마치고 응답과 함께main()
에게 제어권을 넘겨준다.
- 제어권
- 제어권
main()
main()
이 멈췄던 부분부터 다시 실행된다.
- 제어권
제어권의 소유가 main()
-> job()
-> main()
의 순서로 변경된다.
논블로킹
논블로킹에서는 제어권의 소유가 변경되지 않는다.
main()
이 제어권을 가지고 코드를 실행하던 중 job()
을 호출했을 때, 제어권이 job()
으로 넘어가지 않고, main()
은 제어권이 있기 때문에 job()
을 기다리지 않고, main()
의 실행을 한다. 그리고 job()
의 실행이 끝나면 그대로 job()
은 종료된다.
- 제어권
main()
main()
이 제어권을 가지고 실행중이다.
- 제어권
- 제어권
main()
main()
이job()
을 호출하지만, 제어권은 넘기지 않는다.
- 제어권
- 제어권
main()
job()
은 작업을 시작하고, 완료하면 그대로job()
이 종료된다.
- 제어권
- 제어권
main()
main()
은job()
과 별개로 이후 로직을 실행한다.(=논블로킹)
- 제어권
동기/비동기와 블로킹/논블로킹에 대해 다양한 시각 자료로 설명한 좋은 글이 있어 소개한다. 이곳에서 제어권의 키워드로도 잘 정리해 놓았으니 같이 읽으면 좋을 것 같다. 출처: 완벽히 이해하는 동기/비동기 & 블로킹/논블로킹
멀티스레팅
지금까지 비동기와 논블로킹이 어떤 차이가 있는지 알아봤고, 그래서 비동기+논블로킹의 방식이 가장 효율적이라는 것까지 알았다. 그럼 우리는 어떻게 비동기+논블로킹의 방식으로 로직을 작성할 수 있을까? 멀티스레딩을 이용한다면 가능하다.
스레드 풀
멀티스레딩은 하나의 프로세스 내에서 여러 개의 작업(=스레드)이 동시에 실행되는 방식이라는 것을 잘 알 안다. java에서 간단한 방식으로 Thread
를 상속 받거나, Runnable
을 구현하는 방식으로 멀티스레딩 로직을 만들 수 있다. 하지만 이렇게 계속 새로운 스레드를 생성하는 것은 비효율 적일 수 있어, 스레드 풀이라는 개념이 생겼다.
- 스레드 풀
- 미리 생성된 스레드들의 집합이다.
- 스레드를 생성하는 것은 많은 비용(CPU, 메모리 사용량 등)이 들기 때문에, 미리 스레드를 생성해놓고 그 스레드를 사용하는 것이다.
- 이 외에도 너무 많은 스레드가 생성되어 실행된다면, 컨텍스트 스위칭 비용이 많이 들게되어 오히려 성능이 저하된다.
- 스레드를 생성하는 것은 많은 비용(CPU, 메모리 사용량 등)이 들기 때문에, 미리 스레드를 생성해놓고 그 스레드를 사용하는 것이다.
java에서 지원하는 스레드 풀은 다양하게 있지만, 이는 여기서 다루지 않을 것이다. 여기서는 비동기와 논블로킹을 중심으로 작성하려고 한다.
스레드 풀을 이용한 비동기 처리
java와 spring에서 스레드 풀을 이용한 비동기 처리 방식에 대해 알아보자.
ExecutorService를 이용한 비동기 처리
- ExecutorService
- Java에서 멀티스레딩을 쉽게 관리할 수 있도록 제공하는 인터페이스로, 스레드 풀(Thread Pool)을 활용하여 작업을 처리하는 방식을 지원한다.
ExecutorService
는 스레드 풀을 관리하는 인터페이스로, 직접 스레드 풀을 생성하고 스레드에 작업을 할당해서 비동기로 동작할 수 있도록 구성하면된다. 비동기는 실행할 수 있지만 future.get()
을 사용하면 블로킹이 발생되기 때문에, 논 블로킹 처리에 주의해야한다.
CompletableFuture를 이용한 비동기 처리
- CompletableFuture
- Java 8에서 도입된 비동기 프로그래밍을 위한 클래스이다.
- 내부적으로는 스레드 풀을 이용해 비동기로 동작할 수 있도록 한다.
- 설정을 변경하면, 단일 스레드로 동작할 수 있다.
- 내부적으로는 스레드 풀을 이용해 비동기로 동작할 수 있도록 한다.
CompletableFuture
를 이용하면 CompletableFuture
가 내부적으로 스레드 풀을 활용한 비동기를 지원해주기 때문에, ExecutorService
와 달리 직접 스레드 풀을 관리하지 않아도 된다. thenApply()
, thenAccept()
, allOf()
, anyOf()
등의 API를 활용해 논 블로킹 방식으로 동작하도록 해야한다.
@Async와 ThreadPoolTaskExecutor를 활용한 비동기 처리
- @Async
- Spring에서 메서드를 비동기적으로 실행하도록 만들어 주는 어노테이션이다.
- 스레드 풀을 사용하여 작업을 백그라운드에서 실행한다.
- ThreadPoolTaskExecutor
- Spring에서 제공하는 스레드 풀 관리 도구다.
- 기본적으로 ExecutorService 기반으로 동작한다.
@Async
는 Spring에서 메서드를 비동기로 실행하도록 만드는 어노테이션이고, @Async
으로 지정한 메서드는 자동으로 ThreadPoolTaskExecutor
에서 관리하는 스레드 풀을 활용해 비동기로 실행된다. 그렇기 때문에 CompletableFuture
와 마찬가지로 스레드 풀을 직접 관리하지 않아도 된다. 하지만 작성한 로직에 따라 블로킹이 발생할 수 있으므로, 논블로킹 방식이 되도록 주의해야한다.
예제
ExecutorService를 이용한 비동기 처리
스레드 풀을 이용한 비동기 실행 예시 ExecutorService
를 이용해 스레드 풀을 관리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ThreadPoolAsyncExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
Future<String> future = executor.submit(() -> {
Thread.sleep(3000);
return "비동기 작업 완료!";
});
System.out.println("메인 스레드는 블로킹 없이 계속 실행!");
try {
String result = future.get();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
executor.shutdown();
}
}
- 7 스레드 풀에 스레드를 3개 생성
- 10 3초를 대기하는 작업
- 14 비동기로 동작하기 때문에, “메인 스레드는 블로킹 없이 계속 실행!” 문구는 바로 출력된다.
- 17 하지만, 이와 같이 작성하면
future.get()
의 결과를 기다리는 블로킹이 발생될 수 있으니 주의해야한다.
CompletableFuture를 이용한 비동기 처리
CompletableFuture
를 이용하면, CompletableFuture
내부적으로 스레드 풀을 활용해 비동기를 지원한다. CompletableFuture
의 다양한 API를 이용해 논블로킹 방식으로 비동기 실행이 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CompletableFutureExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "비동기 작업 완료!";
}, executor).thenAccept(result -> System.out.println(result));
System.out.println("메인 스레드는 논블로킹으로 계속 실행!");
executor.shutdown();
}
}
- 7 스레드 풀에 스레드를 3개 생성
- 11 3초를 대기하는 작업
- 16
CompletableFuture
의 다양한 API를 이용해, 작업 이후 다른 작업을 실행한다. - 18 비동기로 동작하기 때문에, “메인 스레드는 블로킹 없이 계속 실행!” 문구는 바로 출력된다.
- 20
main
은 작업이 종료되면,main
스레드가 사라지고,CompletableFuture
는 다른 스레드로 실행되기 때문에 해당 작업이 종료되면CompletableFuture
스레드가 사라진다.
@Async와 ThreadPoolTaskExecutor를 활용한 비동기 처리
@Async
로 비동기로 지정한 메서드만 ThreadPoolTaskExecutor
를 활용하여 스레드 풀 기반의 비동기 처리가 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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 = "asyncExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(10);
executor.initialize();
return executor;
}
}
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class AsyncService {
@Async("asyncExecutor")
public void asyncMethod() {
System.out.println(Thread.currentThread().getName() + " - 비동기 작업 시작");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " - 비동기 작업 완료");
}
}
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/async")
public class AsyncController {
private final AsyncService asyncService;
public AsyncController(AsyncService asyncService) {
this.asyncService = asyncService;
}
@GetMapping("/run")
public String runAsyncTask() {
asyncService.asyncMethod();
return "비동기 작업 실행됨!";
}
}
- 8 설정: 비동기 기능 활성화 설정을 해야한다.
- 14~16 스레드 풀에 대한 설정을 한다.
- 11~19 설정한 스레드 풀의 이름을 지정한다.
- 27
@Async
를 이용해 비동기로 동작해야하는 메서드임을 명시하고, 어떤 스레드 풀을 이용할 것인지도 지정한다. - 53~57
/async/run
으로 GET 요청이 들어오면, 비동기 메서드를 실행한다. 컨트롤러에 들어온 요청은 비동기 메서드를 실행한 이후 바로 “비동기 작업 실행됨!”을 리턴하며 종료된다.(=논블로킹)
Spring에서 블로킹 논블로킹
블로킹과 논블로킹이 Spring에서도 적용되어 있는데, 어떻게 적용되어있는지 알아보자.
Spring MVC와 Spring webFlux
그동안 사용했던 Spring MVC와 Spring 5 부터 지원되는 Spring webFlux의 차이도 블로킹과 논블로킹에 대한 차이라고 한다.
- Spring MVC
- 하나의 요청마다 스레드를 할당해 처리한다. = 요청이 해당 스레드를 점유하고 있으며, 요청을 처리하는 동안 스레드는 다른 일을 할 수 없다. = 요청을 처리하는 동안 스레드는 블로킹된다.
- 스레드 풀을 이용하지만 스레드 풀의 사이즈가 유동적이라, 요청에 따라 스레드의 수가 유동적으로 증가했다가 줄어든다. = 스레드 풀의 수보다 많은 요청이 많으면 추가로 생성하기도 하거나 요청을 대기 시킨다.
- Spring webFlux
- 이벤트 루프(Event Loop) 기반의 논블로킹 방식을 사용한다.
- 요청을 처리하는 동안 스레드가 블로킹되지 않고, 다른 요청도 동시에 처리할 수 있다. = 이벤트에 대한 처리를 진행하다가 I/O 작업(DB 조회, 외부 API 호출 등)으로 블로킹될 필요가 있으면, 비동기적으로 콜백을 등록하고 다른 요청을 처리한다. = 비동기 + 논블로킹으로 동작하므로, 동일한 스레드가 여러 요청을 동시에 처리할 수 있다.
예제
예제를 보면 더욱 쉽게 이해할 수 있어, 먼저 예제를 보겠다.
Spring MVC
1
2
3
4
5
6
7
8
9
10
@RestController
@RequestMapping("/mvc")
public class MvcController {
@GetMapping("/blocking")
public String blockingEndpoint() throws InterruptedException {
Thread.sleep(3000);
return "완료!";
}
}
위와 같은 로직이라고 할 때, Spring MVC에서는 어떻게 동작할까?
- 클라이언트가
/mvc/blocking
엔드포인트에 요청한다. - 서버가 스레드 풀의 스레드를 이용해 요청을 처리한다.
- 요청 스레드는 요청을 처리하다가
Thread.sleep(3000)
나 I/O(DB 조회, 외부 API 호출 등)의 작업이 있어도, 아무것도 하지 않고 그대로 기다린다. = 블로킹된다. - 만약 새로운 요청이 들어왔들때 스레드 풀에서 스레드가 없다면, 스레드를 요청마다 새로 생성해야한다.
Spring webFlux
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@RequestMapping("/webflux")
public class WebFluxController {
@GetMapping("/non-blocking")
public Mono<String> nonBlockingEndpoint() {
return Mono.fromSupplier(() -> {
try {
Thread.sleep(3000); // 3초 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
return "완료!";
});
}
}
위와 같은 로직이라고 할 때, Spring WebFlux에서는 어떻게 동작할까?
- 클라이언트가
/webflux/non-blocking
엔드포인트에 요청한다. - 서버는 스레드 풀의 스레드를 이용해 요청을 처리한다.
- 요청 스레드는 요청을 처리하다가
Thread.sleep(3000)
나 I/O(DB 조회, 외부 API 호출 등)의 작업이 있으면, 비동기적으로 콜백을 등록하고 다른 요청을 처리한다. = 논블로킹된다. - 작업이 끝나면 응답을 비동기적으로 전환한다.
그럼 무조건 Spring webFlux를 사용하는 것이 좋을까?
지금까지 비동기+논블로킹이 효율적으로 동작하는 것을 확인으니 Spring 에서도 무조건 WebFlux를 사용하는 것이 좋을까? 그렇지는 않다. Spring에서 Webflux를 사용할 때 고려해야하는 부분들이 있는데, 간단히 살펴보자.
Spring에서 Webflux를 사용할 때 고려해야하는 부분
- 모든 작업을 논블로킹으로 유지해야 효과적인데, 블로킹 방식으로 동작하는 요소들(JDBC, JPA(Hibernate), RestTemplate, synchronized 등)과 함께 사용하면 논블로킹의 이점이 사라진다.
- Mono, Flux와 같은 리액티브 스트림을 이해하고 사용해야해서 디버깅이 어렵고, 코드가 복잡해질 수 있다.
Spring WebFlux를 사용하면 좋은 경우
- 동시 요청이 많은 애플리케이션
- 배달 앱, 실시간 채팅, 대규모 IoT 데이터 수집 시스템과 같이 대규모 사용자 트래픽을 처리해야 하는 서비스에서 사용될 수 있다.
- 외부 API를 호출하는 서비스
- Spring MVC에서는 외부 API 호출 시 응답을 받을 때까지 스레드가 블로킹되는데, Spring WebFlux는 비동기 방식으로 API를 호출해 동시에 다른 작업을 수행할 수 있다.
- 스트리밍 데이터 처리
- 대량의 스트리밍 데이터를 실시간으로 처리할 수 있어, 실시간 주식 시세, 비디오 스트리밍 서비스에서 사용될 수 있다.
- 마이크로서비스 아키텍처
- 마이크로서비스 간 비동기 통신시 많이 사용되는 메시지 큐와 연동하여 이벤트 기반 시스템 구축시에 사용될 수 있다.
이외에도 Spring MVC와 Spring WebFlux는 같이 사용할 수도 있다.