Rest Template & Web Client

변현섭·2023년 7월 21일
0

Spring 잡학사전

목록 보기
6/10

소셜로그인을 학습할 때, 카카오서버에서 전달해주는 유저의 정보를 받아와 유저를 로그인시키기 위한 목적으로 잠깐 Rest Template을 다룬 적이 있습니다.
>> 소셜로그인
Rest Template과 Web Client를 사용하면 브라우저 없이도 API 요청을 처리할 수 있기 때문에, API 안에서 또 다른 API를 호출하기 위해 주로 사용합니다. 이번 포스팅에서는 두 가지 HTTP 클라이언트 라이브러리에 대해 배워보기로 하겠습니다.

1. 사전지식

1) Process & Thread

① 프로세스

  • 운영체제에서 자원을 할당받은 작업의 단위로, 쉽게 말해 컴퓨터에서 실행 중인 프로그램이다.
  • 프로세스는 다른 프로세스와 자원을 공유하지 않는다.

② 쓰레드

  • 프로세스 내 실행 흐름의 단위로, 프로세스 내에서 실제로 작업을 수행하는 주체이다.
  • 모든 프로세스에는 적어도 하나 이상의 쓰레드가 존재해야 한다.
  • 같은 프로세스 내의 쓰레드는 서로 자원을 공유하기 때문에 두 개 이상의 쓰레드를 갖는 프로세스도 있는데, 이를 멀티쓰레드 프로세스라 한다.

2) Blocking & Non-Blocking

① 블로킹

  • 이전 작업이 완료되어야만 다음 작업을 수행할 수 있음을 의미한다.
  • 우리가 흔히 "동기"라고 부르는 방식이 바로 블로킹이다.

② 논 블로킹

  • 이전 작업의 완료여부에 무관하게 다음 작업을 수행할 수 있음을 의미한다.
  • 우리가 흔히 "비동기"라고 부르는 방식이 바로 논 블로킹이다.

3) 쓰레드-블로킹의 종류

① 싱글 쓰레드 + 블로킹

  • 한명의 바리스타가 한 번에 한 명의 손님의 주문만 받고, 주문이 완료될 때까지 다음 주문을 받지 않는 방식과 같다.
  • 손님은 다른 주문이 처리되기까지 주문도 못하고 기다려야하기 때문에 자원활용이 매우 비효율적이다.

② 싱글 쓰레드 + 논 블로킹

  • 매우 능력 좋은 바리스타가 커피를 만드는 동안에도 계속 주문을 받을 수 있는 방식과 같다.
  • 손님은 커피가 나올 때까지 자신의 볼 일을 자유롭게 볼 수 있게 된다.
  • 여러 개의 작업을 동시에 처리하기 때문에 효율적이지만, 요청이 급격하게 늘어나는 경우 서버 부하를 초래할 수 있다.
  • 논 블로킹 방식은 싱글 쓰레드에서만 사용된다.

    ※ 멀티 쓰레드 + 논 블로킹
    논 블로킹을 멀티 쓰레드에서 사용하지 않는다고 말하는 이유는, 멀티 쓰레드 방식의 프로그래밍이 매우 복잡하고 불안정하기 때문이다. 따라서, 멀티 쓰레드에 논 블로킹 방식을 적용하는 경우는 거의 없다. 또한 프로세스를 여러 개 두는 멀티 프로세싱 방식으로 대체할 수 있기 때문에 더더욱 멀티 쓰레드 + 논 블로킹 방식을 사용할 이유가 없다. 물론 최근 들어 멀티 스레드와 논 블로킹 방식을 조합하는 경우가 늘었다고는 하나, 우리의 수준을 벗어난 이야기이다.

③ 멀티 쓰레드 + 블로킹

  • 여러 명의 바리스타가 각각 한 명의 손님의 주문을 받고, 주문을 처리할 때까지 다음 주문을 받지 않는다.
  • 싱글 쓰레드에 비해 커피가 나오는 속도는 빠르지만, 다른 주문이 처리되기까지 주문도 못하고 기다려야하는 것은 마찬가지이다.

2. Rest Template

1) 개념

스프링 3.0 버전 부터 사용된 HTTP Client로 REST API 호출을 위한 함수를 제공하는 클래스이다. Rest Template의 특징은 아래와 같다.

  • HTTP 프로토콜의 메서드인 Get, Post, Delete, Put을 사용할 수 있다.
  • Patch 메서드는 지원하지 않는다.
  • 단순한 통신방식을 지향하고, RESTful 원칙을 따른다.
  • Multi-Thread 방식과 Blocking 방식을 사용한다.

2) 의존성 설정

기본 스프링 부트 의존성을 추가하면 Rest Template 관련 의존성은 자동으로 추가된다. 따라서 따로 추가해주어야 할 일은 거의 없을 것이다.

implementation 'org.springframework.boot:spring-boot-starter-web'

3) Rest Template 사용법

Rest Template에서 지원하는 메서드는 아래와 같다.

이 많은 메서드를 다 알면 좋지만, 개인적인 생각으로는 exchange 메서드의 사용법만 알아도 충분한 것 같다. exchange 메서드를 사용하여 브라우저 없이 API 요청을 보내는 방법을 알아보자.

① HTTP 요청 객체 생성

  • 요청헤더에 값 입력하기
HttpHeaders httpHeaders = new HttpHeaders(); // 요청 헤더 생성
httpHeaders.add("Authorization", "Bearer " + accessToken); // Authorization 필드 추가
  • 요청바디에 값 입력하기
// Httpheader와 HttpBody를 하나의 객체에 담기
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, httpHeaders);

MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("client_id", "AetzjOnyYuxithVHLRfr97pTGAjVdHXf"); // 따옴표가 있으면 고정값
  • Query Parameter에 값 입력하기
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("nickName", nickName); // key-value 쌍, 따옴표가 없으면 전달받은 값(동적)
String url = "http://localhost:8080/update";
// 쿼리 파라미터가 추가된 URL 생성
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url).queryParams(queryParams);
  • Path Variable에 값 입력하기
ResponseEntity<List<GetCommentRes>> responseEntity = restTemplate.exchange(
                "http://localhost:8080/boards/{boardId}/comments/{userId}",  
                HttpMethod.GET,  // 요청 방법 (GET, POST 등)
                null,  // 요청 객체
                new ParameterizedTypeReference<List<GetCommentRes>>() {},
                boardId,  // URL 경로 변수에 대한 값 (필요에 따라 설정)
                userId

② 요청 URL, HTTP 메서드, 요청객체, 반환타입 입력

  • exchange 메서드의 입력 파라미터는 URL, HTTP 메서드, HTTP 요청 객체, 반환타입을 순서대로 입력해야한다.
  • 위와 같이 경로 변수가 있는 경우에는 경로 변수도 뒤이어서 입력해야 한다.
  • 반환타입의 경우, 단일 객체나 DTO를 반환할 때에는 User.class, String.class와 같이 쓸 수 있지만, List를 반환할 때에는 ParameterizedTypeReference<>의 제네릭 타입을 사용해야 한다.

③ 반환 값 저장

  • ResponseEntity<List<GetCommentRes>>와 같이 ResponseEntity<>의 제네릭 타입으로 반환 값을 받을 수 있다.

지금까지의 내용을 코드로 보면 아래와 같이 될 것이다.

물론, 실제 코드를 위와 같이 작성하면 안 된다. 쿼리 파라미터와 요청바디, 요청 헤더를 모두 사용하는 API를 작성할 일은 없기 때문이다. 위의 코드는 그냥 설명을 위한 예시로 쓰인 것이고 실제로 동작하지는 않는다. 본인이 설계한 API에 맞게 수정할 때 참고할 수 있는, 일종의 템플릿을 제공해준 것이다. 실제 프로젝트에서 사용한 Rest Template 코드는 아래와 같다.


게시글 상세 조회 API에서 댓글 조회 API를 같이 부르기 위해 Rest Template을 사용하고 있다.

구글 서버로 부터 액세스 토큰을 전달받기 위해 Rest Template을 사용하고 있다.

카카오 서버로 부터 반환된 유저 정보를 가져오기 위해 Rest Template을 사용하고 있다.

3. Web Client

1) 개념

Web Cleint는 스프링 5.0에서 추가된 인터페이스이다. Web Client가 더 최근에 추가된 기능인만큼, 되도록 Rest Template보다 Web Client를 사용할 것을 권장한다고 한다. 하지만 개인적으로 Web Client가 무조건 더 좋다고 볼 수만은 없다고 생각한다. 그 이유는 4번에서 설명하기로 한다. Web Client의 특징은 아래와 같다.

  • Single-Thread 방식과 Non-Blocking 방식을 사용한다.
  • Json과 XML 타입의 응답을 받는 데에 최적화되어 있다.

2) 의존성 설정

Web Client는 webflux 의존성을 추가해주어야 한다.

implementation 'org.springframework.boot:spring-boot-starter-webflux'

3) Web Client 생성하기

① create를 이용해 생성하기

public String testMethod(){
      WebClient webClient = WebClient.create("http://localhost:8080");

      ResponseEntity<String> responseEntity = webClient.get()
              .uri(uriBuilder -> uriBuilder.path("/update/{nickName}")
                      .build("Chrome"))
              .retrieve().toEntity(String.class).block();

      return responseEntity.getBody();
  }
  • http://localhost:8080 은 Base Url을 의미한다.
  • get()은 HTTP Get 메서드 요청임을 나타낸다.
  • uri()는 엔드포인트를 입력하는 곳으로, uriBuilder를 사용하면 동적으로 URI를 구성할 수 있게 된다. 현재 path variable인 nickName에 Chrome이 입력된다.
  • 요청에 대한 응답 객체의 바디는 retrieve()로 가져올 수 있다. 이 때, 응답을 String 타입으로 받기 위해 toEntity(String.class)를 사용하였다.
  • block은 동기적으로 응답을 기다리라는 의미이다. Web Client는 기본적으로 논 블로킹 방식으로 동작하기 때문에 블로킹 방식으로 변경하기 위해 사용되었다. (block을 사용하는 이유 역시 아래의 4번에서 자세히 설명하겠다.)

② builder를 이용해 생성하기

public String testMethod() {
      WebClient webClient = WebClient.builder()
              .baseUrl("http://localhost:8080")
              .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
              .build();

      return webClient.get()
              .uri("/users")
              .retrieve()
              .bodyToMono(String.class)
              .block();
  }
  • defaultHeader()는 WebClient의 기본 헤더를 설정한다.
  • Json 형식의 데이터를 송수신할 때에는 요청 헤더의 Content-Type 필드 값을 application/json으로 설정해주어야 하므로, HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE을 넣어주었다.
  • bodyToMono(String.class)는 응답 객체의 바디를 Mono로 변환한다. 여기서 Mono는 비동기적 결과를 처리하는 Reactor의 일종이다.

    ※ Reactor
    Java와 Kotlin에서 비동기 및 반응형 프로그래밍을 지원하는 라이브러리이다. Reactor는 주로 리액티브 프로그래밍 구현에 사용되며, 비동기 데이터 스트림 처리에 유용하다. 높은 성능과 확장성 때문에 멀티 쓰레드 환경에서도 매우 효과적이다.
    Reactor의 타입에는 Mono와 Flux가 있다. Mono는 0 또는 1개의 데이터를 비동기적으로 발생시키기 때문에 단일 값에 대한 처리에 유리하고, Flux는 0 또는 여러 개의 데이터를 비동기적으로 발생시키므로, 여러 데이터에 대한 처리에 유리하다.

4) Query Parameter와 Post 요청 다루기

① Query Parameter 사용하기

public String testMethod(){
        WebClient webClient = WebClient.create("http://localhost:8080");
        
        return webClient.get().uri(uriBuilder -> uriBuilder.path("/read")
                .queryParam("nickName", "Chrome")
                .build())
                .exchangeToMono(clientResponse -> {
                    if(clientResponse.statusCode().equals(HttpStatus.OK)){
                        return clientResponse.bodyToMono(String.class);
                    } else{
                        return clientResponse.createException().flatMap(Mono::error);
                    }
                })
                .block();
    }

② Post 요청 사용하기

public ResponseEntity<PostMemberRes> testMethod(){
      WebClient webClient = WebClient.builder()
              .baseUrl("http://localhost:8080")
              .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
              .build();

      PostBoardRes postBoardRes = new PostBoardRes();
      postBoardRes.setTitle("Hello everyone!");
      postBoardRes.setContent("My name is Chrome.");
		String accessToken = jwtService.getJwt();

      return webClient
              .post()
              .uri(uriBuilder -> uriBuilder.path("/board")
                      .build())
              .bodyValue(poostBoardRes)
              .header("Authorization", accessToken)
              .retrieve()
              .toEntity(PostBoardRes.class)
              .block();
  }

4. Rest Template과 Web Client의 차이

1) RestTemplate과 Web Client의 성능 비교

Rest Template과 Web Client의 가장 큰 차이점이자, Web Client의 사용이 권장되는 이유는 바로 Web Client가 Non-Blocking 방식을 지원하기 때문이다.


위 그림에서 Boot1은 Rest Template을 사용하고, Boot2는 Web Client를 사용하고 있다. 동시 사용자가 1000명일 때까지는 처리속도에 큰 차이를 보이지 않지만, 사용자가 늘어나면 늘어날수록, Rest Template을 사용하는 Boot1의 속도가 기하급수적으로 느려지는 것을 볼 수 있다.

즉, 동시 사용자의 규모가 크지 않은 서비스라면, Rest Template을 사용하는 것이 큰 문제가 되지 않지만, 규모가 있는 서비스를 개발할 생각이라면 Web Client를 사용한 편이 훨씬 유리하다.

2) Non-Blocking 처리

다만, 위의 결과는 Web Client를 비동기방식으로 사용한 경우를 가정하고 있다. 사실 Non-Blocking 처리는 그렇게 간단하지 않다. 코드가 최적화되어 있다는 전제 하에 프로그래밍에서 성능은 코드 길이에 비례한다. 그만큼 코드 작성이 어려워진다는 의미이다. 아래의 예시는 Web Client를 이용해 공공데이터포털의 API를 이용하고 있는 코드이다.

그리고 아래는 이 메서드에 대한 실행 코드이다.

이 코드는 Web Client를 블로킹으로 처리하고 있는데, 만약 논 블로킹 방식으로 변경하면 어떻게 될까?


gpt를 돌려 변환한 코드이다보니 곳곳에 에러가 나고 있기는 하지만, 중요한 것은 코드의 길이가 매우 길어지면서 가독성 또한 떨어지게 된다는 것이다. Rest Template이 구식 방식임에도 불구하고, 가독성은 훨씬 좋고 사용하기도 쉽기 때문에 Rest Template을 사용하게 되는 경우가 많은 것 같다.

만약, Web Client가 Rest Template보다 무조건 좋다고 생각해서 무턱대고 Web Client를 사용한 후, 블로킹 방식으로 처리하면 오히려 Rest Template보다 성능이 안 좋다. 즉, 코드는 코드대로 복잡해지고, 성능은 오히려 더 떨어져버린다는 것이다. 따라서 Web Client를 동기 방식으로 사용할 생각이라면 Rest Template을 쓰는 편이 좋다.

본인의 역량에 맞는 선택을 하는 것이 중요하다. 대규모 서비스를 런칭할 생각이라면, 코드 작성이 어려워도 성능 상의 이점이 있는 Web Client를 사용하는 편이 좋고, 그런게 아니라면 Rest Template으로 작성하는 것이 훨씬 좋은 선택일 수 있다.

[이미지 출처]

profile
Java Spring, Android Kotlin, Node.js, ML/DL 개발을 공부하는 인하대학교 정보통신공학과 학생입니다.

0개의 댓글