์ง๊ธ๊น์ง ๋ํ์ ์ผ๋ก ์ฌ์ฉํ๋ HTTP ํด๋ผ์ด์ธํธ ๋ชจ๋์ WebClient์ RestTemplate๊ฐ ๋ํ์ ์ผ๋ก ์ฌ์ฉ๋์๋ค. ํ์ง๋ง RestTemplate์ ์ด์ maintenance ๋ชจ๋๋ก ๋ณ๊ฒฝ๋์๊ณ (deprecated๋ ์๋๋ค) WebClient๋ ๊ฐ๋ ฅํ์ง๋ง ๋จ์ํ ๋๊ธฐ ํธ์ถ์๋ ์ข ๊ณผํ ๋ฉด์ด ์์๋ค.
์ด๋ฐ ๋ฌธ์ ๋ฅผ ์กฐ๊ธ ๋ ๊ฐ์ ํด ์ค ์ ์๋ HTTP ํด๋ผ์ด์ธํธ ๋ชจ๋์ด Spring Framework 6.1 (Spring Boot 3.2) ๋ถํฐ ์๊ฐ๋ RestClient๋ค. RestTemplate์ ์ง๊ด์ ์ธ API ๋์์ธ๊ณผ WebClient์ ๊ฐ์ด fluent API๋ฅผ ๊ฒฐํฉํ์ฌ ๋๊ธฐ ๋ฐฉ์์ HTTP ํต์ ์ ์ง์ํ๋ Spring Boot RestClient์ ๋ํด์ ์ ๋ฆฌํ๊ณ ์ ํ๋ค.
RestClient๋ฅผ ์ํ ๋ํ๋์
RestClient๋ spring boot 3.2๋ถํฐ ์ฌ์ฉ ๊ฐ๋ฅํ๋ฉฐ spring-boot-starter-web์ ํฌํจ๋์ด ์๋ค.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}JavaHttp Message Conversion
spring-web ๋ชจ๋์๋ InputStream ๋ฐ OutputStream์ ํตํด HTTP ์์ฒญ ๋ฐ ์๋ต ๋ณธ๋ฌธ์ ์ฝ๊ณ ์ฐ๋ HttpMessageConverter ์ธํฐํ์ด์ค๊ฐ ํฌํจ๋์ด ์๋ค.
HttpMessageConverter๋ RestTemplate, RestClient, WebClient, @RequestBody, @ResponseBody์ ๊ฐ์ด body์ ๋ํ ์ง๋ ฌํ, ์ญ์ง๋ ฌํ๊ฐ ํ์ํ ๊ณณ์์ ์ฌ์ฉ๋๋ค.
๋ค์ ๋งํฌ๋ Spring Boot Auto Configuration์ ์ํด์ ์๋์ผ๋ก ์์ฑ๋๋ HttpMessageConverter ๋น ์ธ์คํด์ค์ ๋ํ ์ค๋ช
์ ํ๊ณ ์๋ค.
RestClient์ ๋ํ ๋ด์ฉ์ ์์ฑํ๊ธฐ์ ์์ ํด๋น ๋งํฌ๋ฅผ ์ฐ์ ์ฐธ๊ณ ํ๋ฉด ๋์์ด ๋ ๊ฒ ๊ฐ๋ค.
https://docs.spring.io/spring-framework/reference/web/webmvc/message-converters.html#message-converters
RestClient์ HttpClient
RestClient๋ Spring์์ ์ ๊ณตํ๋ ๊ณ ์์ค์ REST API ํด๋ผ์ด์ธํธ๋ค.
์ด๋ ์ค์ ์ ์ก ๊ณ์ธต์์ ์ ์ก์ ๋ด๋นํ๋ HttpClient๋ฅผ ํธ์ถํ๊ธฐ ์ํ ์ถ์ํ ๊ณ์ธต์ด๋ผ๊ณ ํ ์ ์๋ค.
RestClient๋ฅผ ํตํด์ ์๋ฒ์ ํต์ ์ ํ ๋ ์ค์ ๋ก ๋ด๋ถ์์๋ ๋ค์๊ณผ ๊ฐ์ ์ ์ฐจ๋ฅผ ๊ฑฐ์น๋ค.
๋ฌผ๋ก RestTemplate, WebClient ์ญ์ ๊ณ ์์ค์ ์ถ์ํ ๊ณ์ธต์ด๊ณ ๋ด๋ถ์ ์ผ๋ก๋ HttpClient ๊ตฌํ์ฒด๊ฐ ์ ์ก ๊ณ์ธต์ ๋ด๋นํ๋๋ก ์ค๊ณ๋์ด ์๋ค.
RestClient -> RestClientAdapter -> ClientHttpRequestFactory -> HttpClient ๊ตฌํ์ฒดJava์ฆ RestClient๋ HTTP ํด๋ผ์ด์ธํธ(HttpClient)๋ฅผ ์ง์ ์์ฑํ๊ฑฐ๋ ์์ ํ์ง ์๋๋ค. ๋์ ์ Spring์ ์ถ์ํ์ธ ClientHttpRequestFactory๋ฅผ ํตํด์ ์ค์ ์ ์ก ๊ณ์ธต์ ์์ํ๋ค.
์ ํ๋ฆฌ์ผ์ด์ classpath์ ์ฌ์ฉ ๊ฐ๋ฅํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋ฐ๋ผ์ ํจ๊ป ์ฌ์ฉํ HTTP ํด๋ผ์ด์ธํธ๋ฅผ ์๋์ผ๋ก ๊ฐ์งํ๋๋ฐ ๋ค์๊ณผ ๊ฐ์ ์ฐ์ ์์๋ฅผ ๊ฐ์ง๋ค.
- Apache HttpClient
- Jetty HttpClient
- Reactor Netty HttpClient
- JDK client (java.net.http.HttpClient)
- Simple JDK client (java.net.HttpURLConnection)
ํด๋์คํจ์ค์ ์ฌ๋ฌ ํด๋ผ์ด์ธํธ๊ฐ ์กด์ฌํ๊ณ ์ ์ญ ๊ตฌ์ฑ์ด ์ ๊ณต๋์ง ์์ ๊ฒฝ์ฐ ๊ฐ์ฅ ์ฐ์ ์์๊ฐ ๋์ ํด๋ผ์ด์ธํธ๊ฐ ์ฌ์ฉ๋๋ค.
์ ์ญ HttpClient ์ค์
์๋ ๊ฐ์ง๋ HTTP ํด๋ผ์ด์ธํธ๊ฐ ์๊ตฌ ์ฌํญ์ ์ถฉ์กฑํ์ง ์์ ๊ฒฝ์ฐ spring.http.client.factory ์์ฑ์ ์ฌ์ฉํ์ฌ ํน์ ํฉํ ๋ฆฌ๋ฅผ ์ ํํ ์ ์๋ค. ์๋ฅผ ๋ค์ด, classpath์ Apache HttpClient๊ฐ ์์ง๋ง Jetty์ HttpClient๋ฅผ ์ ํธํ๋ ๊ฒฝ์ฐ ๋ค์์ ์ถ๊ฐํ ์ ์๋ค.
spring.http.client.factory=jettyPlaintext๋ชจ๋ ํด๋ผ์ด์ธํธ์ ์ ์ฉ๋ ๊ธฐ๋ณธ๊ฐ์ ๋ณ๊ฒฝํ๊ธฐ ์ํด ์์ฑ์ ์ค์ ํ ์๋ ์๋ค.
์๋ฅผ ๋ค์ด ํ์์์ ์ค์ ์ด๋ ๋ฆฌ๋๋ ์
์ถ์ ์ฌ๋ถ๋ฅผ ๋ณ๊ฒฝํ ์ ์๋ค.
spring.http.client.connect-timeout=2s
spring.http.client.read-timeout=1s
spring.http.client.redirects=dont-followPlaintextSpring Boot RestClient ์ธ์คํด์ค ์์ฑ
RestClient๋ static create ๋ฉ์๋ ์ค์์ ํ๋๋ฅผ ์ฌ์ฉํ์ฌ ์์ฑํ๋ค. ๋ํ builder๋ฅผ ์ฌ์ฉํ์ฌ ์ถ๊ฐ ์ต์ ์ ์ง์ ํ ๋น๋๋ฅผ ์ป์ ์๋ ์๋ค. ์๋ฅผ ๋ค์ด ์ฌ์ฉํ HTTP ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ง์ , ์ฌ์ฉํ ๋ฉ์์ง ๋ณํ๊ธฐ ์ง์ , ๊ธฐ๋ณธ URI ์ค์ , ๊ธฐ๋ณธ ๊ฒฝ๋ก ๋ณ์ ์ค์ , ๊ธฐ๋ณธ ์์ฒญ ํค๋ ์ค์ ๋ฑ์ ์ธ์คํด์ค ์์ฑ ๊ณผ์ ์์ ํ ์ ์๋ค. RestClient ์ธ์คํด์ค๋ thread safe ํ๋ค. RestClient๋ฅผ ๋น์ผ๋ก ์์ฑํ๊ณ ์์ฑ ์ ๊ธฐ๋ณธ์ ์ธ ์ ๋ณด๋ฅผ ์ธํ ํด ๋๋ฉด ์ข์ ๊ฒ ๊ฐ๋ค.
// static create๋ฅผ ์ด์ฉํ ์์ฑ.
RestClient defaultClient = RestClient.create();
// builder()๋ฅผ ์ด์ฉํ ์์ฑ
RestClient customClient = RestClient.builder()
.requestFactory(new HttpComponentsClientHttpRequestFactory())
.messageConverters(converters -> converters.add(new MyCustomMessageConverter()))
.baseUrl("https://example.com")
.defaultUriVariables(Map.of("variable", "foo"))
.defaultHeader("My-Header", "Foo")
.defaultCookie("My-Cookie", "Bar")
.requestInterceptor(myCustomInterceptor)
.requestInitializer(myCustomInitializer)
.build();JavaWebClient ์์ฑ ๋ฐฉ์๊ณผ ์๋นํ ์ ์ฌํจ์ ์ ์ ์๋ค.
Spring Boot RestClient ๋น ์์ฑํ๊ธฐ
RestClient๋ ์ค๋ ๋ ์์ ํ๋ฏ๋ก ์ฑ๊ธํค์ผ๋ก ๋์ํ๋ ๋น์ ์์ฑํ์ฌ ํ์ํ ๊ณณ์ ์ฃผ์
ํ์ฌ ์ฌ์ฉํ ์ ์๋ค.
๋ค์์ RestClient ๋น์ ์์ฑํ๋ ๋ฐฉ๋ฒ์ ์๊ฐํ๋ค.
์ง์ ์์ฑ
๋ค์๊ณผ ๊ฐ์ด @Baen ๋ฉ์๋ ๋ด์์ ์ง์ RestClient ์ธ์คํด์ค๋ฅผ ์์ฑํ๋ ๋ฐฉ๋ฒ์ด๋ค.
์ด ๊ฒฝ์ฐ์๋ ์๋ ์ค๋ช
ํ๋ RestClientCustomizer์ ์ง์ ๋ ์ค์ ์ด ๋ฐ์๋์ง ์๋๋ค.
@Configuration
public class RestClientConfig {
@Bean
public RestClient myRestClient() {
return RestClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader("Accept", MediaType.APPLICATION_JSON_VALUE)
.defaultHeader("User-Agent", "MyApp/1.0")
.defaultHeader("Authorization", "Bearer " + token)
.build();
}
}Java์ง์ RestClient.builder()๋ฅผ ์ฌ์ฉํ์ฌ RestClient ๋น ์ธ์คํด์ค๋ฅผ ์์ฑํ๋ ๋ฐฉ์์ด๋ค.
customizer ์ฌ์ฉ
์์ฑ๋๋ ๋ชจ๋ RestClient ๋น์ ๊ณตํต ์ ์ฑ
(๋ก๊น
, ํ์์์, ์ปค๋ฅ์
๋ฑ)์ ์ ์ฉํ๋ ค๋ ๊ฒฝ์ฐ customizer๋ฅผ ์ฌ์ฉํ๋ค. customizer๋ฅผ ํตํด์ ์ค์ ๋ ํญ๋ชฉ์ RestClient.builder์ ๋ฐ์๋๋ค.
customizer๋ ๋ค์๊ณผ ๊ฐ์ ํ๋ฆ์ผ๋ก ์ ์ฉ๋๋ค.
RestClientCustomizer -> RestClient.Builder -> RestClient<br>Plaintext์ฃผ์ํด์ผ ํ ๋ถ๋ถ์ RestClientCustomizer๋ RestClient.Builder์ ์ ์ฉ๋๋ ๊ฒ์ด๋ค.
RestClient.Builder๋ Spring Boot Auto Configuration์ ์ํด์ ์๋ ์์ฑ๋์ง๋ง RestClient๋ ์๋ ์์ฑ๋์ง ์๋๋ค. RestClient.Builder ๋น์ ์ฃผ์
๋ฐ์ RestClient ๋น์ ์ง์ ์์ฑํด ์ค์ผ ํ๋ค.
๋ค์์ RestClient.Builder์ Authorization, User-Agent, Accept ํค๋๋ฅผ ๊ณตํต์ ์ผ๋ก ์ ์ฉํ๋ค.
@Configuration
public class RestClientCommonConfig {
@Bean
public RestClientCustomizer authHeaderCustomizer() {
return builder -> builder
.defaultHeader("Authorization", "Bearer my-shared-access-token")
.defaultHeader("User-Agent", "MyCompanyService/1.0")
.defaultHeader("Accept", "application/json");
}
// RestClientCustomizer๊ฐ ์ ์ฉ๋ builder๊ฐ ์ฃผ์
๋๋ค.
@Bean
public RestClient restClient( RestClient.Builder builder ) {
return builder.baseUrl("https://api.example.com").build();
}
}Javabuilder์์ ์ง์ ๋ defaultHeader๋ RestClient.Builder์ ๋ฐ์์ด ๋๊ณ RestClient.Builder๋ฅผ ํตํด์ ์์ฑ๋๋ ๋ชจ๋ RestClient์ ์ ์ฉ๋๋ค. ์ฆ RestClient.Builder ๋น์ ํตํด์ RestClient ๋น์ ์์ฑํ๋ ๊ฒฝ์ฐ RestClientCustomizer ์ค์ ์ด ์ ์ฉ๋๋ค.
๋ค์ ์ฝ๋๋ JDK11 ์ด์๋ถํฐ ์ ๊ณต๋๋ ๋ด์ฅ๋ HttpClient ์ธ์คํด์ค ๋น์ ์์ฑํ๊ณ ์ด ๋น์ JdkClientHttpRequestFactory์ ์ ์ฉ ํ RestClientCustomizer์ ์ ์ฉํ๋ค.
package org.example.spring.restclient.sample.configuration;
import org.springframework.boot.web.client.RestClientCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
import java.net.http.HttpClient;
import java.time.Duration;
@Configuration
public class RestClientConfiguration {
@Bean
public HttpClient httpClient() {
return HttpClient.newBuilder()
.connectTimeout( Duration.ofSeconds( 3 ) )
.followRedirects( HttpClient.Redirect.NORMAL )
.version( HttpClient.Version.HTTP_2 )
.build();
}
@Bean
public JdkClientHttpRequestFactory clientHttpRequestFactory( HttpClient httpClient ) {
JdkClientHttpRequestFactory jdkClientHttpRequestFactory = new JdkClientHttpRequestFactory( httpClient );
jdkClientHttpRequestFactory.setReadTimeout( Duration.ofSeconds( 5 ) );
return jdkClientHttpRequestFactory;
}
@Bean
public RestClientCustomizer restClientCustomizer( JdkClientHttpRequestFactory requestFactory ) {
return builder ->
builder.requestFactory( requestFactory )
.defaultHeader( "Authorization", "Bearer " + System.getenv( "BEARER_TOKEN" ) )
.defaultHeader( "User-Agent", System.getenv( "USER_AGENT" ) )
.defaultHeader( "Accept", "application/json" );
}
// ์ฌ์ฉ์ ์๋น์ค API ํด๋ผ์ด์ธํธ
@Bean
public RestClient userApiClient(RestClient.Builder builder) {
return builder
.baseUrl("https://api.example.com/users") // ์๋น์ค๋ณ Base URL ์ง์
.build();
}
// ์ฃผ๋ฌธ ์๋น์ค API ํด๋ผ์ด์ธํธ
@Bean
public RestClient orderApiClient(RestClient.Builder builder) {
return builder
.baseUrl("https://api.example.com/orders")
.build();
}
// ๊ฒฐ์ ์๋น์ค API ํด๋ผ์ด์ธํธ
@Bean
public RestClient paymentApiClient(RestClient.Builder builder) {
return builder
.baseUrl("https://api.example.com/payments")
.build();
}
}Java๋ฐ๋ณตํ์ฌ ์ค๋ช
ํ์ง๋ง RestClientCustomizer ๋น์ RestClient.Builder ๋น์ ์ ์ฉ๋๋ค.
๋ฐ๋ผ์ RestClient.Builder๋ฅผ ํตํด์ ์์ฑ๋๋ ๋ชจ๋ RestClient ๋น์๋ RestClientCustomizer๊ฐ ๊ณตํต์ ์ผ๋ก ์ ์ฉ๋๋ค.
์ ์ํ ์ฝ๋๋ HttpClient ์ค์ (ํ์์์, ๋ฒ์ ๋ฑ)์ RestClient.Builder ๋น์ ๊ณต์ ํ๋ค.
๋ณ๋์ RestClient๋น์๋ baseUrl๋ง ์ง์ ํ๋ฉด ๋ฐ๋ก ์ฌ์ฉ ๊ฐ๋ฅํ๋ค.
Spring Boot RestClient ์ฌ์ฉ
Path Variable ์ง์
RestClient๋ RestTemplate์ฒ๋ผ URI ํ ํ๋ฆฟ ๋ณ์๋ฅผ ์ง์ ์นํํ ์ ์๋ค.
User user = RestClient.create()
.get()
.uri("https://api.example.com/users/{id}", 12345) // ์์๋๋ก ์นํ
.retrieve()
.body(User.class);JavaMap์ ์ฌ์ฉํ์ฌ ์ด๋ฆ ๊ธฐ๋ฐ์ผ๋ก ์นํํ ์๋ ์๋ค.
Map<String, Object> uriVariables = Map.of("userId", 12345);
User user = RestClient.create()
.get()
.uri("https://api.example.com/users/{userId}", uriVariables)
.retrieve()
.body(User.class);JavaUriBuilder๋ฅผ ์ฌ์ฉํ์ฌ PathVariable์ ๊ตฌ์ฑํ ์ ์๋ค.
User user = RestClient.create()
.get()
.uri(uriBuilder -> uriBuilder
.path("/users/{id}")
.build(12345))
.retrieve()
.body(User.class);JavaQuery Parameter ์ง์
uri()์ ์ง์ ํ๋์ฝ๋ ๋ฐฉ์์ผ๋ก ์ง์ ํ ์ ์์ง๋ง ๊ถ์ฅํ์ง ์๋๋ค.
List<User> users = RestClient.create()
.get()
.uri("https://api.example.com/users?active=true&limit=10")
.retrieve()
.body(new ParameterizedTypeReference<List<User>>() {});JavaUriBuilder๋ฅผ ์ฌ์ฉํ์ฌ Query Parameter๋ฅผ ๊ตฌ์ฑํ ์ ์๋ค. (๊ถ์ฅ ๋ฐฉ์)
List<User> users = RestClient.create()
.get()
.uri(uriBuilder -> uriBuilder
.path("/users")
.queryParam("active", true)
.queryParam("limit", 10)
.queryParam("sort", "name")
.build())
.retrieve()
.body(new ParameterizedTypeReference<List<User>>() {});JavaUriBuilder๋ ๊ฐ์ ์ด๋ฆ์ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ๋ฌ ๋ฒ ์ง์ ํ ์ ์๋ค.
List<String> roles = List.of("ADMIN", "USER");
List<User> users = RestClient.create()
.get()
.uri(uriBuilder -> uriBuilder
.path("/users")
.queryParam("role", roles.toArray()) // role=ADMIN&role=USER
.build())
.retrieve()
.body(new ParameterizedTypeReference<List<User>>() {});JavaPathVariable + Query Parameter ํผํฉ
UriBuilder๋ฅผ ์ฌ์ฉํ์ฌ ํผํฉํ์ฌ ๊ตฌ์ฑํ ์ ์๋ค.
UserDetail detail = RestClient.create()
.get()
.uri(uriBuilder -> uriBuilder
.path("/users/{id}")
.queryParam("include", "profile")
.queryParam("include", "roles")
.build(12345))
.retrieve()
.body(UserDetail.class);JavaRequest headers and body
header(String, String), headers(Consumer, <HttpHeaders>)๋ฅผ ์ฌ์ฉํ๊ฑฐ๋ accept(MediaType), acceptCharset(Charset…)๊ณผ ๊ฐ์ ํธ์ ๋ฉ์๋๋ฅผ ํตํด ์์ฒญ ํค๋๋ฅผ ์ถ๊ฐํ ์ ์๋ค. ๋ณธ๋ฌธ์ ํฌํจํ ์ ์๋ HTTP ์์ฒญ (POST, PUT, PATCH)์ ๊ฒฝ์ฐ contentType(MediaType) ๋ฐ contentLength(long)๊ณผ ๊ฐ์ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ ์ ์๋ค.
์์ฒญ ๋ณธ๋ฌธ ์์ฒด๋ ๋ด๋ถ์ ์ผ๋ก HTTP ๋ฉ์์ง ๋ณํ์ ์ฌ์ฉํ๋ body(Object)๋ก ์ค์ ํ ์ ์๊ณ ๋๋ ParameterizedTypeReference๋ฅผ ์ฌ์ฉํ์ฌ ์์ฒญ ๋ณธ๋ฌธ์ ์ค์ ํ ์ ์์ผ๋ฉฐ ์ด๋ฅผ ํตํด ์ ๋๋ฆญ์ ํ์ฉํ ์ ์๋ค. ๋ง์ง๋ง์ผ๋ก ๋ณธ๋ฌธ์ OutputStream์ ๊ธฐ๋กํ๋ ์ฝ๋ฐฑ ํจ์๋ก ์ค์ ํ ์๋ ์๋ค.
ParameterizedTypeReference๋?
ParameterizedTypeReference๋ Spring Framework์์ ์ ๋ค๋ฆญ ํ์ ์ ๋ณด๋ฅผ ๋ฐํ์์ ๋ณด์กดํ๊ธฐ ์ํด ์ฌ์ฉํ๋ ํด๋์ค๋ค.
ํ์ ์๊ฑฐ
– Java์ ์ ๋ค๋ฆญ์ ์ปดํ์ผ ํ์์๋ง ์กด์ฌํ๊ณ ๋ฐํ์์๋ ํ์ ์ ๋ณด๊ฐ ์๊ฑฐ๋๋ค.
//์ปดํ์ผ ํ์
List<User> users = new ArrayList<User>();
//๋ฐํ์ – ํ์ ์ ๋ณด ์์ค (User ์ ๋ณด๊ฐ ์ฌ๋ผ์ง)
List<User> users = new ArrayList<>();๋ฌธ์ ๊ฐ ๋๋ ๊ฒฝ์ฐ
// List<User>.class๋ ๋ถ๊ฐ๋ฅ
List<User> users = restClient.get().uri(“/users”).retrieve().body(List<User>.class);
// ํ์ ์ ๋ณด ์์ค
List<User> users = restClient.get().uri(“/users”).retrieve().body(List.class) // List<Object>
๊ฐ์ฅ ํํ ์ผ์ด์ค๋ก POST ์์ฒญ ์ Content-Type, Authorization ๋ฑ์ ํค๋์ DTO ํํ์ JSON body๋ฅผ ํจ๊ป ์ ์กํ๋ ๋ฐฉ์์ด๋ค.
UserRequest requestBody = new UserRequest("test", "developer");
UserResponse response = restClient.post()
.uri("https://api.example.com/users")
.contentType(MediaType.APPLICATION_JSON) // Content-Type ์ค์
.accept(MediaType.APPLICATION_JSON) // Accept ์ค์
.header("Authorization", "Bearer " + accessToken) // Custom Header
.body(requestBody) // Body ์ง๋ ฌํ (์๋ JSON ๋ณํ)
.retrieve()
.body(UserResponse.class); // ์๋ต ์ญ์ง๋ ฌํJavabody(Object) ๋ฉ์๋๋ ๋ด๋ถ์ ์ผ๋ก HttpMessageConverter๋ฅผ ํตํด ์๋์ผ๋ก JSON ์ง๋ ฌํ๋๋ค.
Map<String, Object>๋ก Body๋ฅผ ์ง์ ๊ตฌ์ฑ
Map<String, Object> payload = Map.of(
"name", "test",
"role", "developer"
);
String result = restClient.post()
.uri("https://api.example.com/users")
.contentType(MediaType.APPLICATION_JSON)
.headers(headers -> {
headers.setBearerAuth("your-jwt-token");
headers.add("X-Custom-Header", "example");
})
.body(payload)
.retrieve()
.body(String.class);Javaheaders()๋ Consumer<HttpHeaders>๋ฅผ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ๋๋ค. ๊ฐ๋จํ ํค๋ ์ค์ ์ .header(key, value) ์ค์ ์ผ๋ก ๊ฐ๋ฅํ๋ค.
Form URL Encoded ์ ์ก (application/x-www-form-urlencoded)
Map ํํ๋ฅผ ์ฌ์ฉํ๋, Content-Type์ ํผ ํ์์ผ๋ก ์ง์ ํ๋ค.
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("username", "user1");
formData.add("password", "secret");
String tokenResponse = restClient.post()
.uri("https://auth.example.com/token")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.accept(MediaType.APPLICATION_JSON)
.body(formData)
.retrieve()
.body(String.class);Java๋ด๋ถ์ ์ผ๋ก FormHttpMessageConverter๊ฐ ์๋์ผ๋ก ์ฌ์ฉ๋๋ค.
Raw Body (๋ฌธ์์ด ์ง์ ์ง์ )
์ด๋ฏธ ์ง๋ ฌํ๋ JSON ๋ฌธ์์ด์ ์ง์ ์ ์กํ ์ ์๋ค.
String rawJson = """
{
"email": "test@example.com",
"active": true
}
""";
String result = restClient.post()
.uri("https://api.example.com/register")
.contentType(MediaType.APPLICATION_JSON)
.body(rawJson)
.retrieve()
.body(String.class);JavaBinary Body (์: ํ์ผ ์ ๋ก๋)
InputStream ๋๋ byte[]๋ฅผ ํตํด์ body๋ฅผ ์ ์กํ ์ ์๋ค.
Path filePath = Path.of("/tmp/sample.pdf");
byte[] fileBytes = Files.readAllBytes(filePath);
String response = restClient.post()
.uri("https://api.example.com/upload")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header("X-Filename", filePath.getFileName().toString())
.body(fileBytes)
.retrieve()
.body(String.class);Java์ด ๋ฐฉ์์ multipart ์ ๋ก๋๊ฐ ์๋ raw binary ์ ์ก์ด๋ค.
OutputStream์ ์ด์ฉํ ํ์ผ ์ ๋ก๋
Path filePath = Path.of("/tmp/large-video.mp4");
client.post()
.uri("https://api.example.com/upload")
.contentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM)
.body(outputStream -> {
try (InputStream inputStream = Files.newInputStream(filePath)) {
inputStream.transferTo(outputStream); // Input โ OutputStream ๋ณต์ฌ
}
})
.retrieve()
.toBodilessEntity(); // ์๋ต ๋ณธ๋ฌธ์ด ํ์ ์์ ๋Java์ ๋ก๋ ํ์ผ ์ ์ฒด์ธจ ๋ฉ๋ชจ๋ฆฌ์ ์ฌ๋ฆฌ์ง ์๊ณ ์คํธ๋ฆผ์ ํตํด ๋ฐ๋ก ์ ์กํ๊ธฐ ๋๋ฌธ์ ๋์ฉ๋ ํ์ผ ์ ๋ก๋์ ์ ํฉํ๋ค.
Multipart/Form-Data (ํ์ผ + JSON์ ์ก)
MultipartBodyBuilder๋ฅผ ์ด์ฉํ์ฌ ๊ฐ๋จํ ๊ตฌ์ฑํ ์ ์๋ค.
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("file", new FileSystemResource("/tmp/photo.png"));
builder.part("meta", "{\"author\":\"myname\"}", MediaType.APPLICATION_JSON);
String result = restClient.post()
.uri("https://api.example.com/upload")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(builder.build())
.retrieve()
.body(String.class);JavaRetrieving the Response (์๋ต ๊ฐ์ ธ์ค๊ธฐ)
๊ธฐ๋ณธ body(class<T>) ์ฌ์ฉ
๊ฐ์ฅ ๋จ์ํ๊ณ ์ผ๋ฐ์ ์ธ ๋ฐฉ์์ด๋ค.
์๋ต์ Content-Type(application/json, text/plain๋ฑ)์ ๋ฐ๋ผ HttpMessageConverter๊ฐ ์๋์ผ๋ก ์ง๋ ฌํ/์ญ์ง๋ ฌํ๋ฅผ ์ํํ๋ค.
User user = restClient.get()
.uri("https://api.example.com/users/1")
.retrieve()
.body(User.class); // JSON โ User ๊ฐ์ฒดJavaContent-Type์ด JSON์ด๋ฉด ObjectMapper๋ฅผ ํตํด์ ์๋์ผ๋ก ๋ณํ๋๋ค.
body (ParameterizedTypeReference<T>) ์ฌ์ฉ (์ ๋๋ฆญ ์ปฌ๋ ์ ๋์)
์๋ต์ด ๋ฆฌ์คํธ ํน์ ๋งต ํํ์ผ ๊ฒฝ์ฐ ์๋ฐ์ ํ์ ์๊ฑฐ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ParameterizedTypeReference๋ฅผ ์ฌ์ฉํ๋ค.
List<User> users = restClient.get()
.uri("https://api.example.com/users")
.retrieve()
.body(new ParameterizedTypeReference<List<User>>() {});JavaList<User> ํน์ Map<String, Object>์ ๊ฐ์ ๋ณตํฉ ํ์
๋ ์์ ํ๊ฒ ์ญ์ง๋ ฌํ ํ๋ค.
Spring์ ParameterizedTypeReference๋ ๋ด๋ถ์ ์ผ๋ก TypeReference๋ฅผ ์ ์งํด ์ค๋ค.
exchange() ํน์ toEntity()๋ก Raw Body ์ง์ ์ฒ๋ฆฌ
์๋ต ์ฝ๋, ์ํ ์ฝ๋ ๋ฑ์ ํจ๊ป ๋ค๋ฃจ๋ฉด์ ์ง์ body๋ฅผ ํ์ฑํ๊ณ ์ ํ ๋ ์ ์ฉํ๋ค.
ResponseEntity<String> response = restClient.get()
.uri("https://api.example.com/raw")
.retrieve()
.toEntity(String.class);
if (response.getStatusCode().is2xxSuccessful()) {
String rawJson = response.getBody();
User user = new ObjectMapper().readValue(rawJson, User.class);
}JavaString ํ์
์ผ๋ก body payload๋ฅผ ๋ฐ์ ๋ค ์ง์ Jackson/Gson์ผ๋ก ์ญ์ง๋ ฌํ ํ ์ ์๋ค.
๋นํ์ค ์๋ต (JSON + Base64, JSON + Hex๋ฑ)์ ์ฒ๋ฆฌํ ๋ ์์ฃผ ์ฌ์ฉ๋๋ค.
OutputStream์ ํตํ body ์์
๋์ฉ๋ ํ์ผ์ด๋ ์คํธ๋ฆฌ๋ฐ ์๋ต์ ๋ฐ์ ๋๋ OutputStream์ ์ง์ ๋ฐ์ ์ ์๋ค. ์ด ๋ฐฉ์์ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฒด body๋ฅผ ์ฌ๋ฆฌ์ง ์๊ณ ์คํธ๋ฆผ์ ํตํด ์์ฐจ์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋กํ๊ธฐ ๋๋ฌธ์ OutOfMemoryError๋ฅผ ๋ฐฉ์งํ ์ ์๋ค.
restClient.get()
.uri("https://example.com/large-file.zip")
.retrieve()
.body((inputStream, headers) -> {
try (OutputStream outputStream =
new FileOutputStream("/tmp/large-file.zip")) {
inputStream.transferTo(outputStream);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return null; // body()๋ ๋ฐํ๊ฐ์ด ํ์ํ๋ฏ๋ก null ๋ฆฌํด
});Javabody(BiFunction<InputStream, HttpHeaders, T>) ํํ๋ฅผ ์ง์ํ๋ค. ์ฆ, ์
๋ ฅ์คํธ๋ฆผ์ ์ง์ ์ฝ๊ณ ์ฒ๋ฆฌํ ์ ์๋ค.
exchange()๋ฅผ ์ด์ฉํ์ฌ ์ ์์ค ๋ฐฉ์์ผ๋ก ์ฒ๋ฆฌํ ์ ์๋ค.
restClient.get()
.uri("https://example.com/large-file.zip")
.exchange((request, response) -> {
try (InputStream in = response.body();
OutputStream out = new FileOutputStream("/tmp/large-file.zip")) {
byte[] buffer = new byte[8192];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
}
return null;
});Java๋ณด๋ค ์ธ๋ฐํ ์ ์ด๊ฐ ํ์ํ ๊ฒฝ์ฐ์๋ exchange()๋ฅผ ์ด์ฉํด์ ClientHttpResponse๋ฅผ ์ง์ ๋ค๋ฃฌ๋ค.
exchange()๋ ์ํ์ฝ๋, ํค๋, body ์คํธ๋ฆผ์ ์์ ํ ์ ์ดํ ์ ์๋ค.
์ผ๋ฐ์ ์ธ retrieve()๋ณด๋ค ์ ์์ค์ด๋ฉฐ ๋น๋๊ธฐ/์คํธ๋ฆฌ๋ฐ ์ฒ๋ฆฌ์๋ ์ ํฉํ๋ค.
Error Handling (์๋ฌ ์ฒ๋ฆฌ)
๊ธฐ๋ณธ์ ์ผ๋ก RestClient๋ 4xx ๋๋ 5xx ์ํ ์ฝ๋์ ์๋ต์ ๊ฐ์ ธ์ฌ ๋ RestClientException์ ํ์ ํด๋์ค๋ฅผ ๋ฐ์์ํจ๋ค.
์ด ๋์์ onStatus()๋ฅผ ์ฌ์ฉํ์ฌ ์ฌ์ ์ ํ ์ ์๋ค.
String result = restClient.get()
.uri("https://example.com/this-url-does-not-exist")
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders());
})
.body(String.class);Java์ ์ฝ๋๋ 4xx ์๋ต ์ฝ๋๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ MyCustomRuntimeException์ ๋ฐ์์ํค๋ค.
Exchange
๊ณ ๊ธ ์๋๋ฆฌ์ค์์๋ RestClient๊ฐ retrieve() ๋์ ์ exchange() ๋ฉ์๋๋ฅผ ํตํด ๊ธฐ๋ณธ HTTP ์์ฒญ ๋ฐ ์๋ต์ ๋ํ ์ ๊ทผ์ ์ ๊ณตํ๋ค.
exchange() ์ฌ์ฉ ์ ์ํ ํธ๋ค๋ฌ๋ ์ ์ฉ๋์ง ์๋๋ค.
exchange() ๋ฉ์๋๊ฐ ์ด๋ฏธ ์ ์ฒด ์๋ต์ ๋ํ ์ ๊ทผ์ ์ ๊ณตํ๋ฏ๋ก ํ์ํ ์ค๋ฅ ์ฒ๋ฆฌ๋ฅผ ์ง์ ์ํํ ์ ์๊ธฐ ๋๋ฌธ์ด๋ค.
Pet result = restClient.get()
.uri("https://petclinic.example.com/pets/{id}", id)
.accept(APPLICATION_JSON)
.exchange((request, response) -> {
if (response.getStatusCode().is4xxClientError()) {
throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders());
}
else {
Pet pet = convertResponse(response);
return pet;
}
});Javaexchange() ๋ฉ์๋๋ request, response๋ฅผ ํ๋ผ๋ฏธํฐ๋ก ์ ๊ณตํ๋ ๋๋ค ํจ์๋ฅผ ํตํด ์์ฒญ, ์๋ต์ ๋ํ ์ฒ๋ฆฌ๋ฅผ ๋์ ์ผ๋ก ๊ฐ๋ฅํ๋๋ก ํ๋ค.
RestTemplate ์์ RestClient ๋ง์ด๊ทธ๋ ์ด์
https://docs.spring.io/spring-framework/reference/integration/rest-clients.html#_migrating_from_resttemplate_to_restclient ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ๋ฉด ๋์์ด ๋ ๊ฒ์ด๋ค.
๋.
์ฐธ๊ณ ๋งํฌ
https://docs.spring.io/spring-framework/reference/integration/rest-clients.html
