[Spring] WebClient를 사용한 외부 API 통신

2024. 6. 16. 16:48·Back-end/Spring
반응형

개발진스 [출처 @dev_hee 님]

 

사이드 프로젝트를 진행하며 모델 서버와의 연동이 필요했는데, 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 타입으로 캐스팅해줘야 한다.

 

 

참고

https://gngsn.tistory.com/154

 

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을 사용하여 서버 실행 시 데이터 삽입하기  (2) 2024.05.28
[Spring] JPA "Row size too large" 문제 분석하기  (0) 2024.05.26
'Back-end/Spring' 카테고리의 다른 글
  • [Spring] Spring Security + OAuth2 + JWT [카카오 소셜 로그인 구현]
  • [Spring] JPA Hibernate의 ddl-auto 속성 분석하기
  • [Spring] Swagger 사용 및 JWT과 https 적용해보기
  • [Spring] data.sql을 사용하여 서버 실행 시 데이터 삽입하기
류건
류건
개발 일지
  • 류건
    건's Dev
    류건
  • 전체
    오늘
    어제
    • 분류 전체보기 (96)
      • Back-end (56)
        • Spring (31)
        • Nest.js (3)
        • Next.js (2)
        • Node.js (3)
      • Infra & Cloud (21)
        • Cloud Computing (6)
        • Docker (3)
        • AWS (7)
      • Java (2)
      • Computer Science (12)
        • Computer Network (0)
        • Operating System (0)
        • 정보 보호와 시스템 보안 (12)
        • Software Architecture (0)
      • 회고록 (4)
        • 우아한테크코스 (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Nest.js
    EC2
    node.js
    db
    nginx
    ddl-auto
    CORS
    express.js
    WebClient
    public key
    ssl
    보안
    Github Actions
    CD
    Lock
    Spring Boot
    JPA
    정보보호
    고가용성
    오블완
    Docker
    Kafka
    https
    티스토리챌린지
    Webflux
    어노테이션
    aws
    JWT
    CI/CD
    Spring
  • 최근 댓글

  • 최근 글

  • 반응형
  • hELLO· Designed By정상우.v4.10.0
류건
[Spring] WebClient를 사용한 외부 API 통신
상단으로

티스토리툴바