본문 바로가기
Spring/SpringBoot

[SpringBoot] WebClient를 이용하여 외부 API 호출하기

by J4J 2023. 3. 15.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 webClient를 이용하여 외부 api 호출하는 방법에 대해 적어보는 시간을 가져보려고 합니다.

 

 

반응형

 

 

WebClient란?

 

webClient는 spring 5에서 부터 등장한 HTTP 클라이언트 라이브러리입니다.

 

여기서 말하는 HTTP 클라이언트라고 하는 것은 HTTP 프로토콜을 이용하여 서버와 통신하는 것을 의미하며 다른 말로는 서버에 API 요청을 보내는 주체라고도 말할 수 있습니다.

 

 

 

webClient가 등장하기 이전까지는 spring에서 자주 사용되던 HTTP 클라이언트로 restTemplate이 존재했었습니다.

 

그래서 spring에서 다른 서버와 통신을 하기 위해서는 restTemplate를 사용하고는 했는데 webClient가 등장한 이후로는 webClient의 사용을 권장하고 있습니다.

 

restTemplate과 비교했을 때 webClient가 가지는 장점들은 다음과 같이 있습니다.

 

  • 비동기적으로 요청하는 non-blocking 처리 방식
  • 요청을 보내고 응답을 받을 때까지 대기하지 않기 때문에 처리 속도가 빠름
  • 비동기 처리 방식으로 인해 대용량 처리를 할 때 용이함

 

 

 

 

Dependency 추가

 

webClient를 사용하기 위해서는 아래에 해당되는 dependency를 추가해줘야 합니다.

 

dependencies {
    implementation group: 'org.springframework.boot', name: 'spring-boot-starter-webflux'
}

 

 

 

 

Get

 

webClient를 이용하여 일반적인 API 요청을 주고받는 예제 코드를 간단하게 작성해 보겠습니다.

 

먼저 get과 관련된 것을 보겠습니다.

 

다음과 같은 API를 webClient를 이용하여 호출해 보도록 하겠습니다.

 

get api

 

package com.webclient.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;

import java.util.HashMap;
import java.util.Map;

@Service
@Slf4j
public class WebClientServiceImpl {

    public void get() {
        String code = "myCode";

        // webClient 기본 설정
        WebClient webClient =
                WebClient
                        .builder()
                        .baseUrl("http://localhost:8080")
                        .build();

        // api 요청
        Map<String, Object> response =
                webClient
                        .get()
                        .uri(uriBuilder ->
                                uriBuilder
                                        .path("/api/get")
                                        .queryParam("code", code)
                                        .build())
                        .retrieve()
                        .bodyToMono(Map.class)
                        .block();

        // 결과 확인
        log.info(response.toString());
    }
}

 

 

 

코드를 위와 같이 작성한 뒤 다음과 같이 간단하게 테스트 코드를 짜서 실행해 보면 다음과 같은 결과를 확인할 수 있습니다.

 

package com.webclient;

import com.webclient.service.WebClientServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class WebClientApplicationTests {

    @Autowired
    private WebClientServiceImpl webClientService;

    @Test
    void get() {
        webClientService.get();
    }
}

 

2023-03-15 21:40:50.580  INFO 18660 --- [    Test worker] c.w.service.WebClientServiceImpl         : {code=myCode, message=Success}

 

 

 

 

Post

 

post도 사용 방법이 get과 큰 차이가 없습니다.

 

다음과 같은 API를 webClient를 이용하여 호출해 보도록 하겠습니다.

 

post api

 

package com.webclient.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;

import java.util.HashMap;
import java.util.Map;

@Service
@Slf4j
public class WebClientServiceImpl {
    public void post() {
        Map<String, Object> bodyMap = new HashMap<>();
        bodyMap.put("name", "j4j");
        bodyMap.put("age", 123);

        // webClient 기본 설정
        WebClient webClient =
                WebClient
                        .builder()
                        .baseUrl("http://localhost:8080")
                        .build();

        // api 요청
        Map<String, Object> response =
                webClient
                        .post()
                        .uri("/api/post")
                        .bodyValue(bodyMap)
                        .retrieve()
                        .bodyToMono(Map.class)
                        .block();

        // 결과 확인
        log.info(response.toString());
    }
}

 

 

 

코드를 위와 같이 작성하고 다음과 같이 테스트 코드를 짜서 실행해 보면 아래와 같은 결과를 확인할 수 있습니다.

 

package com.webclient;

import com.webclient.service.WebClientServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class WebClientApplicationTests {

    @Autowired
    private WebClientServiceImpl webClientService;

    @Test
    void post() {
        webClientService.post();
    }
}

 

2023-03-15 22:02:13.878  INFO 15788 --- [    Test worker] c.w.service.WebClientServiceImpl         : {name=j4j, message=Success , age=123}

 

 

 

 

Default 설정

 

정말 단순하게 get, post 요청 등을 수행하는 코드는 위와 같이 다뤄봤습니다.

 

이번엔 조금 더 상세화된 내용들을 다뤄보겠습니다.

 

가장 먼저 default 설정값입니다.

 

 

 

webClient를 이용하여 코드를 작성하다 보면 하나의 메서드 안에 여러 API 요청이 수행될 수도 있습니다.

 

또한 API에 데이터를 담다 보면 API마다 공통적으로 들어가야 하는 header값이나 cookie 등의 값들이 존재할 수 있습니다.

 

코드로 예를 들면 다음과 같습니다.

 

package com.webclient.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.util.HashMap;
import java.util.Map;

@Service
@Slf4j
public class WebClientServiceImpl {
    public void defaultValue() {
        String code = "myCode";

        // webClient 기본 설정
        WebClient webClient =
                WebClient
                        .builder()
                        .baseUrl("http://localhost:8080")
                        .build();

        // api 요청 - 1
        webClient
                .get()
                .uri(uriBuilder ->
                        uriBuilder
                                .path("/api/get")
                                .queryParam("code", code)
                                .build())
                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .cookie("cookie", "cookieValue")
                .retrieve()
                .bodyToMono(Map.class)
                .block();

        // api 요청 - 2
        webClient
                .get()
                .uri(uriBuilder ->
                        uriBuilder
                                .path("/api/get")
                                .queryParam("code", code)
                                .build())
                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .cookie("cookie", "cookieValue")
                .retrieve()
                .bodyToMono(Map.class)
                .block();
    }
}

 

 

 

위의 코드를 보면 API마다 공통적으로 header와 cookie가 들어가기 때문에 요청할 때마다 데이터를 담아서 전달해주고 있습니다.

 

하지만 매번 요청할 때마다 데이터를 담아주는 것은 불편한 반복 작업 중 하나이고, 이를 위한 개선 방법으로 webClient를 처음 생성할 때 default 값을 다음과 같이 담아 줄 수 있습니다.

 

package com.webclient.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;

import java.util.HashMap;
import java.util.Map;

@Service
@Slf4j
public class WebClientServiceImpl {
    public void defaultValue() {
        String code = "myCode";

        // webClient 기본 설정
        WebClient webClient =
                WebClient
                        .builder()
                        .baseUrl("http://localhost:8080")
                        .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                        .defaultCookie("cookie", "cookieValue")
                        .build();

        // api 요청 - 1
        webClient
                .get()
                .uri(uriBuilder ->
                        uriBuilder
                                .path("/api/get")
                                .queryParam("code", code)
                                .build())
                .retrieve()
                .bodyToMono(Map.class)
                .block();

        // api 요청 - 2
        webClient
                .get()
                .uri(uriBuilder ->
                        uriBuilder
                                .path("/api/get")
                                .queryParam("code", code)
                                .build())
                .retrieve()
                .bodyToMono(Map.class)
                .block();
    }
}

 

 

 

 

Retrieve Return

 

지금까지 코드들을 잘 살펴보면 get, post 등의 요청을 한 뒤에 .retrieve() 라는 코드가 들어가 있는 것을 볼 수 있습니다.

 

그리고 여기서 사용되는 retrieve는 일반적으로 webClient로 request를 서버에 전달한 응답 값을 추출하기 위해 사용됩니다.

 

retrieve의 return 값으로는 총 3개가 있습니다.

 

  • toEntity → response 자체를 얻기 위해 사용
  • bodyToMono → response body 데이터를 얻기 위해 사용
  • bodyToFlux → response body 데이터를 stream으로 활용하기 위해 사용

 

 

 

현재까지는 모두 코드들에 bodyToMono들이 사용되었습니다.

 

여기서는 bodyToMono를 제외하고 toEntity, bodyToFlux를 사용했을 때 어떤 결과들을 확인할 수 있는지 코드로 작성해 보겠습니다.

 

 

 

[ 1. toEntity ]

 

package com.webclient.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
@Slf4j
public class WebClientServiceImpl {
    public void getEntity() {
        String code = "myCode";

        // webClient 기본 설정
        WebClient webClient =
                WebClient
                        .builder()
                        .baseUrl("http://localhost:8080")
                        .build();

        // api 요청
        ResponseEntity<Map> response =
                webClient
                        .get()
                        .uri(uriBuilder ->
                                uriBuilder
                                        .path("/api/get")
                                        .queryParam("code", code)
                                        .build())
                        .retrieve()
                        .toEntity(Map.class)
                        .block();

        // 결과 확인
        log.info(response.toString());
    }
}

 

package com.webclient;

import com.webclient.service.WebClientServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class WebClientApplicationTests {

    @Autowired
    private WebClientServiceImpl webClientService;

    @Test
    void getEntity() {
        webClientService.getEntity();
    }
}

 

2023-03-15 22:48:24.260  INFO 13808 --- [    Test worker] c.w.service.WebClientServiceImpl         : <200,{code=myCode, message=Success},[Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", Content-Type:"application/json", Transfer-Encoding:"chunked", Date:"Wed, 15 Mar 2023 13:48:24 GMT"]>

 

 

 

[ 2. bodyToFlux ]

 

package com.webclient.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
@Slf4j
public class WebClientServiceImpl {
    public void getFlux() {
        String code = "myCode";

        // webClient 기본 설정
        WebClient webClient =
                WebClient
                        .builder()
                        .baseUrl("http://localhost:8080")
                        .build();

        // api 요청
        List<String> response =
                webClient
                        .get()
                        .uri(uriBuilder ->
                                uriBuilder
                                        .path("/api/get")
                                        .queryParam("code", code)
                                        .build())
                        .retrieve()
                        .bodyToFlux(Map.class)
                        .toStream()
                        .map(map -> map.toString())
                        .collect(Collectors.toList());

        // 결과 확인
        log.info(response.toString());
    }
}

 

package com.webclient;

import com.webclient.service.WebClientServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class WebClientApplicationTests {

    @Autowired
    private WebClientServiceImpl webClientService;

    @Test
    void getFlux() {
        webClientService.getFlux();
    }
}

 

2023-03-15 22:48:41.571  INFO 40948 --- [    Test worker] c.w.service.WebClientServiceImpl         : [{code=myCode, message=Success}]

 

 

 

 

동기처리 방식

 

공식문서에 따르면 동기처리 방식을 사용하기 위해 webClient에서 block()을 사용한다고 되어 있습니다.

 

그리고 위의 코드들을 다시 살펴보면 block()들이 사용되고 있는 것도 볼 수 있습니다.

 

 

 

만약 block()을 사용하지 않고 비동기처리 방식으로 코드를 작성하면 다음과 같은 코드로 작성될 수 있습니다.

 

package com.webclient.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
@Slf4j
public class WebClientServiceImpl {
    public void getMultiple() {
        String code = "myCode";

        // webClient 기본 설정
        WebClient webClient =
                WebClient
                        .builder()
                        .baseUrl("http://localhost:8080")
                        .build();

        // api 요청
        Mono<Map> responseMono =
                webClient
                        .get()
                        .uri(uriBuilder ->
                                uriBuilder
                                        .path("/api/get")
                                        .queryParam("code", code)
                                        .build())
                        .retrieve()
                        .bodyToMono(Map.class);
    }
}

 

 

 

하지만 위와 같이 코드를 작성하게 될 경우 Map에 담겨있는 데이터 값을 확인할 수가 없었습니다.

 

그래서 block()을 추가하여 Map에 담겨있는 값을 확인했었습니다.

 

 

 

그럼 여기서 의문이 드는 것은 block()을 추가할 경우 비동기로 처리가 되지 않는다는 것이고, 한 메서드에 여러 API 요청을 할 때 순서대로 처리가 진행된다는 것입니다.

 

반대로 block()을 추가하지 않으면 응답된 데이터를 확인할 수 없습니다.

 

이런 상황을 위해 webClient에서는 Mono.zip이라는 메서드를 제공해주고 있으며 다음과 같이 코드를 작성해 볼 수 있습니다.

 

package com.webclient.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
@Slf4j
public class WebClientServiceImpl {
    public void getMultiple() {
        String code = "myCode";

        // webClient 기본 설정
        WebClient webClient =
                WebClient
                        .builder()
                        .baseUrl("http://localhost:8080")
                        .build();

        // api 요청 - 1
        Mono<Map> responseMono1 =
                webClient
                        .get()
                        .uri(uriBuilder ->
                                uriBuilder
                                        .path("/api/get")
                                        .queryParam("code", code)
                                        .build())
                        .retrieve()
                        .bodyToMono(Map.class);

        // api 요청 - 2
        Mono<Map> responseMono2 =
                webClient
                        .get()
                        .uri(uriBuilder ->
                                uriBuilder
                                        .path("/api/get")
                                        .queryParam("code", code)
                                        .build())
                        .retrieve()
                        .bodyToMono(Map.class);

        // multiple api 요청
        Map<String, Object> response =
                Mono
                        .zip(responseMono1, responseMono2, (response1, response2) -> {
                            Map<String, Object> map = new HashMap<>();
                            map.put("response1", response1);
                            map.put("response2", response2);

                            return map;
                        })
                        .block();

        // 결과 확인
        log.info(response.toString());
    }
}

 

 

 

위와 같이 코드를 작성하고 테스트 코드를 실행해 보면 아래와 같이 각 API들의 응답에 대해 blocking 되는 것을 해결하며 또한 body에 담겨있는 데이터를 확인해 볼 수 있습니다.

 

package com.webclient;

import com.webclient.service.WebClientServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class WebClientApplicationTests {

    @Autowired
    private WebClientServiceImpl webClientService;

    @Test
    void getMultiple() {
        webClientService.getMultiple();
    }
}

 

2023-03-15 23:04:05.328  INFO 29744 --- [    Test worker] c.w.service.WebClientServiceImpl         : {response2={code=myCode, message=Success}, response1={code=myCode, message=Success}}

 

 

 

 

 

 

 

 

이상으로 webClient를 이용하여 외부 api 호출하는 방법에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글