
기존에는 Spring에서 외부로 HTTP 요청을 보내기 위해서 RestTemplate라는 내장 클래스를 주로 사용을 하였다. RestTemplate는 다음과 같이 사용한다.
import org.springframework.web.client.RestTemplate
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
fun createPost(): Map<String, Any>? {
val restTemplate = RestTemplate()
val url = "https://jsonplaceholder.typicode.com/posts"
// 요청 헤더
val headers = HttpHeaders()
headers.contentType = MediaType.APPLICATION_JSON
// 요청 바디 데이터
val requestBody = mapOf(
"title" to "New Post",
"body" to "This is the content of the new post.",
"userId" to 1
)
// 요청 엔티티
val requestEntity = HttpEntity(requestBody, headers)
// POST 요청 수행
val response = restTemplate.postForObject(url, requestEntity, Map::class.java)
return response
}
fun main() {
val response = createPost()
println("Response: $response")
}
그런데 이 RestTemplate는 Sprign 5.0부터 maintenance(유지보수) mode로 들어가게 되었고 Spirng 6.1(Spring Boot 3.2)부터 RestClient라는 새로운 내장 툴이 등장하여 사용이 권장되었다. RestClient의 가장 큰 차이점은 비동기 지원 + 메소드 체이닝 스타일이다. RestClient는 다음과 같이 사용한다.
import org.springframework.web.client.RestClient
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
fun createPost(): Map<String, Any> {
val restClient = RestClient.create()
val url = "https://jsonplaceholder.typicode.com/posts"
// 요청 바디
val requestBody = mapOf(
"title" to "New Post",
"body" to "This is the content of the new post.",
"userId" to 1
)
// POST 요청 수행
val response = restClient.post()
.uri(url)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.body(requestBody)
.retrieve()
.body(Map::class.java)
return response
}
fun main() {
val response = createPost()
println("Response: $response")
}
메소드 체이닝을 통해 훨씬 가독성 좋은 코드를 짤 수 있다.
WebClient는 Spring WebFlux 기반의 HTTP Client로 RestTempalte가 maintenance mode로 들어간 시점부터 RestClient가 등장하기 전까지 사용이 권장되었다.
Spring WebFlux란?우리가 일반적으로 Spring이라 부르며 사용하는 Spring MVC는
Multi-Thread와Blocking방식으로 동작한다. 그래서 Spring에서Single-Thread와Non-Blocking방식으로 동작하며 Reactive 스타일의 개발을 가능하게 해주는 모듈이 존재하는데 그게 바로 Spring WebFlux이다.
WebClient는 다음과 같이 사용한다.
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.WebClientResponseException
fun main() {
// WebClient 빌더 생성
val webClient = WebClient.builder()
.baseUrl("https://jsonplaceholder.typicode.com")
.build()
try {
// 동기 방식으로 GET 요청
val response = webClient.get()
.uri("/posts/1")
.retrieve()
.bodyToMono(String::class.java) // 응답 본문을 String으로 처리
.block() // 동기 블로킹
println("Response: $response")
} catch (ex: WebClientResponseException) {
println("Error: ${ex.statusCode}, ${ex.responseBodyAsString}")
} catch (ex: Exception) {
println("Unexpected error: ${ex.message}")
}
}
RestTemplate, RestClient, WeblClient의 주요한 차이점은 다음과 같다.
| RestTemplate | WebClient | RestClient |
|---|---|---|
| Spring 3.0 | Spring WebFlux | Spring 6.1 |
| 동기 | 동기 or 비동기 | 동기 or 비동기 |
| 메소드 호출 | 메소드 체이닝 | 메소드 체이닝 |
WebClient와 RestClient와 무슨 차이냐고 물어볼 수 있는데 가장 큰 차이점은 Spring MVC에 내장되어있냐 아니냐의 차이이다. Spring MVC에서 개발을 진행하는데 단순히 HTTP 호출을 위해 굳이 Spring WebFlux라는 모듈을 설치해야 하는가?는 누군가에겐 제법 중대사항일 수 있다. 그럼에도 불구하고 내가 WebClient를 선택한 이유는 다음과 같다.
RestClient에서 비동기로 코드를 처리하는데는 두 가지의 방법이 있다. 첫 번째는 CompletableFuture를 이용한 방법이다.
import org.springframework.web.client.RestClient
fun fetchDataAsync(): CompletableFuture<String> {
val restClient = RestClient.create()
return restClient.get()
.uri("https://api.example.com/data")
.retrieve()
.toEntity(String::class.java)
.toFuture() // 결과를 CompletableFuture로 변환
}
fetchDataAsync().thenAccept { response ->
println("Response: $response")
}.exceptionally { throwable ->
println("Error occurred: ${throwable.message}")
null
}
두 번째는 Mono를 사용하는 이용하는 방법이다.
import org.springframework.web.client.RestClient
import reactor.core.publisher.Mono
fun fetchDataAsMono(): Mono<String> {
val restClient = RestClient.create()
return restClient.get()
.uri("https://api.example.com/data")
.retrieve()
.bodyToMono(String::class.java) // Mono로 결과 변환
}
fetchDataAsMono()
.subscribe(
{ response -> println("Response: $response") },
{ error -> println("Error occurred: ${error.message}") }
)
여기서 주의해야할 점은 Mono는 WebFlux의 객체라는 것이다. 그리고 RestClient로 간단한 비동기 처리에는 CompletableFuture로도 충분하지만 복잡한 비동기 처리를 위해서는 아이러니하게도 이 Mono를 쓰는게 더 유용하다는 점이다.
WebClient 대신 RestClient를 선택하는 이유에는 WebFulx의 Mono와 Flux등의 사용법과 Reactive 방식에 대한 추가적인 이해와 학습이 필요하다는 점이 크다. 그런데 어차피 Mono를 쓸거라면 굳이 RestClient를 쓸 필요성이 있을까? 라는 생각이 들었다. 어차피 WebFlux 의존성 추가도 해야한다. 게다가 WebFlux가 무거운 의존성이라 빌드 타임에 큰 영향을 끼치는 것 또한 아니다.
또한 Mono의 사용법 또한 익혀두면 좋을 것 같다는 생각 또한 들었다. 언젠가 Reactive 서버 개발이 필요할 수도 있고 Spring WebFlux를 사용할 수도 있다. 물론 추가 학습에 대한 코스트가 들기는 하지만 어차피 학습을 위한 프로젝트를 하고 있는 지금 그건 기피의 이유가 되지 못했다.
ResTemplate는 Spirng 6.1(Spring Boot 3.2) 이전 버전을 사용하는게 아닌 이상 선택할 이유가 없다. 하지만 RestClient가 없는 버전이고 없고 비동기 호출이 필요 없다면 ResTemplate를 사용하는 것도 괜찮을 것이다.
만약에 Spirng 6.1(Spring Boot 3.2) 이후 버전을 사용하며, 동기로만 호출을 할 것이거나 간단한 비동기 호출만이 필요하다면 RestClient을 사용하는게 좋은 선택일 것이다. WebFlux 의존성을 추가할 필요도 없고, Mono나 Flux의 사용법에 대한 추가적인 학습 또한 필요가 없다.
하지만 한 번에 여러 비동기 요청을 하거나 중첩된 비동기 요청이 필요하거나 등의 복잡한 비동기 처리가 필요하다면 Mono를 사용하는 것이 성능적인 면에서나 편의성면에서나 더 유리하다. 나는 사용해본 적이 없지만(?) 간단히 알아봐도 ExchangeFilterFunction이나 retryWhen(), zip() 등 유용해보이는 기능들이 많다.
하지만 그렇다고 RestClient가 무조건 구린 것은 아니다. RestClient에서 Mono를 안쓰고도 사실 앵간한 케이스들의 비동기 처리는 충분히 가능하고, 반드시 Mono나 Flux를 안쓰면 안 될 정도로 대규모 비동기 처리가 필요한 서비스라면 Blocking 기반인 Spring MVP를 쓸 게 아니라 Node.js나 Express, Spring WebFlux, Vert.x와 같은 Non-Blocking 프레임워크를 사용하는게 올바른 선택일 것이다!
그리고 무엇보다 Mono는 넘나 어렵다... 사실 개발하던 부분이 그렇게 많은 비동기 호출이 발생하는 작업이 아니었기에 RestClient나 막말로 전부 동기로 작업해도 문제 없을 상황이었지만 API 호출부터 Scraping, DB 호출까지 모두 비동기로 처리해버리겠어! 라는 개인적인 욕심에 WebClient를 선택하게 되었다. 물론 그 덕에 개발 시간이 한참 늘어났지만 하하😂