Loading...
Spring Framework Reference Documentation 7.0.2의 REST Clients의 한국어 번역본입니다.
아래의 경우에 피드백에서 신고해주신다면 반영하겠습니다.
감사합니다 :)
The Spring Framework는 REST endpoint에 대한 호출을 수행하기 위해 다음과 같은 선택지를 제공합니다:
RestClient — fluent API를 갖춘 동기 클라이언트WebClient — fluent API를 갖춘 논블로킹, 리액티브 클라이언트RestTemplate — template 메서드 API를 갖춘 동기 클라이언트로, 이제 RestClient를 선호함에 따라 deprecated되었습니다RestClientRestClient는 요청을 수행하기 위한 fluent API를 제공하는 동기 HTTP 클라이언트입니다.
이는 HTTP 라이브러리 위의 추상화 계층 역할을 하며, HTTP 요청과 응답 content를 상위 수준 Java 객체로, 그리고 그 반대로 변환하는 작업을 처리합니다.
RestClientRestClient에는 static create shortcut 메서드가 있습니다.
또한 추가 옵션을 위한 builder()도 노출합니다:
ApiVersionInserter 구성일단 생성되면, RestClient는 여러 thread에서 안전하게 사용할 수 있습니다.
아래는 RestClient를 생성하거나 build하는 방법을 보여줍니다:
1RestClient defaultClient = RestClient.create(); 2 3RestClient customClient = RestClient.builder() 4 .requestFactory(new HttpComponentsClientHttpRequestFactory()) 5 .messageConverters(converters -> converters.add(new MyCustomMessageConverter())) 6 .baseUrl("https://example.com") 7 .defaultUriVariables(Map.of("variable", "foo")) 8 .defaultHeader("My-Header", "Foo") 9 .defaultCookie("My-Cookie", "Bar") 10 .defaultVersion("1.2") 11 .apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build()) 12 .requestInterceptor(myCustomInterceptor) 13 .requestInitializer(myCustomInitializer) 14 .build();
1val defaultClient = RestClient.create() 2 3val customClient = RestClient.builder() 4 .requestFactory(HttpComponentsClientHttpRequestFactory()) 5 .messageConverters { converters -> converters.add(MyCustomMessageConverter()) } 6 .baseUrl("https://example.com") 7 .defaultUriVariables(mapOf("variable" to "foo")) 8 .defaultHeader("My-Header", "Foo") 9 .defaultCookie("My-Cookie", "Bar") 10 .defaultVersion("1.2") 11 .apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build()) 12 .requestInterceptor(myCustomInterceptor) 13 .requestInitializer(myCustomInitializer) 14 .build()
RestClientHTTP 요청을 수행하려면 먼저 사용할 HTTP 메서드를 지정합니다.
get(), head(), post() 등의 편의 메서드 또는 method(HttpMethod)를 사용합니다.
다음으로 uri 메서드를 사용하여 요청 URI를 지정합니다.
이는 선택 사항이며, builder를 통해 baseUrl을 구성한 경우 이 단계를 건너뛸 수 있습니다.
URL은 일반적으로 선택적인 URI 템플릿 변수와 함께 String으로 지정됩니다.
다음은 요청을 수행하는 방법을 보여줍니다:
1int id = 42; 2restClient.get() 3 .uri("https://example.com/orders/{id}", id) 4 // ...
1val id = 42 2restClient.get() 3 .uri("https://example.com/orders/{id}", id) 4 // ...
요청 파라미터를 지정하는 등의 더 많은 제어를 위해 function도 사용할 수 있습니다.
String URL은 기본적으로 인코딩되지만, custom uriBuilderFactory로 클라이언트를 build하여 이를 변경할 수 있습니다.
URL은 function으로 또는 java.net.URI로 제공될 수도 있으며, 이 둘은 인코딩되지 않습니다.
URI를 다루고 인코딩하는 방법에 대한 자세한 내용은 URI Links를 참조하십시오.
필요한 경우, header(String, String), headers(Consumer<HttpHeaders>) 또는 accept(MediaType…), acceptCharset(Charset…) 등의 편의 메서드로 요청 헤더를 추가하여 HTTP 요청을 조작할 수 있습니다.
body를 포함할 수 있는 HTTP 요청(POST, PUT, PATCH)의 경우, contentType(MediaType) 및 contentLength(long)와 같은 추가 메서드를 사용할 수 있습니다.
클라이언트가 ApiVersionInserter로 구성된 경우 요청에 API 버전을 설정할 수 있습니다.
요청 body 자체는 body(Object)로 설정할 수 있으며, 이는 내부적으로 HTTP Message Conversion을 사용합니다.
또는, ParameterizedTypeReference를 사용하여 요청 body를 설정할 수 있으며, 이를 통해 generics를 사용할 수 있습니다.
마지막으로, body는 OutputStream에 쓰는 callback function으로 설정할 수 있습니다.
요청이 설정되면 retrieve() 이후에 메서드 호출을 체이닝하여 전송할 수 있습니다.
예를 들어, 응답 body는 parameterized type(예: list)에 대해 retrieve().body(Class) 또는 retrieve().body(ParameterizedTypeReference)를 사용하여 접근할 수 있습니다.
body 메서드는 응답 content를 다양한 type으로 변환합니다. 예를 들어, byte는 String으로 변환될 수 있고, JSON은 Jackson을 사용하여 객체로 변환될 수 있습니다( HTTP Message Conversion 참조).
응답은 또한 ResponseEntity로 변환될 수 있으며, retrieve().toEntity(Class)를 사용하여 body뿐 아니라 응답 헤더에도 접근할 수 있습니다.
retrieve()만 단독으로 호출하는 것은 아무 작업도 하지 않으며ResponseSpec을 반환합니다. 애플리케이션은 side effect를 가지려면ResponseSpec에서 terminal operation을 호출해야 합니다. 응답을 소비하는 것이 use case에 중요하지 않다면retrieve().toBodilessEntity()를 사용할 수 있습니다.
다음 예시는 RestClient를 사용하여 단순한 GET 요청을 수행하는 방법을 보여줍니다.
1String result = restClient.get() // (1) 2 .uri("https://example.com") // (2) 3 .retrieve() // (3) 4 .body(String.class); // (4) 5 6System.out.println(result); // (5)
| 1 | GET 요청을 설정합니다 |
| 2 | 연결할 URL을 지정합니다 |
| 3 | 응답을 retrieve합니다 |
| 4 | 응답을 string으로 변환합니다 |
| 5 | 결과를 출력합니다 |
1val result = restClient.get() // (1) 2 .uri("https://example.com") // (2) 3 .retrieve() // (3) 4 .body<String>() // (4) 5 6println(result) // (5)
| 1 | GET 요청을 설정합니다 |
| 2 | 연결할 URL을 지정합니다 |
| 3 | 응답을 retrieve합니다 |
| 4 | 응답을 string으로 변환합니다 |
| 5 | 결과를 출력합니다 |
응답 status 코드와 헤더에 대한 접근은 ResponseEntity를 통해 제공됩니다:
1ResponseEntity<String> result = restClient.get() // (1) 2 .uri("https://example.com") // (1) 3 .retrieve() 4 .toEntity(String.class); // (2) 5 6System.out.println("Response status: " + result.getStatusCode()); // (3) 7System.out.println("Response headers: " + result.getHeaders()); // (3) 8System.out.println("Contents: " + result.getBody()); // (3)
| 1 | 지정된 URL에 대한 GET 요청을 설정합니다 |
| 2 | 응답을 ResponseEntity로 변환합니다 |
| 3 | 결과를 출력합니다 |
1val result = restClient.get() // (1) 2 .uri("https://example.com") // (1) 3 .retrieve() 4 .toEntity<String>() // (2) 5 6println("Response status: " + result.statusCode) // (3) 7println("Response headers: " + result.headers) // (3) 8println("Contents: " + result.body) // (3)
| 1 | 지정된 URL에 대한 GET 요청을 설정합니다 |
| 2 | 응답을 ResponseEntity로 변환합니다 |
| 3 | 결과를 출력합니다 |
RestClient는 Jackson 라이브러리를 사용하여 JSON을 객체로 변환할 수 있습니다.
이 예시에서 URI 변수 사용과 Accept 헤더가 JSON으로 설정되어 있는 점에 주목하십시오.
1int id = ...; 2Pet pet = restClient.get() 3 .uri("https://petclinic.example.com/pets/{id}", id) // (1) 4 .accept(APPLICATION_JSON) // (2) 5 .retrieve() 6 .body(Pet.class); // (3)
| 1 | URI 변수 사용 |
| 2 | Accept 헤더를 application/json으로 설정 |
| 3 | JSON 응답을 Pet 도메인 객체로 변환 |
1val id = ... 2val pet = restClient.get() 3 .uri("https://petclinic.example.com/pets/{id}", id) // (1) 4 .accept(APPLICATION_JSON) // (2) 5 .retrieve() 6 .body<Pet>() // (3)
| 1 | URI 변수 사용 |
| 2 | Accept 헤더를 application/json으로 설정 |
| 3 | JSON 응답을 Pet 도메인 객체로 변환 |
다음 예시에서, RestClient는 JSON을 포함하는 POST 요청을 수행하는 데 사용되며, 이는 다시 Jackson을 사용하여 변환됩니다.
1Pet pet = ...; // (1) 2ResponseEntity<Void> response = restClient.post() // (2) 3 .uri("https://petclinic.example.com/pets/new") // (2) 4 .contentType(APPLICATION_JSON) // (3) 5 .body(pet) // (4) 6 .retrieve() 7 .toBodilessEntity(); // (5)
| 1 | Pet 도메인 객체를 생성 |
| 2 | POST 요청과 연결할 URL을 설정 |
| 3 | Content-Type 헤더를 application/json으로 설정 |
| 4 | 요청 body로 pet을 사용 |
| 5 | body가 없는 응답 엔터티로 응답을 변환 |
1val pet: Pet = ... // (1) 2val response = restClient.post() // (2) 3 .uri("https://petclinic.example.com/pets/new") // (2) 4 .contentType(APPLICATION_JSON) // (3) 5 .body(pet) // (4) 6 .retrieve() 7 .toBodilessEntity() // (5)
| 1 | Pet 도메인 객체를 생성 |
| 2 | POST 요청과 연결할 URL을 설정 |
| 3 | Content-Type 헤더를 application/json으로 설정 |
| 4 | 요청 body로 pet을 사용 |
| 5 | body가 없는 응답 엔터티로 응답을 변환 |
기본적으로, RestClient는 4xx 또는 5xx status 코드를 가진 응답을 retrieve할 때 RestClientException의 subclass를 throw합니다.
이 동작은 onStatus를 사용하여 override할 수 있습니다.
1String result = restClient.get() // (1) 2 .uri("https://example.com/this-url-does-not-exist") // (1) 3 .retrieve() 4 .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { // (2) 5 throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); // (3) 6 }) 7 .body(String.class);
| 1 | 404 status 코드를 반환하는 URL에 대한 GET 요청을 생성 |
| 2 | 모든 4xx status 코드에 대한 status 핸들러를 설정 |
| 3 | custom exception을 throw |
1val result = restClient.get() // (1) 2 .uri("https://example.com/this-url-does-not-exist") // (1) 3 .retrieve() 4 .onStatus(HttpStatusCode::is4xxClientError) { _, response -> // (2) 5 throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) // (3) 6 } 7 .body<String>()
| 1 | 404 status 코드를 반환하는 URL에 대한 GET 요청을 생성 |
| 2 | 모든 4xx status 코드에 대한 status 핸들러를 설정 |
| 3 | custom exception을 throw |
보다 고급 시나리오의 경우, RestClient는 retrieve() 대신 사용할 수 있는 exchange() 메서드를 통해 기본 HTTP 요청과 응답에 대한 접근을 제공합니다.
exchange()를 사용할 때는 status 핸들러가 적용되지 않습니다. 왜냐하면 exchange function이 이미 전체 응답에 대한 접근을 제공하여 필요한 모든 error handling을 수행할 수 있게 해주기 때문입니다.
1Pet result = restClient.get() 2 .uri("https://petclinic.example.com/pets/{id}", id) 3 .accept(APPLICATION_JSON) 4 .exchange((request, response) -> { // (1) 5 if (response.getStatusCode().is4xxClientError()) { // (2) 6 throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); // (2) 7 } 8 else { 9 Pet pet = convertResponse(response); // (3) 10 return pet; 11 } 12 });
| 1 | exchange는 요청과 응답을 제공합니다 |
| 2 | 응답이 4xx status 코드를 가진 경우 exception을 throw |
| 3 | 응답을 Pet 도메인 객체로 변환 |
1val result = restClient.get() 2 .uri("https://petclinic.example.com/pets/{id}", id) 3 .accept(MediaType.APPLICATION_JSON) 4 .exchange { request, response -> // (1) 5 if (response.getStatusCode().is4xxClientError()) { // (2) 6 throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) // (2) 7 } else { 8 val pet: Pet = convertResponse(response) // (3) 9 pet 10 } 11 }
| 1 | exchange는 요청과 응답을 제공합니다 |
| 2 | 응답이 4xx status 코드를 가진 경우 exception을 throw |
| 3 | 응답을 Pet 도메인 객체로 변환 |
지원되는 HTTP message 컨버터는 전용 section에서 확인하십시오.
객체 property의 subset만 serialize하려면, 다음 예시와 같이 Jackson JSON View를 지정할 수 있습니다:
1MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23")); 2value.setSerializationView(User.WithoutPasswordView.class); 3 4ResponseEntity<Void> response = restClient.post() // or RestTemplate.postForEntity 5 .contentType(APPLICATION_JSON) 6 .body(value) 7 .retrieve() 8 .toBodilessEntity();
multipart 데이터를 전송하려면, 값이 part content에 대한 Object, file part에 대한 Resource, 헤더가 있는 part content에 대한 HttpEntity가 될 수 있는 MultiValueMap<String, Object>를 제공해야 합니다.
예를 들면 다음과 같습니다:
1MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>(); 2 3parts.add("fieldPart", "fieldValue"); 4parts.add("filePart", new FileSystemResource("...logo.png")); 5parts.add("jsonPart", new Person("Jason")); 6 7HttpHeaders headers = new HttpHeaders(); 8headers.setContentType(MediaType.APPLICATION_XML); 9parts.add("xmlPart", new HttpEntity<>(myBean, headers)); 10 11// send using RestClient.post or RestTemplate.postForEntity
대부분의 경우, 각 part에 대한 Content-Type을 지정할 필요가 없습니다.
content type은 이를 serialize하기 위해 선택된 HttpMessageConverter 또는 Resource의 경우 file 확장자를 기반으로 자동으로 결정됩니다.
필요한 경우, HttpEntity wrapper로 MediaType을 명시적으로 제공할 수 있습니다.
MultiValueMap이 준비되면, RestClient.post().body(parts)(또는 RestTemplate.postForObject)를 사용하여 POST 요청의 body로 사용할 수 있습니다.
MultiValueMap에 하나 이상의 non-String 값이 포함되어 있으면, Content-Type은 FormHttpMessageConverter에 의해 multipart/form-data로 설정됩니다.
MultiValueMap에 String 값만 있는 경우, Content-Type은 기본적으로 application/x-www-form-urlencoded가 됩니다.
필요한 경우 Content-Type을 명시적으로 설정할 수도 있습니다.
HTTP 요청을 실행하기 위해, RestClient는 클라이언트 HTTP 라이브러리를 사용합니다.
이들 라이브러리는 ClientRequestFactory 인터페이스를 통해 적응됩니다.
다양한 구현이 제공됩니다:
HttpClient용 JdkClientHttpRequestFactoryHttpClient와 함께 사용하기 위한 HttpComponentsClientHttpRequestFactoryHttpClient용 JettyClientHttpRequestFactoryHttpClient용 ReactorNettyClientRequestFactorySimpleClientHttpRequestFactoryRestClient가 build될 때 요청 팩토리가 지정되지 않으면, classpath에서 사용할 수 있는 경우 Apache 또는 Jetty HttpClient를 사용합니다.
그렇지 않고 java.net.http module이 로드된 경우 Java의 HttpClient를 사용합니다.
마지막으로, simple 기본값으로 돌아갑니다.
SimpleClientHttpRequestFactory는 error를 나타내는 응답(예: 401)의 status에 접근할 때 exception을 발생시킬 수 있습니다. 이것이 문제가 된다면, 다른 대체 요청 팩토리 중 하나를 사용하십시오.
WebClientWebClient는 HTTP 요청을 수행하기 위한 논블로킹, 리액티브 클라이언트입니다. 이는
5.0에서 도입되었으며, 동기, 비동기 및 스트리밍 시나리오를 지원하는
RestTemplate에 대한 대안을 제공합니다.
WebClient는 다음을 지원합니다:
자세한 내용은 WebClient를 참조하십시오.
RestTemplateRestTemplate은 classic Spring Template class의 형태로 HTTP 클라이언트 라이브러리 위에 high-level API를 제공합니다.
이는 다음과 같은 overload된 메서드 그룹을 노출합니다:
Spring Framework 7.0부터,
RestTemplate은RestClient를 선호함에 따라 deprecated되었으며 향후 버전에서 제거될 예정입니다, "Migrating to RestClient" guide를 사용하십시오. 비동기 및 스트리밍 시나리오의 경우, 리액티브 WebClient를 고려하십시오.
| Method group | Description |
|---|---|
getForObject | GET을 통해 representation을 retrieve합니다. |
getForEntity | GET을 사용하여 ResponseEntity(즉, status, 헤더 및 body)를 retrieve합니다. |
headForHeaders | HEAD를 사용하여 resource에 대한 모든 헤더를 retrieve합니다. |
postForLocation | POST를 사용하여 새 resource를 생성하고 응답에서 Location 헤더를 반환합니다. |
postForObject | POST를 사용하여 새 resource를 생성하고 응답에서 representation을 반환합니다. |
postForEntity | POST를 사용하여 새 resource를 생성하고 응답에서 representation을 반환합니다. |
put | PUT을 사용하여 resource를 생성하거나 업데이트합니다. |
patchForObject | PATCH를 사용하여 resource를 업데이트하고 응답에서 representation을 반환합니다.<br>JDK HttpURLConnection은 PATCH를 지원하지 않지만, Apache HttpComponents 등은 지원합니다. |
delete | DELETE를 사용하여 지정된 URI의 resource를 삭제합니다. |
optionsForAllow | ALLOW를 사용하여 resource에 허용된 HTTP 메서드를 retrieve합니다. |
exchange | 필요할 때 추가적인 유연성을 제공하는, 앞의 메서드보다 더 일반화된(그리고 덜 opinionated한) 버전입니다.<br>RequestEntity(HTTP 메서드, URL, 헤더 및 body 포함)를 입력으로 받아 ResponseEntity를 반환합니다.<br>이들 메서드는 generics가 있는 응답 type을 지정하기 위해 Class 대신 ParameterizedTypeReference를 사용할 수 있게 합니다. |
execute | callback 인터페이스를 통해 요청 준비 및 응답 추출에 대한 전체 제어를 제공하는, 요청을 수행하는 가장 일반화된 방법입니다. |
Table 1. RestTemplate methods
RestTemplate은 RestClient와 동일한 HTTP 라이브러리 추상화를 사용합니다.
기본적으로, SimpleClientHttpRequestFactory를 사용하지만, constructor를 통해 이를 변경할 수 있습니다.
Client Request Factories를 참조하십시오.
RestTemplate는 observability를 위해 instrument될 수 있으며, 이를 통해 metric과 trace를 생성할 수 있습니다. RestTemplate Observability support section을 참조하십시오.
RestTemplate 메서드에 전달되고 반환되는 객체는 HttpMessageConverter의 도움을 받아 HTTP message로부터 그리고 HTTP message로 변환됩니다. HTTP Message Conversion을 참조하십시오.
RestClient애플리케이션은 먼저 API 사용에, 그 다음에는 인프라 설정에 집중하여 점진적으로 RestClient를 채택할 수 있습니다.
다음 단계를 고려할 수 있습니다:
기존 RestTemplate 인스턴스에서 하나 이상의 RestClient를 생성합니다. 예: RestClient restClient = RestClient.create(restTemplate).
먼저 요청 발행에 집중하여, 애플리케이션에서 컴포넌트별로 점진적으로 RestTemplate 사용을 대체합니다.
API 동등성에 대해서는 아래 표를 참조하십시오.
모든 클라이언트 요청이 RestClient 인스턴스를 통해 진행되면, 이제 RestClient.Builder를 사용하여 기존
RestTemplate 인스턴스 생성을 복제하는 작업을 수행할 수 있습니다. RestTemplate과 RestClient는
동일한 인프라를 공유하므로, 설정에서 custom ClientHttpRequestFactory 또는 ClientHttpRequestInterceptor를 재사용할 수 있습니다.
RestClient builder API를 참조하십시오.
classpath에 다른 라이브러리가 없는 경우, RestClient는 modern JDK HttpClient로 구동되는 JdkClientHttpRequestFactory를 선택하는 반면,
RestTemplate은 HttpURLConnection을 사용하는 SimpleClientHttpRequestFactory를 선택합니다.
이는 HTTP 수준에서 runtime 시 미묘한 동작 차이를 설명할 수 있습니다.
다음 표는 RestTemplate 메서드에 대한 RestClient 동등체를 보여줍니다.
RestTemplate method | RestClient equivalent |
|---|---|
getForObject(String, Class, Object…) | get()<br>.uri(String, Object…)<br>.retrieve()<br>.body(Class) |
getForObject(String, Class, Map) | get()<br>.uri(String, Map)<br>.retrieve()<br>.body(Class) |
getForObject(URI, Class) | get()<br>.uri(URI)<br>.retrieve()<br>.body(Class) |
getForEntity(String, Class, Object…) | get()<br>.uri(String, Object…)<br>.retrieve()<br>.toEntity(Class) |
getForEntity(String, Class, Map) | get()<br>.uri(String, Map)<br>.retrieve()<br>.toEntity(Class) |
getForEntity(URI, Class) | get()<br>.uri(URI)<br>.retrieve()<br>.toEntity(Class) |
headForHeaders(String, Object…) | head()<br>.uri(String, Object…)<br>.retrieve()<br>.toBodilessEntity()<br>.getHeaders() |
headForHeaders(String, Map) | head()<br>.uri(String, Map)<br>.retrieve()<br>.toBodilessEntity()<br>.getHeaders() |
headForHeaders(URI) | head()<br>.uri(URI)<br>.retrieve()<br>.toBodilessEntity()<br>.getHeaders() |
postForLocation(String, Object, Object…) | post()<br>.uri(String, Object…)<br>.body(Object)<br>.retrieve()<br>.toBodilessEntity()<br>.getLocation() |
postForLocation(String, Object, Map) | post()<br>.uri(String, Map)<br>.body(Object)<br>.retrieve()<br>.toBodilessEntity()<br>.getLocation() |
postForLocation(URI, Object) | post()<br>.uri(URI)<br>.body(Object)<br>.retrieve()<br>.toBodilessEntity()<br>.getLocation() |
postForObject(String, Object, Class, Object…) | post()<br>.uri(String, Object…)<br>.body(Object)<br>.retrieve()<br>.body(Class) |
postForObject(String, Object, Class, Map) | post()<br>.uri(String, Map)<br>.body(Object)<br>.retrieve()<br>.body(Class) |
postForObject(URI, Object, Class) | post()<br>.uri(URI)<br>.body(Object)<br>.retrieve()<br>.body(Class) |
postForEntity(String, Object, Class, Object…) | post()<br>.uri(String, Object…)<br>.body(Object)<br>.retrieve()<br>.toEntity(Class) |
postForEntity(String, Object, Class, Map) | post()<br>.uri(String, Map)<br>.body(Object)<br>.retrieve()<br>.toEntity(Class) |
postForEntity(URI, Object, Class) | post()<br>.uri(URI)<br>.body(Object)<br>.retrieve()<br>.toEntity(Class) |
put(String, Object, Object…) | put()<br>.uri(String, Object…)<br>.body(Object)<br>.retrieve()<br>.toBodilessEntity() |
put(String, Object, Map) | put()<br>.uri(String, Map)<br>.body(Object)<br>.retrieve()<br>.toBodilessEntity() |
put(URI, Object) | put()<br>.uri(URI)<br>.body(Object)<br>.retrieve()<br>.toBodilessEntity() |
patchForObject(String, Object, Class, Object…) | patch()<br>.uri(String, Object…)<br>.body(Object)<br>.retrieve()<br>.body(Class) |
patchForObject(String, Object, Class, Map) | patch()<br>.uri(String, Map)<br>.body(Object)<br>.retrieve()<br>.body(Class) |
patchForObject(URI, Object, Class) | patch()<br>.uri(URI)<br>.body(Object)<br>.retrieve()<br>.body(Class) |
delete(String, Object…) | delete()<br>.uri(String, Object…)<br>.retrieve()<br>.toBodilessEntity() |
delete(String, Map) | delete()<br>.uri(String, Map)<br>.retrieve()<br>.toBodilessEntity() |
delete(URI) | delete()<br>.uri(URI)<br>.retrieve()<br>.toBodilessEntity() |
optionsForAllow(String, Object…) | options()<br>.uri(String, Object…)<br>.retrieve()<br>.toBodilessEntity()<br>.getAllow() |
optionsForAllow(String, Map) | options()<br>.uri(String, Map)<br>.retrieve()<br>.toBodilessEntity()<br>.getAllow() |
optionsForAllow(URI) | options()<br>.uri(URI)<br>.retrieve()<br>.toBodilessEntity()<br>.getAllow() |
exchange(String, HttpMethod, HttpEntity, Class, Object…) | method(HttpMethod)<br>.uri(String, Object…)<br>.headers(Consumer<HttpHeaders>)<br>.body(Object)<br>.retrieve()<br>.toEntity(Class)[ 1] |
exchange(String, HttpMethod, HttpEntity, Class, Map) | method(HttpMethod)<br>.uri(String, Map)<br>.headers(Consumer<HttpHeaders>)<br>.body(Object)<br>.retrieve()<br>.toEntity(Class)[ 1] |
exchange(URI, HttpMethod, HttpEntity, Class) | method(HttpMethod)<br>.uri(URI)<br>.headers(Consumer<HttpHeaders>)<br>.body(Object)<br>.retrieve()<br>.toEntity(Class)[ 1] |
exchange(String, HttpMethod, HttpEntity, ParameterizedTypeReference, Object…) | method(HttpMethod)<br>.uri(String, Object…)<br>.headers(Consumer<HttpHeaders>)<br>.body(Object)<br>.retrieve()<br>.toEntity(ParameterizedTypeReference)[ 1] |
exchange(String, HttpMethod, HttpEntity, ParameterizedTypeReference, Map) | method(HttpMethod)<br>.uri(String, Map)<br>.headers(Consumer<HttpHeaders>)<br>.body(Object)<br>.retrieve()<br>.toEntity(ParameterizedTypeReference)[ 1] |
exchange(URI, HttpMethod, HttpEntity, ParameterizedTypeReference) | method(HttpMethod)<br>.uri(URI)<br>.headers(Consumer<HttpHeaders>)<br>.body(Object)<br>.retrieve()<br>.toEntity(ParameterizedTypeReference)[ 1] |
exchange(RequestEntity, Class) | method(HttpMethod)<br>.uri(URI)<br>.headers(Consumer<HttpHeaders>)<br>.body(Object)<br>.retrieve()<br>.toEntity(Class)[ 2] |
exchange(RequestEntity, ParameterizedTypeReference) | method(HttpMethod)<br>.uri(URI)<br>.headers(Consumer<HttpHeaders>)<br>.body(Object)<br>.retrieve()<br>.toEntity(ParameterizedTypeReference)[ 2] |
execute(String, HttpMethod, RequestCallback, ResponseExtractor, Object…) | method(HttpMethod)<br>.uri(String, Object…)<br>.exchange(ExchangeFunction) |
execute(String, HttpMethod, RequestCallback, ResponseExtractor, Map) | method(HttpMethod)<br>.uri(String, Map)<br>.exchange(ExchangeFunction) |
execute(URI, HttpMethod, RequestCallback, ResponseExtractor) | method(HttpMethod)<br>.uri(URI)<br>.exchange(ExchangeFunction) |
Table 2. RestClient equivalents for RestTemplate methods
RestClient와 RestTemplate 인스턴스는 exception을 throw하는 동작에 대해 동일한 behavior를 공유합니다
(RestClientException type이 hierarchy의 최상위에 있음).
RestTemplate이 "4xx" 응답 status에 대해 일관되게 HttpClientErrorException을 throw하는 반면,
RestClient는 custom "status handler"를 통해 더 많은 유연성을 제공합니다.
HTTP Service는 @HttpExchange 메서드가 있는 Java 인터페이스로 정의할 수 있으며,
RestClient, WebClient, 또는 RestTemplate을 통해 HTTP를 통한 remote access를 위해
HttpServiceProxyFactory를 사용하여 그로부터 클라이언트 프록시를 생성할 수 있습니다.
서버 side에서는, @Controller 클래스가 동일한 인터페이스를 구현하여
@HttpExchange
controller 메서드로 요청을 처리할 수 있습니다.
먼저, Java 인터페이스를 생성합니다:
1public interface RepositoryService { 2 3 @GetExchange("/repos/{owner}/{repo}") 4 Repository getRepository(@PathVariable String owner, @PathVariable String repo); 5 6 // more HTTP exchange methods... 7 8}
선택적으로, type level에서 @HttpExchange를 사용하여 모든 메서드에 대한 공통 attribute를 선언할 수 있습니다:
1@HttpExchange(url = "/repos/{owner}/{repo}", accept = "application/vnd.github.v3+json") 2public interface RepositoryService { 3 4 @GetExchange 5 Repository getRepository(@PathVariable String owner, @PathVariable String repo); 6 7 @PatchExchange(contentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE) 8 void updateRepository(@PathVariable String owner, @PathVariable String repo, 9 @RequestParam String name, @RequestParam String description, @RequestParam String homepage); 10 11}
다음으로, 클라이언트를 구성하고 HttpServiceProxyFactory를 생성합니다:
1// Using RestClient... 2 3RestClient restClient = RestClient.create("..."); 4RestClientAdapter adapter = RestClientAdapter.create(restClient); 5 6// or WebClient... 7 8WebClient webClient = WebClient.create("..."); 9WebClientAdapter adapter = WebClientAdapter.create(webClient); 10 11// or RestTemplate... 12 13RestTemplate restTemplate = new RestTemplate(); 14RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate); 15 16HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
이제 클라이언트 프록시를 생성할 준비가 되었습니다:
1RepositoryService service = factory.createClient(RepositoryService.class); 2// Use service methods for remote calls...
HTTP service 클라이언트는 HTTP를 통한 remote access를 위한 강력하고 표현력이 풍부한 선택입니다. 이는 한 팀이 REST API가 어떻게 동작하는지, 어떤 부분이 클라이언트 애플리케이션에 관련 있는지, 어떤 input과 output type을 생성해야 하는지, 어떤 endpoint 메서드 시그니처가 필요한지, 어떤 Javadoc이 있어야 하는지 등에 대한 지식을 소유할 수 있게 합니다. 그 결과 생성된 Java API는 안내 역할을 하며 바로 사용할 준비가 되어 있습니다.
@HttpExchange 메서드는 다음과 같은 입력을 가진 유연한 메서드 시그니처를 지원합니다:
| Method parameter | Description |
|---|---|
URI | 요청에 대한 URL을 동적으로 설정하여 annotation의 url attribute를 override합니다. |
UriBuilderFactory | URI 템플릿과 URI 변수를 expand할 UriBuilderFactory를 제공합니다.<br>실질적으로, underlying 클라이언트의 UriBuilderFactory(및 그 base URL)를 대체합니다. |
HttpMethod | 요청에 대한 HTTP 메서드를 동적으로 설정하여 annotation의 method attribute를 override합니다 |
@RequestHeader | 요청 헤더 또는 여러 헤더를 추가합니다. argument는 단일 값,<br>Collection<?> 값, Map<String, ?>,MultiValueMap<String, ?>일 수 있습니다.<br>non-String 값에 대해서는 type conversion이 지원됩니다. 헤더 값은 추가되며<br>이미 추가된 헤더 값을 override하지 않습니다. |
@PathVariable | 요청 URL의 placeholder를 expand하기 위한 변수를 추가합니다. argument는 여러 변수가 있는<br>Map<String, ?> 또는 개별 값일 수 있습니다. non-String 값에 대해서는 type conversion이<br>지원됩니다. |
@RequestAttribute | 요청 attribute로 추가할 Object를 제공합니다. RestClient와 WebClient에서만<br>지원됩니다. |
@RequestBody | serialize될 Object 또는 Mono, Flux와 같은 Reactive Streams Publisher, 또는 구성된<br>ReactiveAdapterRegistry를 통해 지원되는 다른 async type을 요청 body로 제공합니다. |
@RequestParam | 요청 파라미터 또는 여러 파라미터를 추가합니다. argument는 여러 파라미터가 있는 Map<String, ?><br>또는 MultiValueMap<String, ?>, Collection<?> 값, 또는 개별 값일 수 있습니다.<br>non-String 값에 대해서는 type conversion이 지원됩니다.<br>"content-type"이 "application/x-www-form-urlencoded"로 설정된 경우, 요청<br>파라미터는 요청 body에 인코딩됩니다. 그렇지 않으면 URL query<br>파라미터로 추가됩니다. |
@RequestPart | String(form field), Resource(file part), Object(JSON 등으로 인코딩될 entity), HttpEntity(part content와 헤더),<br>Spring Part, 또는 위의 어떤 것에 대한 Reactive Streams Publisher가 될 수 있는 요청 part를 추가합니다. |
MultipartFile | 일반적으로 Spring MVC controller에서 uploaded file을 나타내는 MultipartFile에서 요청 part를 추가합니다. |
@CookieValue | 쿠키 또는 여러 쿠키를 추가합니다. argument는 여러 쿠키가 있는 Map<String, ?> 또는<br>MultiValueMap<String, ?>, Collection<?> 값, 또는 개별 값일 수 있습니다.<br>non-String 값에 대해서는 type conversion이 지원됩니다. |
메서드 parameter는 parameter annotation에서 사용할 수 있는 required attribute가 false로 설정되어 있거나,
parameter가 MethodParameter#isOptional에 의해 optional로 표시되지 않는 한 null일 수 없습니다.
RestClientAdapter는 요청 body를 OutputStream에 쓰는 방식으로 전송할 수 있게 해주는
StreamingHttpOutputMessage.Body type의 메서드 parameter에 대한 추가 지원을 제공합니다.
custom HttpServiceArgumentResolver를 구성할 수 있습니다.
아래 예시 인터페이스는 custom Search 메서드 parameter type을 사용합니다:
1public interface RepositoryService { 2 3 @GetExchange("/repos/search") 4 List<Repository> searchRepository(Search search); 5 6}
1interface RepositoryService { 2 3 @GetExchange("/repos/search") 4 fun searchRepository(search: Search): List<Repository> 5 6}
custom argument resolver는 다음과 같이 구현할 수 있습니다:
1static class SearchQueryArgumentResolver implements HttpServiceArgumentResolver { 2 @Override 3 public boolean resolve(Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) { 4 if (parameter.getParameterType().equals(Search.class)) { 5 Search search = (Search) argument; 6 requestValues.addRequestParameter("owner", search.owner()); 7 requestValues.addRequestParameter("language", search.language()); 8 requestValues.addRequestParameter("query", search.query()); 9 return true; 10 } 11 return false; 12 } 13}
1class SearchQueryArgumentResolver : HttpServiceArgumentResolver { 2 override fun resolve( 3 argument: Any?, 4 parameter: MethodParameter, 5 requestValues: HttpRequestValues.Builder 6 ): Boolean { 7 if (parameter.getParameterType() == Search::class.java) { 8 val search = argument as Search 9 requestValues.addRequestParameter("owner", search.owner) 10 .addRequestParameter("language", search.language) 11 .addRequestParameter("query", search.query) 12 return true 13 } 14 return false 15 } 16}
custom argument resolver를 구성하려면:
1RestClient restClient = RestClient.builder().baseUrl("https://api.github.com/").build(); 2RestClientAdapter adapter = RestClientAdapter.create(restClient); 3HttpServiceProxyFactory factory = HttpServiceProxyFactory 4 .builderFor(adapter) 5 .customArgumentResolver(new SearchQueryArgumentResolver()) 6 .build(); 7RepositoryService repositoryService = factory.createClient(RepositoryService.class); 8 9Search search = Search.create() 10 .owner("spring-projects") 11 .language("java") 12 .query("rest") 13 .build(); 14List<Repository> repositories = repositoryService.searchRepository(search);
1val restClient = RestClient.builder().baseUrl("https://api.github.com/").build() 2val adapter = RestClientAdapter.create(restClient) 3val factory = HttpServiceProxyFactory 4 .builderFor(adapter) 5 .customArgumentResolver(SearchQueryArgumentResolver()) 6 .build() 7val repositoryService = factory.createClient<RepositoryService>(RepositoryService::class.java) 8 9val search = Search(owner = "spring-projects", language = "java", query = "rest") 10val repositories = repositoryService.searchRepository(search)
기본적으로,
RequestEntity는 메서드 parameter로 지원되지 않으며, 대신 요청의 개별 부분에 대한 더 세분화된 메서드 parameter 사용을 권장합니다.
지원되는 return value는 underlying 클라이언트에 따라 달라집니다.
RestClient와 RestTemplate과 같은 HttpExchangeAdapter에 적응된 클라이언트는
동기 return value를 지원합니다:
| Method return value | Description |
|---|---|
void | 주어진 요청을 수행합니다. |
HttpHeaders | 주어진 요청을 수행하고 응답 헤더를 반환합니다. |
<T> | 주어진 요청을 수행하고 응답 content를 선언된 return type으로 decode합니다. |
ResponseEntity<Void> | 주어진 요청을 수행하고 status와 헤더가 있는 ResponseEntity를 반환합니다. |
ResponseEntity<T> | 주어진 요청을 수행하고 응답 content를 선언된 return type으로 decode한 뒤,<br>status, 헤더 및 decode된 body가 있는 ResponseEntity를 반환합니다. |
WebClient와 같이 ReactorHttpExchangeAdapter에 적응된 클라이언트는 위의 모든 것과
리액티브 variant를 지원합니다. 아래 표는 Reactor type을 보여주지만,
ReactiveAdapterRegistry를 통해 지원되는 다른 리액티브 type도 사용할 수 있습니다:
| Method return value | Description |
|---|---|
Mono<Void> | 주어진 요청을 수행하고, 있을 경우 응답 content를 release합니다. |
Mono<HttpHeaders> | 주어진 요청을 수행하고, 있을 경우 응답 content를 release한 뒤<br>응답 헤더를 반환합니다. |
Mono<T> | 주어진 요청을 수행하고 응답 content를 선언된 return type으로 decode합니다. |
Flux<T> | 주어진 요청을 수행하고 응답 content를 선언된 element type의 stream으로 decode합니다.<br> |
Mono<ResponseEntity<Void>> | 주어진 요청을 수행하고, 있을 경우 응답 content를 release한 뒤<br>status와 헤더가 있는 ResponseEntity를 반환합니다. |
Mono<ResponseEntity<T>> | 주어진 요청을 수행하고 응답 content를 선언된 return type으로 decode한 뒤,<br>status, 헤더 및 decode된 body가 있는 ResponseEntity를 반환합니다. |
Mono<ResponseEntity<Flux<T>> | 주어진 요청을 수행하고 응답 content를 선언된 element type의 stream으로 decode한 뒤,<br>status, 헤더 및 decode된 응답 body stream이 있는 ResponseEntity를 반환합니다. |
기본적으로, ReactorHttpExchangeAdapter를 사용한 동기 return value의 timeout은
underlying HTTP 클라이언트가 어떻게 구성되었는지에 따라 달라집니다.
adapter 수준에서 blockTimeout 값을 설정할 수도 있지만,
보다 낮은 수준에서 동작하며 더 많은 제어를 제공하는 underlying HTTP 클라이언트의 timeout 설정에 의존하는 것이 좋습니다.
RestClientAdapter는 raw 응답 body content에 대한 접근을 제공하는
InputStream 또는 ResponseEntity<InputStream> type의 return value에 대한 추가 지원을 제공합니다.
HTTP Service 클라이언트 프록시에 대한 error handling을 custom하려면, 필요에 따라 underlying 클라이언트를 구성할 수 있습니다. 기본적으로, 클라이언트는 4xx 및 5xx HTTP status 코드에 대해 exception을 발생시킵니다. 이를 custom하려면, 다음과 같이 클라이언트를 통해 수행되는 모든 응답에 적용되는 응답 status 핸들러를 등록합니다:
1// For RestClient 2RestClient restClient = RestClient.builder() 3 .defaultStatusHandler(HttpStatusCode::isError, (request, response) -> /* ... */) 4 .build(); 5RestClientAdapter adapter = RestClientAdapter.create(restClient); 6 7// or for WebClient... 8WebClient webClient = WebClient.builder() 9 .defaultStatusHandler(HttpStatusCode::isError, resp -> /* ... */) 10 .build(); 11WebClientAdapter adapter = WebClientAdapter.create(webClient); 12 13// or for RestTemplate... 14RestTemplate restTemplate = new RestTemplate(); 15restTemplate.setErrorHandler(myErrorHandler); 16 17RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate); 18 19HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
error status 코드를 억제하는 것과 같은 더 많은 세부 사항과 옵션에 대해서는,
각 클라이언트에 대한 reference documentation뿐 아니라
RestClient.Builder 또는 WebClient.Builder의 defaultStatusHandler와
RestTemplate의 setErrorHandler의 Javadoc을 참조하십시오.
HttpExchangeAdapter와 ReactorHttpExchangeAdapter는 HTTP Interface 클라이언트 인프라를
underlying 클라이언트를 호출하는 세부 사항에서 분리하는 contract입니다.
RestClient, WebClient, RestTemplate에 대한 adapter 구현이 있습니다.
때때로, HttpServiceProxyFactory.Builder에서 구성 가능한 decorator를 통해
클라이언트 호출을 intercept하는 것이 유용할 수 있습니다.
예를 들어, built-in decorator를 적용하여 404 exception을 억제하고
NOT_FOUND 및 null body가 있는 ResponseEntity를 반환할 수 있습니다:
1// For RestClient 2HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(restCqlientAdapter) 3 .exchangeAdapterDecorator(NotFoundRestClientAdapterDecorator::new) 4 .build(); 5 6// or for WebClient... 7HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builderFor(webClientAdapter) 8 .exchangeAdapterDecorator(NotFoundWebClientAdapterDecorator::new) 9 .build();
HttpServiceProxyFactory를 사용하여 클라이언트 프록시를 생성하는 것은 사소하지만,
이를 bean으로 선언하면 반복적인 구성이 발생합니다.
또한 여러 target host가 있을 수 있으므로 여러 클라이언트를 구성해야 하고,
더 많은 클라이언트 프록시 bean을 생성해야 할 수도 있습니다.
인터페이스 클라이언트를 대규모로 더 쉽게 다루기 위해 Spring Framework는 전용 configuration support를 제공합니다. 이를 통해 애플리케이션은 group별 HTTP Service를 식별하고, 각 group에 대한 클라이언트를 custom하는 데 집중할 수 있으며, framework는 투명하게 클라이언트 프록시 registry를 생성하고 각 프록시를 bean으로 선언합니다.
HTTP Service group은 동일한 클라이언트 setup과 프록시 생성을 위한 HttpServiceProxyFactory 인스턴스를 공유하는
인터페이스 집합일 뿐입니다.
일반적으로, 이는 host당 하나의 group을 의미하지만,
underlying 클라이언트를 다르게 구성해야 하는 경우 동일한 target host에 대해 둘 이상의 group을 가질 수 있습니다.
HTTP Service group을 선언하는 한 가지 방법은 아래와 같이
@Configuration 클래스에서 @ImportHttpServices annotation을 사용하는 것입니다:
1@Configuration 2@ImportHttpServices(group = "echo", types = {EchoServiceA.class, EchoServiceB.class}) // (1) 3@ImportHttpServices(group = "greeting", basePackageClasses = GreetServiceA.class) // (2) 4public class ClientConfig { 5}
| 1 | group "echo"에 대한 인터페이스를 수동으로 나열 |
| 2 | base package 아래에서 group "greeting"에 대한 인터페이스를 탐지 |
또한 HTTP Service registrar를 생성한 다음 이를 import하여 programmatic하게 group을 선언할 수도 있습니다:
1public class MyHttpServiceRegistrar extends AbstractHttpServiceRegistrar { // (1) 2 3 @Override 4 protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) { 5 registry.forGroup("echo").register(EchoServiceA.class, EchoServiceB.class); // (2) 6 registry.forGroup("greeting").detectInBasePackages(GreetServiceA.class); // (3) 7 } 8} 9 10@Configuration 11@Import(MyHttpServiceRegistrar.class) // (4) 12public class ClientConfig { 13}
| 1 | AbstractHttpServiceRegistrar의 extension 클래스 생성 |
| 2 | group "echo"에 대한 인터페이스를 수동으로 나열 |
| 3 | base package 아래에서 group "greeting"에 대한 인터페이스를 탐지 |
| 4 | registrar import |
@ImportHttpServiceannotation과 programmatic registrar를 혼합하여 사용할 수 있으며, 여러 configuration 클래스에 import를 분산시킬 수 있습니다. 모든 import는 동일한, 공유HttpServiceProxyRegistry인스턴스에 협력적으로 기여합니다.
HTTP Service group이 선언되면, 각 group에 대한 클라이언트를 custom하기 위해
HttpServiceGroupConfigurer bean을 추가합니다. 예를 들어:
1@Configuration 2@ImportHttpServices(group = "echo", types = {EchoServiceA.class, EchoServiceB.class}) 3@ImportHttpServices(group = "greeting", basePackageClasses = GreetServiceA.class) 4public class ClientConfig { 5 6 @Bean 7 public RestClientHttpServiceGroupConfigurer groupConfigurer() { 8 return groups -> { 9 // configure client for group "echo" 10 groups.filterByName("echo").forEachClient((group, clientBuilder) -> /* ... */); 11 12 // configure the clients for all groups 13 groups.forEachClient((group, clientBuilder) -> /* ... */); 14 15 // configure client and proxy factory for each group 16 groups.forEachGroup((group, clientBuilder, factoryBuilder) -> /* ... */); 17 }; 18 } 19}
Spring Boot는 HTTP Service group별 클라이언트 property에 대한 지원을 추가하기 위해
HttpServiceGroupConfigurer를 사용하고, Spring Security는 OAuth 지원을 추가하며, Spring Cloud는 load balancing을 추가합니다.
위의 결과로, 각 클라이언트 프록시는 bean으로 사용 가능하며 type으로 편리하게 autowire할 수 있습니다:
1@RestController 2public class EchoController { 3 4 private final EchoService echoService; 5 6 public EchoController(EchoService echoService) { 7 this.echoService = echoService; 8 } 9 10 // ... 11}
그러나 동일한 type의 여러 클라이언트 프록시(예: 여러 group에 있는 동일한 인터페이스)가 있는 경우,
해당 type의 고유한 bean이 없으며 type만으로 autowire할 수 없습니다.
이러한 경우, 모든 프록시를 보유한 HttpServiceProxyRegistry를 직접 사용하여,
group별로 필요한 프록시를 얻을 수 있습니다:
1@RestController 2public class EchoController { 3 4 private final EchoService echoService1; 5 6 private final EchoService echoService2; 7 8 public EchoController(HttpServiceProxyRegistry registry) { 9 this.echoService1 = registry.getClient("echo1", EchoService.class); // (1) 10 this.echoService2 = registry.getClient("echo2", EchoService.class); // (2) 11 } 12 13 // ... 14}
| 1 | group "echo1"에 대한 EchoService 클라이언트 프록시에 접근 |
| 2 | group "echo2"에 대한 EchoService 클라이언트 프록시에 접근 |
1. HttpEntity 헤더와 body는 headers(Consumer<HttpHeaders>)와 body(Object)를 통해 RestClient에 제공되어야 합니다.
2. RequestEntity 메서드, URI, 헤더 및 body는 method(HttpMethod), uri(URI), headers(Consumer<HttpHeaders>), body(Object)를 통해 RestClient에 제공되어야 합니다.
Integration
JMS (Java Message Service)