Spring Boot 3.2과 Spring Framework 6.1에서 도입된 최신 HTTP 클라이언트이다.
Fluent API 방식을 사용하여 가독성을 높이고, 간결한 코드로 HTTP 요청을 처리할 수 있다.
동기식으로 작동한다.
RestTemplate을 RestClient로 컨버팅 가능하다.
RestTemplate에서 사용하는 MessageConverter, factory, interceptor 등의 설정을 사용할 수 있다.
WebClient와 메서드들이 흡사하다.
HttpInterface를 지원한다.
생성방법
# 빈생성
@Configuration
public class RestClientConfig {
public RestClient restClient(){
return RestClient.create();
}
}
# 기본호출
RestClient restClient = RestClient.create();
# 호출시 설정값 세팅
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")
.requestInterceptor(myCustomInterceptor)
.requestInitializer(myCustomInitializer)
.build();
# restTemplate 컨버팅
RestClient restClient = RestClient.create(restTemplate);
# 테스트 데이터
# 요청객체
@Getter
@Setter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class MemberRequest {
private Long id;
private String name;
private Integer age;
private List<String> hobby;
private Map<String, Object> score;
public static MemberRequest create(Long id, String name, Integer age, List<String> hobby, Map<String, Object> score) {
return new MemberRequest(id, name, age, hobby, score);
}
public MemberResponse toResponse() {
return MemberResponse.builder()
.id(id)
.name(name)
.age(age)
.hobby(hobby)
.score(score)
.build();
}
public MemberResponse toResponse(Long id) {
return MemberResponse.builder()
.id(id)
.name(name)
.age(age)
.hobby(hobby)
.score(score)
.build();
}
# 응답 객체
@Builder
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class MemberResponse {
private Long id;
private String name;
private Integer age;
private List<String> hobby;
private Map<String, Object> score;
}
# api응답객체
@Builder
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ApiResponse<T> {
private static final HttpStatus DEFAULT_SUCCESS_STATUS = HttpStatus.OK;
private boolean result;
private int code;
private String message;
private T data;
public static <T> ResponseEntity<ApiResponse<T>> success(T data) {
return create(true, null, DEFAULT_SUCCESS_STATUS, data);
}
public static <T> ResponseEntity<ApiResponse<T>> create(boolean result, String message, HttpStatus status, T data) {
return ResponseEntity.status(status).body(
ApiResponse.<T>builder()
.result(result)
.code(status.value())
.message(Optional.ofNullable(message).orElse(status.getReasonPhrase()))
.data(data)
.build()
);
}
}
# 컨트롤러
@RequestMapping("/members")
@RestController
public class MemberController {
@GetMapping
public ResponseEntity<ApiResponse<MemberResponse>> get(@ModelAttribute MemberRequest request) {
return ApiResponse.success(request.toResponse());
}
@PostMapping("/{id}")
public ResponseEntity<ApiResponse<MemberResponse>> post(@PathVariable Long id, @RequestBody MemberRequest request) {
return ApiResponse.success(request.toResponse(id));
}
@PutMapping("/{id}")
public ResponseEntity<ApiResponse<MemberResponse>> put(@PathVariable Long id, @RequestBody MemberRequest request) {
return ApiResponse.success(request.toResponse(id));
}
@PatchMapping("/{id}")
public ResponseEntity<ApiResponse<MemberResponse>> patch(@PathVariable Long id, @RequestBody MemberRequest request) {
return ApiResponse.success(request.toResponse(id));
}
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<MemberResponse>> delete(@PathVariable Long id) {
return ApiResponse.success(MemberResponse.builder().id(id).build());
}
}
get
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class RestClientServiceTest {
@LocalServerPort
private int port;
@Autowired
private RestClient restClient;
@Autowired
private ObjectMapper objectMapper;
private String url;
@BeforeEach
public void init(){
url = "http://localhost:" + port + "/members";
request = MemberRequest.create(1L, "테스트", 20,
List.of("영화감상","운동"),
Map.of("수학", 80, "영어", 70));
}
@Test
void get(){
String fullUrl = buildUriWithParams(url, request);
ResponseEntity<String> response = restClient.get()
.uri(fullUrl)
.retrieve()
.toEntity(String.class);
System.out.println("resposne.body : "+response.getBody());
String response1 = restClient.get()
.uri(fullUrl)
.retrieve()
.body(String.class);
System.out.println("response : "+response1);
ResponseEntity<ApiResponse<MemberResponse>> response2 = restClient.get()
.uri(fullUrl)
.accept(APPLICATION_JSON)
.retrieve()
.toEntity(new ParameterizedTypeReference<>() {});
System.out.println("response2 : "+response2.getBody().getData().getName());
}
private String buildUriWithParams(String url, Object params) {
UriComponentsBuilder urlBuilder = UriComponentsBuilder.fromHttpUrl(url);
Map<String, Object> paramMap = objectMapper.convertValue(params, new TypeReference<>() {});
paramMap.forEach((key, value) -> {
if (value instanceof List) {
((List<?>) value).forEach(item -> urlBuilder.queryParam(key, item));
} else if (value instanceof Map) {
((Map<?, ?>) value).forEach((mapKey, mapValue) ->
urlBuilder.queryParam(key + "[" + mapKey + "]", mapValue));
} else {
urlBuilder.queryParam(key, value);
}
});
return urlBuilder.build().toString();
}
}
로컬 컨트롤러 테스트를 위해 선언
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
객체를 url에 붙이기 위해 buildUriWithParams 사용
toEntity를 사용하면 ResponseEntity를 사용하고 헤더, status등을 받을수 있다.
accept 선언이 가능하다.
내부적으로 messageConverter를 사용한다.
응답으로는 객체와 ParameterizedTypeReference 둘다 지원한다.
post
ResponseEntity<ApiResponse<MemberResponse>> response = restClient.post()
.uri(url)
.contentType(APPLICATION_JSON)
.body(request)
.retrieve()
.toEntity(new ParameterizedTypeReference<>() {});
restClient.post()
.uri(failUrl)
.contentType(APPLICATION_JSON)
.body(request)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (req, res) -> {
throw new ApiRuntimeException(HttpStatus.valueOf(res.getStatusCode().value()), res.getStatusText());
})
.toEntity(new ParameterizedTypeReference<>() {});
ApiResponse<MemberResponse> response1 = restClient.post()
.uri(failUrl)
.contentType(APPLICATION_JSON)
.body(request)
.exchange((req, res) -> {
if (res.getStatusCode().is4xxClientError()) {
throw new ApiRuntimeException(HttpStatus.valueOf(res.getStatusCode().value()), res.getStatusText());
}
else {
return objectMapper.readValue(res.getBody(), new TypeReference<>() {});
}
});
body에 객체를 사용할 수 있고 onStatus로 에러를 잡아서 커스텀 에러로
throw 할수도 있고 exchange로 직접 throw 및 return 정의도 가능하다.
patch
ApiResponse<MemberResponse> response = restClient.patch()
.uri(url)
.contentType(APPLICATION_JSON)
.body(request)
.retrieve()
.body(new ParameterizedTypeReference<>() {});
patch() 로만 바꿔주면 된다.
put
ApiResponse<MemberResponse> response = restClient.put()
.uri(url)
.contentType(APPLICATION_JSON)
.body(request)
.retrieve()
.body(new ParameterizedTypeReference<>() {});
put() 으로만 바꿔주면 된다.
delete
url += "/{id}";
ResponseEntity<Void> response = restClient.delete()
.uri(url, 1)
.retrieve()
.toBodilessEntity();
System.out.println("response : "+ response.getBody());
System.out.println("response : "+ response.getStatusCode());
get()과 비슷하고 pathvariable과 url 둘다 가능하다.
toBodilessEntity로 void로 받을수도 있다.
}
참고 : https://docs.spring.io/spring-framework/reference/integration/rest-clients.html