Back-End/Spring Boot

Spring Boot - RestTemplate를 사용한 Server To Server 연결(2)

러러 2024. 6. 22. 02:39

 

이 전 포스트에서는 RestTemplate로 queryString과 header에 데이터를 담아서 GET요청을 보내는 법을 알아보았다. 이번에는 pathParameter와 지금까지 한 것들을 DTO로 바꿔서 유연하게 대응할 수 있는 코드로 리팩토링 해보자.

  

이전 포스트에서 마지막으로 완성된 client와 server는

 

<client - service - RestTemplateService>

 public ResponseEntity<String> hello() {
    // 요청 보낼 uri 생성
    URI uri = UriComponentsBuilder
      .fromUriString("http://localhost:10000") // baseUrl
      .path("/api/server/hello") // 경로
      .encode() // 인코딩
      .build()
      .toUri();

    log.info("uri: {}", uri);

	// HttpHeaders
    HttpHeaders headers = new HttpHeaders();
    headers.set("x-authorization", "ffff"); // 헤더 생성
    headers.setContentType(MediaType.APPLICATION_JSON); // contentType은 json

    // HttpEntity 객체 생성
 	// body는 안보내기에 Void삽입
    HttpEntity<Void> entity = new HttpEntity<>(headers);

    RestTemplate restTemplate = new RestTemplate(); // RestTemplate생성

    // exchange에 uri, method, requestEntity, restponseType을 넣어서 보낸다.
    ResponseEntity<String> result = restTemplate.exchange(uri, HttpMethod.GET, entity, String.class);

    log.info("response : {}", result);
    log.info("body: {}", result.getBody());
    log.info("status: {}", result.getStatusCode());
    return result;
  }

 

<server - controller>

  @GetMapping("/hello")
  public String hello(
    @RequestHeader("x-authorization") String authorization,
    @RequestParam(name = "name") String name,
    @RequestParam(name = "age") int age
  ) {
    log.info("authorization = {}", authorization);
    log.info("name = {}", name);
    log.info("age = {}", age);
    return "Hello World";
  }

 

와 같은 모습이였다. 이제 String을 리턴해주지 말고 DTO를 리턴하도록 바꾸어보자.


객체로 데이터 주고 받기

먼저 사용자의 이름과 나이를 담은 UserRequest DTO를 생성해보자.

<client - dto> &&  <server- dto> 

@Builder
public record UserRequest(
  String name,
  int age
) {}

 

그런 다음 server와 client에서 UserRequest로 데이터를 보내고, 편의상 다시 그대로 돌려 받도록 만들어보자. 

 

<client - service - RestTemplateService>

public ResponseEntity<UserRequest> hello() {
	
    // [...] 이 전과 동일
    
    ResponseEntity<UserRequest> result = restTemplate.exchange(uri, HttpMethod.GET, entity, UserRequest.class);

    // ...
    return result;
  }

 

클라이언트에서 UserRequest로 받기로 했으니 서버에서도 타입을 맞춰서 돌려주자.

 

  @GetMapping("/hello")
  public ResponseEntity<UserRequest> hello(
    @RequestHeader("x-authorization") String authorization,
    UserRequest request
  ) {
    /// ...
    return ResponseEntity.ok(request);
  }

request와 response 모두 UserRequest로 맞춰주었다. 

이제 테스트를 해보면

원하는 대로 잘 나온 것을 확인할 수 있다.


Path Parameter의 사용

URI에 변수를 사용하려면 어떻게 해야할까? 지금까지 URI를 만들때에는 UriComponentsBuilder를 사용해서 생성을 했다. 그러다면 똑같이 UriComponentsBuilder를 자세히 살펴보면 들어있지 않을까? 공식 문서를 확인해보자.

buildAndExpand라는 함수가 존재한다. URl template에 변수를 등록할 수 있다는 것 같다. 그리고 설명을 보니 build() 후에 expand()를 한 것과 같다고 한다. 

한 번 사용해보자. 

 

<client - service - RestTemplateService>

public ResponseEntity<UserRequest> helloPathParam() {
    // 요청 보낼 uri 생성
    URI uri = UriComponentsBuilder
      .fromUriString("http://localhost:10000") // baseUrl
      .path("/api/server/user/{userId}") // 경로
      .queryParam("name", "Uheejoon")
      .queryParam("age", 25)
      .encode() // 인코딩
      .buildAndExpand(10)
      .toUri();

    // ... 

    ResponseEntity<UserRequest> result = restTemplate.exchange(uri, HttpMethod.GET, entity, UserRequest.class);

    return result;
  }

위에서 사용한 경로와 같은 Endpoint를 server측에도 만들어서 테스트를 진행해 보자.

URI도 잘 만들어지고 문제가 없이 잘 실행되었다.


로깅

지금까지는 로직이 존재하는 함수 내부에 로깅을 했다. 하지만 정말 그게 옳은 코드일까? 관심사의 분리가 시급다고 할 수 있다. 

그렇다면 어떻게 로깅과 로직을 분리해야할까? 또 어느 단계에서 해야할까? filter?, interceptor?, aop? 세 단계중에 하나일 것이다. 단순하게 생각해봤을 때, filter는 너무 큰 범위이고, aop는 모듈화의 개념이다. 다른 서버로의 요청을 확인하기에는 알맞지 않다. interceptor를 생각해보자. interceptor는 요청을 가로채는 것이다. resttemplate는 다른 서버로 요청을 보내고 결과 값을 받아온다. 살짝 감이 올 것이다. 다른 서버로의 요청과 응답을 가로채서 로깅을 하면 될것이다.

 

지금까지 사용한

RestTemplate restTemplate = new RestTemplate();

이러한 방식은 RestTemplate가 바로 구현된 상태이기 때문에 중간 단계에서 interceptor같은 bean을 등록할 수 없다.

bean을 등록하기 위해서는 RestTemplate가 Buid될 때 인터셉터를 추가해야한다. 따라서 RestTemplate가 아닌 RestTemplateBuilder를 사용해서 직접 여러가지 bean들을 추가해야한다.

RestTemplate restTemplate = new RestTemplateBuilder()
  .additionalInterceptors(new CInterceptor())
  .build();

 

RestTemplateBuilder를 사용하고 additionalInterceptors를 사용해서 custom Interceptor를 등록한다.

주의할 점은 여기서 등록되는 Interceptor는 일반적으로 사용되는 HandlerInterceptor가 아닌 ClientHttpRequestInterceptor 를 구현해야한다는 점이다.

@Slf4j
public class CInterceptor implements ClientHttpRequestInterceptor {
  @Override
  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    // header 추가
    request.getHeaders().add("Custom-Header", "Custom-Value");
    // 전처리
    // Request 정보 로깅
    log.info("Request URI : {}", request.getURI());
    log.info("Request Method : {}", request.getMethod());
    log.info("Request Headers : {}", request.getHeaders());
    log.info("Request Body : {}", new String(body, StandardCharsets.UTF_8));

    // 다음 인터셉터 또는 요청 실행
    ClientHttpResponse response = execution.execute(request, body);

    // 후처리
    // Response 정보 로깅
    log.info("Response Status : {}", response.getStatusCode());
    log.info("Response Headers : {}", response.getHeaders());

    return response;
  }
}

ClientHttpRequestInterceptor는 간단하게 intercept라는 메서드만 구현하면 된다. request 정보와 body, http요청을 실행할 ClientHttpRequestExecution를 파라미터로 갖고 Filter를 사용하듯이 전처리와 후처리를 진행해 주면 된다.

또한 해당 Interceptor에서 헤더와 같은 다른 정보들을 추가해줄 수도 있다.

 

위와 같은 인터셉터를 추가하고 실행해주면

예상한 대로 잘 나오는 것을 보 수가 있다.

 

 

다음 포스트에서는 지금까지 한 GET방식이 아닌 POST 방식의 RestTemplate사용을 알아보겠다.