Search
Duplicate

스프링에서 HTTP 요청 보내기 - RestTemplate vs WebClient

글감
노트
BE
Spring
작성자
작성 일자
2023/05/25 00:16
상태
완료
공개여부
공개
Date
생성자
작업자

개요

Spring Security 안쓰고 AOP로 OAuth 구현하기 - 1 를 진행하면서, Service에서 HTTP 요청을 보낼 때 RestTemplate을 사용하였는데, Depricated 된다는 괴담 + 우주님의 제보가 있었고, 공식적으로 추천하는 WebClient로 변경하기 전에, HTTP 요청하는 방식에 대해 좀 더 알아보려고 한다.

옛날옛적에는..

예전에 사용했던 방식을 한 번 짚어보자.
1.
HttpURLConnection: Java에서 기본적으로 제공하는 HttpURLConnection 클래스를 사용하여 HTTP 요청을 보낼 수 있었다고 한다. 스프링과는 별도로 Java의 표준 기능이다. URLConnection 클래스를 상속받아 HTTP 통신을 처리한다고 한다.
예시 코드
2.
HttpClient: Apache HttpClient은 Java HTTP 클라이언트 라이브러리로, 옛날부터 많이 사용되었다고 한다. 스프링에서도 HttpClient를 지원한다.
예시 코드
HttpURLConnection보다는 HttpClient가 코드가 이뻐보인다.

RestTemplate?

RestTemplate은 스프링 3.0부터 도입된 클래스로, 동기적인 HTTP 통신을 위해 사용된다.
간편한 API를 제공하여 HTTP 요청 메서드(GET, POST, PUT, DELETE 등)를 지정하고, 요청 및 응답의 객체 매핑을 수행한다.
주요 기능은 다음과 같다.
요청 및 응답의 객체 매핑: JSON, XML 등의 데이터를 자바 객체로 변환하거나, 자바 객체를 요청 데이터로 변환할 수 있다.
요청 헤더 및 쿠키 설정: 요청 헤더에 특정 값을 설정하거나, 쿠키를 사용할 수 있다.
인터셉터 지원: 요청 및 응답에 대한 인터셉터를 등록하여 전/후 처리 작업을 수행할 수 있다.
인터셉트 예시 코드

WebClient?

WebClient는 스프링 5부터 도입된 클래스로, 비동기 및 리액티브(non-blocking 및 reactive) 방식의 HTTP 통신을 위해 사용된다. WebClient는 Reactor 라이브러리를 기반으로 구현되어 비동기적으로 HTTP 요청을 처리하고, 리액티브 스트림을 활용하여 응답을 처리할 수 있다. (참고 자료 : [Java] Reactive Stream 이란?)
주요 기능은 다음과 같다.
리액티브 스트림 사용: 리액티브 스트림을 사용하여 Backpressure(Publisher와 Subscriber 간의 데이터 처리 속도 차이를 조절하여 데이터의 생산과 소비 간의 밸런스를 맞추는 것)를 지원하고, 비동기적인 데이터 처리를 할 수 있다.
함수형 API: 함수형 프로그래밍 스타일의 API를 제공하여 간결하고 유연한 코드 작성이 가능하다. (참고 자료 : 자바코드로 보는 함수형 프로그래밍 (Functional Programming in Java))
요청 및 응답의 객체 매핑: JSON, XML 등의 데이터를 자바 객체로 변환하거나, 자바 객체를 요청 데이터로 변환할 수 있다.
필터 및 인터셉터 지원: WebClient.Builder를 통해 필터와 인터셉터를 등록하여 전/후 처리 작업을 수행할 수 있다.
인터셉트 예시 코드

RestTemplate vs WebClient

주요한 차이점은 RestTemplate은 동기(BIO, Blocking I/O)방식으로 작동하고, WebClient는 비동기(NIO, Non-Blocking I/O)방식으로 작동한다는 점이다. (참고자료 : 스프링의 싱글톤과 멀티 스레딩)
동기 vs 비동기라면 컨트롤만 가능하다면 당연히 효율적으로 비동기가 좋을 것이다.
그렇다면 WebClient는 Thread-Safe한 것일까? 답은 Yes다.
WebClient는 내부적으로 Reactor 라이브러리를 기반으로 구현되어 있고, 리액티브 스트림의 개념을 사용하여 비동기 작업을 처리한다. 이 때, 리액티브 스트림은 데이터 처리를 위해 Publisher(데이터를 발행하는 역할)와 Subscriber(데이터를 구독하고 처리하는 역할) 사이의 계약(Contract)을 정의한다.
WebClient는 여러 요청을 동시에 처리하기 위해 내부적으로 리액티브 스트림을 활용하고, 각 요청은 Publisher로써 동작한다. 이 때, 요청은 스트림의 개별 이벤트로 처리되며, 리액티브 스트림의 특성을 통해 비동기적으로 처리된다.
WebClient의 비동기 처리는 리액티브 스트림의 전용 스레드 풀 내에서 이루어지며, 내부적으로 스레드 풀을 관리하여 스레드를 효율적으로 사용한다. 따라서 여러 요청이 동시에 처리되더라도 각각의 요청은 서로 다른 스레드에서 병렬로 처리되어 안전하게 동작한다.
WebClient의 Publisher, Subscriber, Contract는 뭘까..?

적용, 테스트 작성하기

이전에 작성한 글(Spring Security 안쓰고 AOP로 OAuth 구현하기 - 2)에 있던 OauthService를 리팩터링했습니다.

서비스 - 코드로 토큰 받아오기

// ................ 이전 코드 ................ public String getGoogleToken(String code) { ObjectMapper objectMapper = new ObjectMapper(); RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); MultiValueMap<String, String> map = new LinkedMultiValueMap<>(); map.add("grant_type", "authorization_code"); map.add("client_id", googleApiProperties.getClientId()); map.add("client_secret", googleApiProperties.getClientSecret()); map.add("redirect_uri", googleApiProperties.getRedirectUri()); map.add("code", code); HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers); try { ResponseEntity<String> response = restTemplate.postForEntity( googleApiProperties.getTokenUri(), request, String.class); return objectMapper.readTree(response.getBody()) .get(googleApiProperties.getAccessTokenName()).asText(); } catch (Exception e) { throw new ServiceException(ExceptionStatus.OAUTH_BAD_GATEWAY); } } // ................ 변경한 코드 ................ public String getTokenByCode(String code, ApiProperties apiProperties) { return WebClient.create().post() .uri(apiProperties.getTokenUri()) .body(BodyInserters.fromFormData( ApiRequestManager.of(apiProperties) .getAccessTokenRequestBodyMap(code))) .retrieve() .bodyToMono(String.class) .map(response -> { try { return objectMapper.readTree(response) .get(apiProperties.getAccessTokenName()).asText(); } catch (JsonProcessingException e) { throw new RuntimeException(e); } }) .onErrorResume(e -> { throw new ServiceException(ExceptionStatus.OAUTH_BAD_GATEWAY); }) .block(); }
Java
복사
ObjectMapper를 스프링에서 기본적으로 주입하는 빈으로 서비스에서 사용하도록 변경했다.
AccessToken을 얻기 위한 map을 생성하는 과정을 ApiRequestManager에게 위임했다.
WebClient.create()를 통해서 WebClient 클래스를 생성, post()를 이용해 post 요청을 생성한다.
.uri()와 .body()를 이용해서 원하는 요청을 세팅한다.
.retrieve()를 이용해 ResponseEntity<T>를 반환받고, bodyToMono()를 이용해 원하는 클래스로 Mono로 변환한다.
받은 response에 대해서 JsonNode로 파싱하고, access token을 받아 오는 것을 try한다. 이 때 일어나는 에러는 Json 변환과정에서 일어나므로 Exception을 걸어둔다.
이외에 일어난 익셉션의 경우는 외부 API로 요청시에 에러가 발생하는 것이므로 별도의 커스텀한 서비스 익셉션으로 처리, 핸들링한다.
.block()은 해당 요청이 동기적으로 종료될 때 까지 기다리므로 동기적으로 사용하고 있는 것이다. 물론 비동기적으로 사용할 수는 있겠으나, 이후에 필요한 경우에 변경해도 충분할 것 같다.

정리

RestTemplate에 비해 뭔가 길어진 것 같으면서도 자세히보면 그 과정과 흐름이 명시되어 있고, 여러 과정들을 원하는 대로 추가, 제거할 수 있어서 잘 알기만 한다면 편하게 사용할 수 있을 것 같다.

참고자료