[Spring] RestClient 사용법

klmin·2024년 10월 18일
0

RestClient

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

profile
웹 개발자

0개의 댓글