
사이드 프로젝트를 진행하며 모델 서버와의 연동이 필요했는데, Spring Boot의 WebClient 라이브러리를 사용하여 해결했었다. 제대로 공부해야 할 것 같아서 기록한다.
WebClient란?
Spring에서 외부 API 서버와의 통신하는 방법으로는 RestTemplate과 WebClient 두 가지 방법이 있다. 그 중 WebClient에 대해 파헤쳐보자
WebClient는 RestTemplate를 대체하는 HTTP 클라이언트이다.
기존의 동기 API를 제공할 뿐만 아니라, 논블로킹 및 비동기 접근 방식을 지원해서 효율적인 통신이 가능하다!
WebClient는 요청을 나타내고 전송하게 해주는 빌더 방식의 인터페이스를 사용하며, 외부 API로 요청을 할 때 리액티브 타입의 전송과 수신을 한다. (Mono, Flux)
WebClient의 특징을 정리하자면 ?
- 싱글 스레드 방식을 사용
- Non-Blocking 방식을 사용
- JSON, XML을 쉽게 응답받는다.
자 이제 WebFlux의 WebClient에 대해 알아보았으니, Spring에서 적용해보자.
1. build.gradle
implementation 'org.springframework.boot:spring-boot-starter-webflux'
위 문장 추가.
2. AppConfig
@Configuration
public class AppConfig {
@Bean
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
}
WebClient 빌더를 사용하기 위해 Bean으로 생성한다!
3. 서비스 로직
Object 형식 리턴
@Transactional(readOnly = true)
public Mono<Response> sendToServer(ReqeustData requestData) {
WebClient webClient = webClientBuilder.baseUrl("http://localhost:3000").build();
return webClient.post()
.uri("/predict")
.bodyValue(requestData)
.retrieve()
.bodyToMono(Response.class);
}
설명
1. RequestData 라는 DTO에 담긴 값을 "http://localhost:3000/predict" 라는 외부 서버 url로 전송한다.
2. 외부 서버에서는 /predict 엔드포인트의 POST 요청을 처리하여 현재 웹서버에 값을 전송한다.
3. 전송 받은 값은 Response 라는 객체에 매핑되어 전송된다.
List 형식 리턴
@Transactional(readOnly = true)
public Mono<List<WebServerToClientDTO>> sendToModelServerAndSendToClient(PromptResponseDTO promptResponseDTO) {
WebClient webClient = webClientBuilder.baseUrl(baseUrl).build();
Mono<List<ModelServerToWebServerDTO>> listMono = webClient.post()
.uri("/prediction")
.bodyValue(promptResponseDTO)
.retrieve()
.bodyToFlux(ModelServerToWebServerDTO.class)
.collectList();
return listMono.flatMap(this::findBeveragesWithSimilarity);
}
private Mono<List<WebServerToClientDTO>> findBeveragesWithSimilarity(List<ModelServerToWebServerDTO> dtos) {
List<WebServerToClientDTO> beverageSimilarityList = dtos.stream()
.sorted(Comparator.comparing(ModelServerToWebServerDTO::getSimilarity).reversed()) // similarity 기준 내림차순 정렬
.limit(8) // 상위 8개 선택
.map(dto -> {
List<Object[]> result = beverageRepository.findBeverageAndRandomOtherBeverage(dto.getId());
if (result.isEmpty()) {
return null; // 없으면 null
}
Object[] row = result.get(0);
Long beverageId = ((Number) row[0]).longValue();
String name = (String) row[1];
Type type = Type.valueOf((String) row[2]); // Enum 타입인 경우
String photo = (String) row[3];
Integer price = (Integer) row[4];
String caffein = (String) row[5];
String fat = (String) row[6];
String kcal = (String) row[7];
String natrium = (String) row[8];
String protein = (String) row[9];
String sugar = (String) row[10];
String otherBeverageName = (String) row[11];
Nutrition nutrition = new Nutrition(caffein, fat, kcal, natrium, protein, sugar);
return new WebServerToClientDTO(dto.getSimilarity(), beverageId, name, nutrition, type, price, photo, otherBeverageName, dto.getCafe());
})
.filter(Objects::nonNull) // null 값을 제외
.collect(Collectors.toList());
return Mono.just(beverageSimilarityList);
}
코드 분석
1. 클라이언트가 Spring 서버로 PromptResponseDTO를 전송한다.
2. Spring에서 외부 서버의 /predict 엔드포인트로 PromptResponseDTO를 전송한 뒤, ModelServerToWebServerDTO에 매핑되는 객체 리스트를 전달 받는다.
public Mono<List<WebServerToClientDTO>> sendToModelServerAndSendToClient(PromptResponseDTO promptResponseDTO) {
WebClient webClient = webClientBuilder.baseUrl(baseUrl).build();
Mono<List<ModelServerToWebServerDTO>> listMono = webClient.post()
.uri("/prediction")
.bodyValue(promptResponseDTO)
.retrieve()
.bodyToFlux(ModelServerToWebServerDTO.class)
.collectList();
return listMono.flatMap(this::findBeveragesWithSimilarity);
}
3. 해당 리스트의 각 element마다 findBeveragesWithSimilarity 라는 메서드를 listMono.flatMap을 통해 적용한다.
이때 외부 서버에서 전달해준 List<ModelServerToWebServerDTO> 를 parameter로 넣어준다.
return 값이 새로운 객체 리스트이기 때문에 Mono.just(List); 형식으로 반환한다.
private Mono<List<WebServerToClientDTO>> findBeveragesWithSimilarity(List<ModelServerToWebServerDTO> dtos) {
List<WebServerToClientDTO> beverageSimilarityList = dtos.stream()
.sorted(Comparator.comparing(ModelServerToWebServerDTO::getSimilarity).reversed()) // similarity 기준 내림차순 정렬
.limit(8) // 상위 8개 선택
.map(dto -> {
List<Object[]> result = beverageRepository.findBeverageAndRandomOtherBeverage(dto.getId());
if (result.isEmpty()) {
return null; // 없으면 null
}
Object[] row = result.get(0);
Long beverageId = ((Number) row[0]).longValue();
String name = (String) row[1];
Type type = Type.valueOf((String) row[2]); // Enum 타입인 경우
String photo = (String) row[3];
Integer price = (Integer) row[4];
String caffein = (String) row[5];
String fat = (String) row[6];
String kcal = (String) row[7];
String natrium = (String) row[8];
String protein = (String) row[9];
String sugar = (String) row[10];
String otherBeverageName = (String) row[11];
Nutrition nutrition = new Nutrition(caffein, fat, kcal, natrium, protein, sugar);
return new WebServerToClientDTO(dto.getSimilarity(), beverageId, name, nutrition, type, price, photo, otherBeverageName, dto.getCafe());
})
.filter(Objects::nonNull) // null 값을 제외
.collect(Collectors.toList());
return Mono.just(beverageSimilarityList);
}
<참고> WebClient Response
- retrieve() : body를 받아 디코딩하는 간단한 메소드
- exchange() : ClientResponse를 상태값 그리고 헤더와 함께 가져오는 메소드
exchange()를 통해 세세한 컨트롤이 가능하지만, Response 컨텐츠에 대한 모든 처리를 직접 하면서 메모리 누수 가능성 때문에 retrieve()를 권고하고 있다고 한다.
bodyToFlux, bodyToMono 는 가져온 body를 각각 Reactor의 Flux와 Mono 객체로 바꿔준다.
Mono 객체는 0-1개의 결과를 처리하는 객체이고, Flux는 0-N개의 결과를 처리하는 객체이다.
따라서 bodyToFlux().collectList()를 사용하여 List 타입으로 캐스팅해줘야 한다.
참고
Spring WebClient, 어렵지 않게 사용하기
WebClient는 스프링 5.0에서 추가된 Blocking과 Non-Blocking 방식을 지원하는 HTTP 클라이언트입니다. - Reactor, 제대로 사용하기 - Error Handling - Reactive Programming, 제대로 이해하기 👉🏻 WebClient 소개 - Spring W
gngsn.tistory.com
'Back-end > Spring' 카테고리의 다른 글
[Spring] Spring Security + OAuth2 + JWT [카카오 소셜 로그인 구현] (1) | 2024.06.18 |
---|---|
[Spring] JPA Hibernate의 ddl-auto 속성 분석하기 (0) | 2024.06.16 |
[Spring] Swagger 사용 및 JWT과 https 적용해보기 (2) | 2024.06.05 |
[Spring] data.sql을 사용하여 서버 실행 시 데이터 삽입하기 (1) | 2024.05.28 |
[Spring] JPA "Row size too large" 문제 분석하기 (0) | 2024.05.26 |