빙응의 공부 블로그

[Spring]WebClient 본문

Spring/개인공부_실습

[Spring]WebClient

빙응이 2024. 5. 24. 18:47

[Spring API]Spring WebFlux와 WebClient (tistory.com)

 

[Spring API]Spring WebFlux와 WebClient

📝WebFlux웹플럭스란 적은 쓰레드로 동시 처리를 제어하고 적은 하드웨어 리소스로 확장하기 위해 만들어진반응형 웹 프레임워크이다.🚩 탄생계기그렇다면 스프링 웹플럭스는 왜 만들어졌을까

quddnd.tistory.com

https://godekdls.github.io/Reactive%20Spring/webclient/

 

WebClient

스프링5 웹 리액티브 스택 WebClient 한글 번역

godekdls.github.io

 

스프링 MVC에서  WebClient를 사용해보자 

📝WebClient 생성

기본적인 생성
  • create()메서드를 사용하여 간단하게 생성이 가능하다
WebClient.create()
WebClient.create(String baseUrl)

🚩 WebClient 인스턴스 생성

  • WebClient를 제대로 사용하기 위해서는 WebClient.Builder를 통해 WebClient 인스턴스를 생성해야 한다.
  • 물론 우리는 스프링 MVC에서 사용하기에 편의성을 위해 빈으로 등록해서 사용할 것이다. 
  • WebClient를 설정하기 위해서는 여러 옵션을 고려하며 주로 baseUrl, timeout, connection pool, interceptor 등 의 설정이 필요하다.
1. baseUrl 설정 
@Configuration
public class WebClientConfig {
  @Value("${url}")
  private String baseUrl;

  @Bean
  public WebClient webClient(){
    return WebClient.builder()
        .baseUrl(baseUrl)
        .build();
  }
}
  • baseUrl은 공통저긍로 사용될 기본 URL을 의미한다. 
  • baseUri을 설정하려면 WebClient.Builder의 baseUrl 메서드를 이용하면 된다.
2. 타임아웃 설정
@Configuration
public class WebClientConfig {
  @Value("${url}")
  private String baseUrl;

  @Bean
  public WebClient webClient(){
    HttpClient httpClient = HttpClient.create()
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) //연걸 타임아웃 5000
        .responseTimeout(Duration.ofMillis(5000)) //응답 타임아웃 5000
        .doOnConnected(conn ->
            conn.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS)) //읽기 타임아웃 5000
                .addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS))); //쓰기 타임아웃 5000

    return WebClient.builder()
        .baseUrl(baseUrl)
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();
  }
}
  • clientConnector 메소드를 이용해서 각 처리에 대한 타임아웃을 설정할 수 있다.
3. Connetion Poll 설정
  • WebClient는 내부적으로 Reactor Netty라는 것을 사용한다. 이것은 네트워크 애플리케이션을 효율적으로 개발할 수 있도록 도와주는 것인데, 우리는 Netty가 다중 연결을 관리하기 위한 내부 Connection Pool을 설정해주는 것이다.
  • 꼭 따로 설정안해도 되지만 필요에 다라 커스터마이징이 가능하다.
@Bean
public WebClient webClient() {
    TcpClient tcpClient = TcpClient.create()
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
            .doOnConnected(conn -> conn
                    .addHandlerLast(new ReadTimeoutHandler(5))
                    .addHandlerLast(new WriteTimeoutHandler(5)));

    return WebClient.builder()
            .baseUrl(baseUrl)
            .clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient)))
            .build();
}
4. Interceptor 추가
  • 요청과 응답에 대해 처리해야 할 내용이 있다면 Interceptor를 추가하여 처리할 수 있다,
@Bean
public WebClient webClient() {
    ExchangeStrategies strategies = ExchangeStrategies.builder()
            .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(16 * 1024 * 1024))
            .build();

    return WebClient.builder()
            .baseUrl(baseUrl)
            .exchangeStrategies(strategies)
            .filter((request, next) -> {
                // 요청 전처리
                System.out.println("Request: " + request.method() + " " + request.url());
                return next.exchange(request);
            })
            .filter((response, next) -> {
                // 응답 후처리
                System.out.println("Response status code: " + response.statusCode());
                return next.exchange(response);
            })
            .build();
}

 

📝 WebClient 사용

  • WebClient는 다양한 요청을 보내며 커스텀할 수 있다.

🚩 첫단. HTTP 요청 메서드

GET 요청 보내기
webClient.get()
    .uri("https://example.com/api/resource")
    .retrieve()
    .bodyToMono(String.class);
POST 요청 보내기
webClient.post()
    .uri("https://example.com/api/resource")
    .contentType(MediaType.APPLICATION_JSON)
    .body(BodyInserters.fromValue(requestBody))
    .retrieve()
    .bodyToMono(String.class);

 

PUT 요청 보내기
webClient.put()
    .uri("https://example.com/api/resource/{id}", resourceId)
    .contentType(MediaType.APPLICATION_JSON)
    .body(BodyInserters.fromValue(requestBody))
    .retrieve()
    .bodyToMono(String.class);
DELETE 요청 보내기
webClient.delete()
    .uri("https://example.com/api/resource/{id}", resourceId)
    .retrieve()
    .bodyToMono(Void.class); // 요청이 몸체를 반환하지 않는 경우

 

🚩 요청 설정

Method 설명
uri() 요청할 URI를 설정한다.
header() 헤더를 추가한다.
cookie(0 쿠키를 추가한다.
bodyValue() 요청 본문에 값을 설정한다.
body(), syncBody() 요청 본문을 설정한다.

 

🚩 응답 처리 

Method 설명
retrieve() 요청을 실행하고 응답을 받습니다.
exchange() 요청을 실행하고 응답을 ClientResponse 객체로 받습니다.
Method 설명
bodyToMono() 응답을 Mono로 변환합니다.
bodyToFlux() 응답을 Flux로 변환합니다.

🚩 오류 처리

Method 설명
onStatus() 특정 상태 코드에 대한 처리를 설정합니다.
onErrorMap() 오류를 다른 오류로 매핑합니다.
onErrorResume() 오류가 발생했을 때 대체 값을 제공합니다.

🚩 기타

Method 설명
uriBuilderFactory() URI 빌더 팩토리를 설정합니다.
exchangeStrategies() 교호나 전략을 설정합니다.
filter() 필터를 적용합니다.

 

📝 WebClient 실습 

[Spring]Spring - RestTemplate (tistory.com)

 

[Spring]Spring - RestTemplate

API를 이용하여 원하는 데이터 정보를 가져와야 하는데 XML처리를 해야한다. 이것에 대해 알아보자 📝RestTemplate HTTP 통신을 위한 도구로 RESTful API 웹 서비스와의 상호작용을 쉽게 위부 도메인에

quddnd.tistory.com

전에 했었던 RestTemplate를 바꿔보자

  @GetMapping("/jobs")
  public ResponseEntity<ResponseData.Items> callApi(
      @RequestParam(value = "local") String local
  ) {
    log.info("API 호출:  local={}", local);

    try {
      // API 호출 및 응답 받기
      RestTemplate restTemplate = new RestTemplate();
      String url = apiUrl + "?serviceKey=" + secretKey + "&pageNo=1" + "&numOfRows=200";
      ResponseEntity<ResponseData> responseEntity = restTemplate.getForEntity(url, ResponseData.class);

      // API 응답 확인 및 처리
        ResponseData responseData = responseEntity.getBody();
        if (responseData != null && responseData.getBody() != null && responseData.getBody().getItems() != null) {
          List<ResponseData.Item> filteredItems = new ArrayList<>();
          String targetPrefix = local.split(" ")[0]; // local 파라미터를 공백을 기준으로 분리하고 첫 번째 단어를 선택
          for (ResponseData.Item item : responseData.getBody().getItems().getItem()) {
            if (item.getCompAddr() != null && item.getCompAddr().startsWith(targetPrefix)) {
              filteredItems.add(item);
            }
          }
          responseData.getBody().getItems().setItem(filteredItems);
        }
        return new ResponseEntity<>(responseData.getBody().getItems(), HttpStatus.OK);
	/* 오류처리 */
  1. RestTemplate로 요청을 받는다.
  2. local 파라미터로 받아온 것으로 필터링한다.
1. WebClientConfig 설정
  • 재사용성 있는 WebClient를 위해 Config를 만들어주자
@Configuration
public class WebClientConfig {
  @Value("${url}")
  private String apiUrl;

  @Bean
  public WebClient webClient() {
    HttpClient httpClient = HttpClient.create() //타임아웃 설정을 위한 객체 
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
        .responseTimeout(Duration.ofMillis(5000))
        .doOnConnected(conn ->
            conn.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS))
                .addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS)));

    return WebClient.builder()
        .baseUrl(apiUrl) //기초 URI 설정
        .clientConnector(new ReactorClientHttpConnector(httpClient)) //타임아웃 설정
        .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) //헤더 타입 JSON
        .build();
  }
}

 

2. 기능 똑같이 구현하기 
@GetMapping("/jobs")
public Mono<ResponseEntity<List<ResponseData.Item>>> callApi(@RequestParam(value = "local") String local) {
  log.info("API 호출:  local={}", local);

  try {
    // API 호출 및 응답 받기
    return webClient.get()
        .uri(uriBuilder -> uriBuilder //파라미터를 통한 URI 구성
            .queryParam("serviceKey", secretKey)
            .queryParam("pageNo", 1)
            .queryParam("numOfRows", 200)
            .build())
        .retrieve() // 응답을 받아온다.
        .bodyToMono(ResponseData.class) //받은 응답을 직접만든 DTO로 변환 
        .map(responseData -> {
          // API 응답 확인 및 처리
          if (responseData != null && responseData.getBody() != null && responseData.getBody().getItems() != null) {
            List<ResponseData.Item> filteredItems = responseData.getBody().getItems().getItem().stream()
                .filter(item -> item.getCompAddr() != null && item.getCompAddr().startsWith(local.split(" ")[0]))
                .collect(Collectors.toList());
            responseData.getBody().getItems().setItem(filteredItems);
          }
          return ResponseEntity.ok(responseData.getBody().getItems().getItem());
        });
  } catch (Exception e) {
    // 그 외의 예외 발생
    log.error("API 호출 중 에러 발생", e);
    return Mono.just(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
  }
}