2024.05.29 - [Spring/대용량 트래픽] - Spring Webflux 사용해보기
이런저런 바쁜일이 많아서 조금 늦은 포스팅입니다...
기억나는대로 정리해보면 Spring Webflux에서 (1) functional endpoint 기반, (2) 어노테이션 기반으로 CRUD API를 직접 구현하고, 테스트코드까지 작성해보았다.
이번 포스팅의 주제는 DB나 외부 API에 네트워크 요청을 하는것을 WebClient를 사용해서 해볼것이다.
알다시피(?) Spring MVC 에서는 RestTemplate이라는 동기적 네트워크 요청이있었는데, WebClient는 비동기적으로 이를 처리해 많은 속도향상을 기대한다.
저번 프로젝트 진행하면서 OpenAI API 연동을 RestTemplate으로 했는데, 속도가 완전 느렸다 ㅡㅡ
우선 Webflux를 사용하려면 요청할수 있는 서버가 하나 더 필요하다. 당장 https://start.spring.io/ 여기서 대충 프로젝트를 만들고 오자.
WebfluxTestApplication.java
@SpringBootApplication
@RestController
public class WebfluxTestApplication {
public static void main(String[] args) {
SpringApplication.run(WebfluxTestApplication.class, args);
}
@GetMapping("posts/{id}")
public Map<String, String> getPosts(@PathVariable Long id) {
return Map.of("id", id.toString(), "content", "Post content is %d".formatted(id));
}
}
application.yml
server:
port: 8090
간단하게 스프링 프로젝트를 하나 더 생성해주고, 포트를 8090으로 설정했다.
이제 요청을 받을 로컬 서버는 따로 하나 구성해줬고, 다시 이전글에서 작성하던 Webflux 프로젝트로 넘어간다.
WebClientConfig.java
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient() {
return WebClient.builder().build();
}
}
WebClient를 사용하기위해 Configuration 파일을 만든다.
PostClient.java
@Service
@RequiredArgsConstructor
public class PostClient {
private final WebClient webClient;
private final String url= "http://127.0.0.1:8090";
public Mono<PostResponse> getPost(Long id){
String uriString = UriComponentsBuilder.fromHttpUrl(url)
.path("/ports/%d".formatted(id))
.buildAndExpand()
.toUriString();
return webClient.get()
.uri(uriString)
.retrieve()
.bodyToMono(PostResponse.class);
}
}
새로 만든 8090포트 로컬 서버로 요청을 보내는 PostClient 클래스를 만든다.
PostService.java
@Service
@RequiredArgsConstructor
public class PostService {
private PostClient postClient;
public Mono<PostResponse> getContent(Long id) {
return postClient.getPost(id);
}
}
PostController.java
@RestController
@RequiredArgsConstructor
@RequestMapping("/posts")
public class PostController {
private final PostService postService;
@GetMapping("/{id}")
public Mono<PostResponse> getPostContent(@PathVariable Long id) {
return postService.getContent(id);
}
}
PostResponse.java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PostResponse {
private String id;
private String content;
}
8090포트에서 데이터를 받아오기 위한 DTO를 만든다.
나의 경우 curl로 요청을 보냈을때
요런 에러가 뜬다. 뭔가해서 찾아봤더니, 맥OS에서 Weblient를 실행할때 의존성이 없어서 그렇다고 한다.
build.gradle에 의존성을 추가해주자.
implementation 'io.netty:netty-resolver-dns-native-macos:4.1.94.Final:osx-aarch_64'
WebClient로 다수의 서버 요청
사실 비동기를 활용한 네트워크 요청의 꽃은 속도이므로, 다수의 서버에 동시요청을 보낼때 빛을 본다.
이를 테스트 해보기 위해서 우선 동시에 많은 데이터를 보내는 API를 테스트하자.
PostController.java
@GetMapping("/search")
public Flux<PostResponse> getMultiplePostContent(@RequestParam("ids")List<Long> idList){
return postService.getMultiplePostContent(idList);
}
PostService.java
public Flux<PostResponse> getMultiplePostContent(List<Long> idList) {
return Flux.fromIterable(idList)
.flatMap(this::getContent);
}
매우 간단한 코드이다.
여기서 잠깐!
- flatMap()을 잊지 않았나? 그냥 map()과 다르게 Stream의 데이터 하나를 인자에 맞게끔 여러가지로 매칭 시켜주는 1:n 매핑 메서드이다.
참고로 concatMap()처럼 동기적인 방식이 아니므로 응답이 오는 순서는 랜덤이다.(요청 보낼때 마다)
- MVC의 RestTemplate과의 속도차이를 보기 위해서 MVC서버에 시간을 걸어두자.
Thread.sleep(3000);
위 코드는 3초동안 기다리는 코드인데, 동기적인 방식으로 네트워크 요청을 보낸다면 위의 예시와 같이 5개 요청일때, 15초 이상이 걸리게 된다.
놀랍게도 3초밖에 안걸리는 놀라운 성능을 보인다.
지금도 충분히 빠르지만, 이전글에서 Reactor의 병렬요청을 사용하면 더욱 극한으로 성능을 끌어낼수 있다. 테스트해보자.
그러면 Webflux에서 어떻게 병렬요청을 하도록 만들것이냐? 이것도 이전 포스팅을 보면 나와있다.
- 비동기 작업을 실행하고 관리해주는 Scheduler의 parallel 정책
PostController.java
@GetMapping("/parallel-search")
public Flux<PostResponse> getParallelMultiplePostContent(@RequestParam("ids")List<Long> idList){
return postService.getParallelMultiplePostContent(idList);
}
PostService.java
public Flux<PostResponse> getParallelMultiplePostContent(List<Long> idList) {
return Flux.fromIterable(idList)
.parallel()
.runOn(Schedulers.parallel())
.flatMap(this::getContent)
.sequential();
}
publishOn, subscribeOn 메서드로도 설정해줄수 있지만 runOn함수로도 스케쥴러를 설정할수 있다.
그렇다면 MVC서버에 이와 같은 제한사항을 걸어두고 다수의 요청을 보내면 어떻게 될까?
if(id >= 10L){
throw new Exception("Too long");
}
당연히 실패한다.. 그렇다면 요청으로 1,2,3,4,5,6,67,8,9,10을 보내는 경우는 어떨까?
10 하나때문에 모든 요청이 실패처리된다면 문제가 생긴다. 물론 이를 처리할수 있는 방법도 있다.
- onErrorResume()
public Mono<PostResponse> getContent(Long id) {
return postClient.getPost(id)
.onErrorResume(error -> Mono.just(new PostResponse(id.toString(), "Fallback data %d".formatted(id))));
}
이런식으로 에러가 반환되는 요청도 함께 응답으로 받을수 있게된다. 물론 이것도 상황에 맞춰서 사용해야한다.
이렇게 WebClient에 대해서 알아보았다.
2024.07.03 - [Spring/대용량 트래픽] - R2DBC 알아보기!!
'Spring > 대용량 트래픽' 카테고리의 다른 글
Spring Webflux R2DBC 사용해보기 (0) | 2024.07.06 |
---|---|
R2DBC 알아보기!! (0) | 2024.07.03 |
Spring Webflux 사용해보기 (0) | 2024.05.29 |
Spring Webflux Reactor와 다양한 연산자 (0) | 2024.05.22 |
Spring MVC와 Webflux (0) | 2024.05.22 |