소셜로그인을 학습할 때, 카카오서버에서 전달해주는 유저의 정보를 받아와 유저를 로그인시키기 위한 목적으로 잠깐 Rest Template을 다룬 적이 있습니다.
>> 소셜로그인
Rest Template과 Web Client를 사용하면 브라우저 없이도 API 요청을 처리할 수 있기 때문에, API 안에서 또 다른 API를 호출하기 위해 주로 사용합니다. 이번 포스팅에서는 두 가지 HTTP 클라이언트 라이브러리에 대해 배워보기로 하겠습니다.
① 프로세스
② 쓰레드
① 블로킹
② 논 블로킹
① 싱글 쓰레드 + 블로킹
② 싱글 쓰레드 + 논 블로킹
※ 멀티 쓰레드 + 논 블로킹
논 블로킹을 멀티 쓰레드에서 사용하지 않는다고 말하는 이유는, 멀티 쓰레드 방식의 프로그래밍이 매우 복잡하고 불안정하기 때문이다. 따라서, 멀티 쓰레드에 논 블로킹 방식을 적용하는 경우는 거의 없다. 또한 프로세스를 여러 개 두는 멀티 프로세싱 방식으로 대체할 수 있기 때문에 더더욱 멀티 쓰레드 + 논 블로킹 방식을 사용할 이유가 없다. 물론 최근 들어 멀티 스레드와 논 블로킹 방식을 조합하는 경우가 늘었다고는 하나, 우리의 수준을 벗어난 이야기이다.
③ 멀티 쓰레드 + 블로킹
스프링 3.0 버전 부터 사용된 HTTP Client로 REST API 호출을 위한 함수를 제공하는 클래스이다. Rest Template의 특징은 아래와 같다.
기본 스프링 부트 의존성을 추가하면 Rest Template 관련 의존성은 자동으로 추가된다. 따라서 따로 추가해주어야 할 일은 거의 없을 것이다.
implementation 'org.springframework.boot:spring-boot-starter-web'
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"); // 따옴표가 있으면 고정값
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);
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 메서드, 요청객체, 반환타입 입력
③ 반환 값 저장
지금까지의 내용을 코드로 보면 아래와 같이 될 것이다.
물론, 실제 코드를 위와 같이 작성하면 안 된다. 쿼리 파라미터와 요청바디, 요청 헤더를 모두 사용하는 API를 작성할 일은 없기 때문이다. 위의 코드는 그냥 설명을 위한 예시로 쓰인 것이고 실제로 동작하지는 않는다. 본인이 설계한 API에 맞게 수정할 때 참고할 수 있는, 일종의 템플릿을 제공해준 것이다. 실제 프로젝트에서 사용한 Rest Template 코드는 아래와 같다.
게시글 상세 조회 API에서 댓글 조회 API를 같이 부르기 위해 Rest Template을 사용하고 있다.
구글 서버로 부터 액세스 토큰을 전달받기 위해 Rest Template을 사용하고 있다.
카카오 서버로 부터 반환된 유저 정보를 가져오기 위해 Rest Template을 사용하고 있다.
Web Cleint는 스프링 5.0에서 추가된 인터페이스이다. Web Client가 더 최근에 추가된 기능인만큼, 되도록 Rest Template보다 Web Client를 사용할 것을 권장한다고 한다. 하지만 개인적으로 Web Client가 무조건 더 좋다고 볼 수만은 없다고 생각한다. 그 이유는 4번에서 설명하기로 한다. Web Client의 특징은 아래와 같다.
Web Client는 webflux 의존성을 추가해주어야 한다.
implementation 'org.springframework.boot:spring-boot-starter-webflux'
① 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();
}
② 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();
}
※ Reactor
Java와 Kotlin에서 비동기 및 반응형 프로그래밍을 지원하는 라이브러리이다. Reactor는 주로 리액티브 프로그래밍 구현에 사용되며, 비동기 데이터 스트림 처리에 유용하다. 높은 성능과 확장성 때문에 멀티 쓰레드 환경에서도 매우 효과적이다.
Reactor의 타입에는 Mono와 Flux가 있다. Mono는 0 또는 1개의 데이터를 비동기적으로 발생시키기 때문에 단일 값에 대한 처리에 유리하고, Flux는 0 또는 여러 개의 데이터를 비동기적으로 발생시키므로, 여러 데이터에 대한 처리에 유리하다.
① 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();
}
Rest Template과 Web Client의 가장 큰 차이점이자, Web Client의 사용이 권장되는 이유는 바로 Web Client가 Non-Blocking 방식을 지원하기 때문이다.
위 그림에서 Boot1은 Rest Template을 사용하고, Boot2는 Web Client를 사용하고 있다. 동시 사용자가 1000명일 때까지는 처리속도에 큰 차이를 보이지 않지만, 사용자가 늘어나면 늘어날수록, Rest Template을 사용하는 Boot1의 속도가 기하급수적으로 느려지는 것을 볼 수 있다.
즉, 동시 사용자의 규모가 크지 않은 서비스라면, Rest Template을 사용하는 것이 큰 문제가 되지 않지만, 규모가 있는 서비스를 개발할 생각이라면 Web Client를 사용한 편이 훨씬 유리하다.
다만, 위의 결과는 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으로 작성하는 것이 훨씬 좋은 선택일 수 있다.
[이미지 출처]