[스프링부트핵심가이드] 12. 서버 간 통신

오늘내일·2023년 12월 6일
0

책 리뷰

목록 보기
11/11

12.1 RestTemplate이란?

RestTemplate은 스프링에서 HTTP 통신 기능을 손쉽게 사용하도록 설계된 템플릿이다. RestTemplate은 기본적으로 동기 방식으로 처리되며, 비동기 방식으로 사용하고 싶을 경우 AsyncRestTemplate을 사용하면 된다. RestTemplate은 현업에서 많이 사용되나 deprecated된 상태라서 WebClient 방식도 함께 알아두면 좋다.

  • RestTemplate 특징
    - HTTP 프로토콜의 메서드에 맞는 여러 메서드를 제공한다.
    • RESTful 형식을 갖춘 템플릿이다.
    • HTTP 요청 후 JSON, XML, 문자열 등의 다양한 형식으로 응답을 받을 수 있다.
    • 블로킹(blocking) I/O 기반의 동기 방식을 사용한다.
    • 다른 API를 호출할 때 HTTP 헤더에 다양한 값을 설정할 수 있다.

12.1.1 RestTemplate의 동작 원리

  1. 애플리케이션이 RestTemplate를 선언하고 URI와 HTTP 메서드, Body 등을 설정한다.
  2. 외부 API로 요청을 보낼 때는 RestTemplate에서 HttpMessageConvertor를 통해 RequestEntity를 요청 메시지로 변환한다.
  3. RestTemplate에서 변환된 요청 메시지를 ClientHttpRequestFactory를 통해 ClientHttpRequest로 가져온 후 외부 API로 요청을 보낸다.
  4. 외부에서 요청에 대한 응답을 받으면 RestTemplate은 ResponseErrorHandler로 오류를 확인하고, 오류가 있다면 ClientHttpRequestFactory를 통해 ClientHttpResponse에서 응답 데이터를 처리한다.
  5. 받은 응답 데이터가 정상적이라면 다시 한번 HttpMessageConvertor를 거쳐 자바 객체로 변환해서 애플리케이션으로 반환한다.

12.2 RestTemplate 사용하기

서버 용도 프로젝트 생성하고 하나의 컴퓨터에서 두 개의 프로젝트를 실행시켜야 하므로 포트를 변경해준다.

// 서버용도 프로젝트 컨트롤러
@RestController
@RequestMapping("/rest-template/test")
public class CrudController {

    @GetMapping
    public String getName() {
        return "통신 성공";
    }

    @GetMapping(value = "/{variable}")
    public String getVariable(@PathVariable String variable) {
        return variable;
    }

    @GetMapping("/param")
    public String getNameWithParam(@RequestParam String name) {
        return "통신 성공" + name + "!";
    }

    @PostMapping
    public ResponseEntity<PartnerDto> getPartner(
            @RequestBody PartnerDto request,
            @RequestParam String name,
            @RequestParam String email,
            @RequestParam String organization) {
        System.out.println(request.getName());
        System.out.println(request.getEmail());
        System.out.println(request.getOrganization());

        PartnerDto partnerDto = new PartnerDto();
        partnerDto.setName(name);
        partnerDto.setEmail(email);
        partnerDto.setOrganization(organization);

        return ResponseEntity.status(HttpStatus.OK).body(partnerDto);
    }

    @PostMapping(value = "/add-header")
    public ResponseEntity<PartnerDto> addHeader(
            @RequestHeader("my-header") String header,
            @RequestBody PartnerDto partnerDto) {
        System.out.println(header);
        return ResponseEntity.status(HttpStatus.OK).body(partnerDto);
    }
}
// 서버용도 프로젝트 PartnerDto
@Getter
@Setter
public class PartnerDto {

    private String name;
    private String email;
    private String organization;
}

12.2.2 RestTemplate 구현하기
RestTemplate을 사용하기 위해 보통 서비스 단에서 URI를 구성하여 사용한다. Body에 객체를 담아 호출을 할 경우에는 객체를 RestTemplate의 메서드에 파라미터로 넣어 호출할 수 있다. Header에 정보를 담아 호출할 경우에는 RequestEntity를 구성하여 RestTemplate의 exchange() 메서드를 사용하여 호출할 수 있다.

// RestTemplate service 구현 예제
@Service
public class RestTemplateService {
    // GET형식의 RestTemplate 작성
    public String getName() {
        URI uri = UriComponentsBuilder
                .fromUriString("http://localhost:9090")
                .path("/rest-template/test")
                .encode()   // 기본적으로 utf-8
                .build()
                .toUri();

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> responseEntity =
                restTemplate.getForEntity(uri, String.class);   // uri와 응답받는 타입

        return responseEntity.getBody();
    }

    public String getNameWithPathVariable() {
        URI uri = UriComponentsBuilder
                .fromUriString("http://localhost:9090")
                .path("/rest-template/test/{name}")
                .encode()
                .build()
                .expand("원빈")   // pathVariable에 넣을 값 : 값이 여러 개일 경우 , 로 구분
                .toUri();

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> responseEntity =
                restTemplate.getForEntity(uri, String.class);

        return responseEntity.getBody();
    }

    public String getNameWithParameter() {
        URI uri = UriComponentsBuilder
                .fromUriString("http://localhost:9090")
                .path("/rest-template/test/param")
                .queryParam("name", "원빈")   // 파라미터 전달
                .encode()
                .build()
                .toUri();

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> responseEntity =
                restTemplate.getForEntity(uri, String.class);

        return responseEntity.getBody();
    }

    // POST 형식의 RestTemplate 작성
    public ResponseEntity<PartnerDto> postWithParamAndBody() {
        URI uri = UriComponentsBuilder
                .fromUriString("http://localhost:9090")
                .path("/rest-template/test")
                .queryParam("name", "원빈")
                .queryParam("email", "aaa@aaaa.com")
                .queryParam("organization", "Happy")
                .encode()
                .build()
                .toUri();

        PartnerDto partnerDto = new PartnerDto();
        partnerDto.setName("강호동");
        partnerDto.setEmail("bbbb@bbbbb.com");
        partnerDto.setOrganization("Rich");

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<PartnerDto> responseEntity =
                restTemplate.postForEntity(uri, partnerDto, PartnerDto.class);

        return responseEntity;
    }

    public ResponseEntity<PartnerDto> postWithHeader() {
        URI uri = UriComponentsBuilder
                .fromUriString("http://localhost:9090")
                .path("/rest-template/test/add-header")
                .encode()
                .build()
                .toUri();

        PartnerDto partnerDto = new PartnerDto();
        partnerDto.setName("강호동");
        partnerDto.setEmail("bbbb@bbbbb.com");
        partnerDto.setOrganization("Rich");

        RequestEntity<PartnerDto> requestEntity = RequestEntity
                .post(uri)
                .header("my-header", "Happy API")
                .body(partnerDto);

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<PartnerDto> responseEntity =
                restTemplate.exchange(requestEntity, PartnerDto.class);

        return responseEntity;
    }
}
// RestTemplate 사용하기 위한 controller 작성 예제
@RestController
@RequiredArgsConstructor
@RequestMapping("/rest-template")
public class RestTemplateController {

    private final RestTemplateService restTemplateService;

    @GetMapping
    public String getName() {
        return restTemplateService.getName();
    }

    @GetMapping("/path-variable")
    public String getNameWithPathVariable() {
        return restTemplateService.getNameWithPathVariable();
    }

    @GetMapping("/parameter")
    public String getNameWithParameter() {
        return restTemplateService.getNameWithParameter();
    }

    @PostMapping
    public ResponseEntity<PartnerDto> postDto() {
        return restTemplateService.postWithParamAndBody();
    }

    @PostMapping("/header")
    public ResponseEntity<PartnerDto> postWithHeader() {
        return restTemplateService.postWithHeader();
    }
}

참고 : spring boot 3.x.x 부터는 swagger 적용을 위해 아래의 의존성을 주입해야 한다.

implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'

12.2.3 RestTemplate 커스텀 설정

RestTemplate은 HttpClient를 추상화하고 있다. RestTemplate은 커넥션 풀을 지원하지 않는다. 커넥션 풀이란 미리 connection을 해놓은 객체들을 pool에 저장해두었다가, 클라이언트 요청이 오면 connection 객체를 빌려주고 처리가 끝나면 다시 connection 객체를 반납받아 pool에 저장하는 방식을 말한다. 커넥션 풀을 지원하지 않으면 매번 호출할 때 마다 포트를 열어 커넥션을 생성하게 되는데, TIME_WAIT 상태가 된 소켓*을 다시 사용하려고 접근한다면 재사용하지 못하게 된다.

  • 소켓 : 소켓은 떨어져 있는 두 호스트를 연결해주는 도구로 인터페이스의 역할을 하는 소프트웨어적인 개념인데, 데이터를 주고 받을 수 있는 구조체로 소켓을 통해 데이터 통로가 만들어 진다. 소켓은 프로토콜, IP 주소, 포트 넘버로 정의된다.

RestTemplate에서 커넥션 풀 기능을 활성화하는 대표적인 방법은 아파치에서 제공하는 HttpClient로 대체해서 사용하는 방식이다. 아파치 HttpClient를 사용하려면 아래와 같이 의존성을 주입한다. spring boot 3.x.x 에서는 아래와 같이 httpclient5를 사용해야 한다.

    implementation 'org.apache.httpcomponents.client5:httpclient5-win:5.2.1'

아래와 같이 커스텀 RestTemplate 객체를 생성해 줄 수 있다.

// 커스텀 RestTemplate 객체 생성 메서드
public RestTemplate restTemplate() {
        HttpComponentsClientHttpRequestFactory factory =
                new HttpComponentsClientHttpRequestFactory();

        PoolingHttpClientConnectionManager connectionManager =
                PoolingHttpClientConnectionManagerBuilder.create()
                        .setMaxConnTotal(500)
                        .setMaxConnPerRoute(500)
                        .build();

//        HttpClient client = HttpClientBuilder.create()
//                .setConnectionManager(connectionManager)
//                .build();

        CloseableHttpClient httpClient = HttpClients.custom()
                .setConnectionManager(connectionManager)
                .build();

        factory.setHttpClient(httpClient);
        factory.setConnectTimeout(2000);
//        factory.setReadTimeout(5000);

        return new RestTemplate(factory);
    }

spring boot 3.x.x 부터 httpclient5를 사용하는데 위에서 RequestFactory에서 setReadTimeout() 메서드가 사용할 수 없게 되었다. 이를 위한 대응방법을 아직 찾지 못 해서, 좀 더 공부가 필요할 듯 하다.

12.3 WebClient란?

실제 운영환경에서는 RestTemplate을 많이 사용하고 있다. 하지만 최신 버전에서는 RestTemplate이 지원 중단되어 WebClient를 사용할 것을 권고하고 있다.

Spring WebFlux는 HTTP 요청을 수행하는 클라이언트로 WebClient를 제공한다. WebClient는 리액터(Reactor)* 기반으로 동작하는 API다.

  • WebClient의 특징
    - 논블로킹(Non-Blocking) I/O를 지원한다.
    • 리액티브 스트림(Reactive Streams)의 백 프레셔(Back Pressure)를 지원한다.
    • 적은 하드웨어 리소스로 동시성을 지원한다.
    • 함수형 API를 지원한다.
    • 동기, 비동기 상호작용을 지원한다.
    • 스트리밍을 지원한다.
  • 리액터(Reactor) : spring 프레임워크에서 '리액티브 프로그래밍*'에서 사용되는 디자인 패턴 또는 프로그램 아키텍처이다.
  • 리액티브 프로그래밍(Reactive Programming) : 데이터 스트림 또는 시퀀스를 처리하는 프로그래밍 패러다임 중 하나로, 주로 비동기적인 이벤트 기반 시스템에서 사용되며, 데이터 스트림의 변화에 대한 반응적이고 효율적인 처리를 목적으로 한다.
  • 리액티브 스트림(Reactive Streams) : 비동기적이고 논블로킹 방식으로 데이터 스트림을 처리하는 표준화된 API이다.
  • 백프레셔(Back Pressure) : 데이터를 소비하는 쪽이 데이터를 생산하는 쪽으로부터의 피드백을 통해 처리 속도를 제어하는데, 이를 백프레셔라 한다.

12.3.1 WebClient 구성
WebClient를 사용하려면 WebFlux 모듈에 대한 의존성을 아래와 같이 추가해야 한다.

    implementation 'org.springframework.boot:spring-boot-starter-webflux'

12.4 WebClient 사용하기

12.4.1 WebClient 구현

// WebClient를 활용한 GET 요청 예제
@Service
public class WebClientService {
    
    public String getName() {
        WebClient webClient = WebClient.builder()
                .baseUrl("http://localhost:9090")
                .defaultHeader(HttpHeaders.CONTENT_TYPE, 
                        MediaType.APPLICATION_JSON_VALUE)
                .build();
        
        // WebClient는 get(), post(), put(), delete() 등 명확하게 HTTP 메서드를 설정할 수 있다.
        return webClient.get()	
                .uri("/rest-template/test")
                .retrieve()		// 요청에 대한 응답을 받았을 대 그 값을 추출하는 방법 중 하나이다.
                .bodyToMono(String.class)	// 리턴타입을 설정해서 문자열 객체를 받아오게 함
                .block();	// WebClinet는 논블로킹 방식으로 동작하기 때문에 블로킹 구조로 별도 설정함.
    }
    
    public String getNameWithPathVariable() {
        WebClient webClient = WebClient.create("http://localhost:9090");

        ResponseEntity<String> responseEntity = webClient.get()
                .uri(uriBuilder -> uriBuilder.path("rest-template/test/{name}")
                        .build("원빈"))
                .retrieve().toEntity(String.class).block();

        assert responseEntity != null;
        return responseEntity.getBody();
    }
    
    public String getNameWithParameter() {
        WebClient webClient = WebClient.create("http://localhost:9090");
        
        return webClient.get()
                .uri(uriBuilder -> uriBuilder.path("rest-template/test")
                        .queryParam("name", "원빈").build())
                .exchangeToMono(clientResponse -> {
                    if (clientResponse.statusCode().equals(HttpStatus.OK)) {
                        return clientResponse.bodyToMono(String.class);
                    } else {
                        return clientResponse.createException().flatMap(Mono::error);
                    }
                })
                .block();
    }
}

일반적으로 WebClient 객체를 이용할 때는 WebClient 객체를 생성한 후 재사용하는 방식으로 구현하는 것이 좋다. builder()를 사용할 경우 확장할 수 있는 메서드는 다음과 같다.

  • defaultHeader() : WebClient의 기본 헤더 설정
  • defaultCookie() : WebClient의 기본 쿠키 설정
  • defaultUriVariable() : WebClient의 기본 URI 확장값 설정
  • filter() : WebClient에서 발생하는 요청에 대한 필터 설정

일단 빌드된 WebClient는 변경할 수 없지만, 다음과 같이 복사해서 사용할 수는 있다.

WebClient webClient = WebClient.create("http://localhost:9090")
WebClient clone = webClient.mutate().build();

Mono와 Flux는 리액터의 핵심 타입으로 둘 다 리액티브 스트림의 Publisher 인터페이스를 구현한 것이다.

  • Mono : 하나의 데이터 항목만 갖는 타입
  • Flux : 0, 1 또는 다수의 데이터를 갖는 타입
  // WebClient를 활용한 POST 요청 예제
  	public ResponseEntity<PartnerDto> postWithParamAndBody() {
        WebClient webClient = WebClient.builder()
                .baseUrl("http://localhost:9090")
                .defaultHeader(HttpHeaders.CONTENT_TYPE,
                        MediaType.APPLICATION_JSON_VALUE)
                .build();

        PartnerDto partnerDto = PartnerDto.builder()
                .name("강호동!")
                .email("aaaa@aaaaa.com")
                .organization("행복한 ~")
                .build();

        return webClient.post()
                .uri(uriBuilder -> uriBuilder.path("rest-template/test")
                        .queryParam("name", "원빈")
                        .queryParam("email", "bbbb@bbb.com")
                        .queryParam("organization", "재미있는").build())
                .bodyValue(partnerDto)
                .retrieve()
                .toEntity(PartnerDto.class)
                .block();
    }

    public ResponseEntity<PartnerDto> postWithHeader() {
        WebClient webClient = WebClient.builder()
                .baseUrl("http://localhost:9090")
                .defaultHeader(HttpHeaders.CONTENT_TYPE,
                        MediaType.APPLICATION_JSON_VALUE)
                .build();

        PartnerDto partnerDto = PartnerDto.builder()
                .name("강호동!")
                .email("aaaa@aaaaa.com")
                .organization("행복한 ~")
                .build();

        return webClient.post()
                .uri(uriBuilder -> uriBuilder.path("rest-template/test/add-header")
                        .build())
                .bodyValue(partnerDto)
                .header("my-header", "TEST API")
                .retrieve()
                .toEntity(PartnerDto.class)
                .block();
    }
            
profile
다시 시작합니다.

0개의 댓글