본문 바로가기
ZB 백엔드 스쿨/블로그 과제

스프링 핵심가이드) 북스터디 7주차 : 12장 서버 간 통신

by son_i 2023. 11. 5.
728x90

12장 서버 간 통신

 

최근 개발 서비스들은 마이크로 서비스 아키텍처 (MSA)를 주로 채택하고 있음.

MSA는 애플리케이션이 가지고있는 기능(서비스)이 하나의 비즈니스 범위만 가지는 형태.

 

각 애플리케이션은 자신이 가진 기능을 API로 외부에 노출하고,

다른 서버가 그러한 API를 호출해서 사용할 수 있게 구성되므로 각 서버가 다른 서버의 클라이언트가 되는 경우도 많음.

 

다른 서버로 웹 요청을 보내고 응답을 받을 수 있게 도와주는 RestTemplate과 WebClient에 대해 기술.

 

12.1 RestTemplate이란 ? 

- RestTemplate : 스프링에서 HTTP 통신 기능을 손쉽게 사용하도록 설계된 템플릿.

  HTTP 서버와의 통신을 단순화한 이 템플릿 이용시 RESTful 원칙을 따르는 서비스를 편리하게 만들 수 있음.

  RestTemplate은 기본적으로 동기 방식으로 처리.

  비동기 방식으로 사용하고 싶을 경우 AsncRestTemplate을 사용하면 됨.

  * RestTemplate은 현업에서 많이 쓰이나 지원 중단된 상태라 WebCliedt 방식을 함께 알아두기

 

- RestTemplate의 특징 :

  · HTTP 프로토콜의 메서드에 맞는 여러 메서드 제공.

  · RESTful 형식을 갖춘 템플릿

  · HTTP 요청 후 JSON, XML, 문자열 등의 다양한 형식으로 응답을 받을 수 있음.

  · 블로킹 I/O 기반의 동기 방식 사용.

  · 다른 API 호출 시 HTTP 헤더에 다양한 값을 설정할 수 있음.

 

12.1.1 RestTemplate의 동작 원리

RestTemplate의 동작의 도식화

1. 여기서 애플리케이션은 우리가 직접 작성하는 애플리케이션 코드 구현부 의미.

애플리케이션에서는 RestTemplate을 선언하고 URI와 HTTP 메서드, Body등을 설정.

 

2. 외부 API로 요청을 보내게 되면 RestTemplate에서 HttpMessageConverter를 통해 RequestEntity를 요청 메세지로 변환.

 

3. RestTemplate에서는 변환된 요청 메세지를 ClientHttpRequestFactory를 통해 ClientHttpRequest로 가져온 후 외부 API로 요청을 보냄.

 

4. 외부에서 요청에 대한 응답을 받으면 RestTemplate은 ResponseErrorHandler로 오류를 확인하고, 오류가 있다면 ClientHttpResponse에서 응답 데이터 처리.

 

5. 받은 응답 데이터가 정상이라면 다시한 번 HttpMessageConverter를 거쳐 자바 객체로 변환해서 애플리케이션으로 반환됨.

 

12.1.2 RestTemplate의 대표적인 메서드

메서드 HTTP 형태 설명
getForObject GET GET 형식으로 요청한 결과를 객체로 반환
getForEntity GET GET 형식으로 요청한 결과를 ResponseEntity 결과로 반환
postForLocation POST POST 형식으로 요청한 결과를 헤더에 저장된 URI로 반환
postForObject POST POST 형식으로 요청한 결과를 객체로 반환
postForEntity POST POST 형식으로 요청한 결과를 ResponseEntity 결과로 반환
delete DELETE DELETE 형식으로 요청
put PUT PUT 형식으로 요청
patchForObject PATCH PATCH 형식으로 요청한 결과를 객체로 반환
optionsForAllow OPTIONS 해당 URI에서 지원하는 HTTP 메서드를 조회
exchange any HTTP 헤더를 임의로 추가할 수 있고, 어떤 메서드 형식에서도 사용할 수 있음.
execute any 요청과 응답에 대한 콜백을 수정

 


12.2 RestTemplate 사용하기

요청을 보낼 서버 용도로 별도의 프로젝트를 하나 생성,

다른 프로젝트에서 RestTemplate을 통해 요청을 보내는 방식으로 실습.

 

12.2.1 서버 프로젝트 생성하기

먼저  RestTemplate의 동작을 확인하기 위해 서버 용도의 프로젝트 생성.

한 컴퓨터 안에서 두 개의 프로젝트 동작 시키기 위해 포트 변경.

 

컨트롤러에 GET과 POST 메서드 형식의 요청을 받기 위한 코드 구성.

@RestController
@RequestMapping("/api/v1/crud-api")
public class CrudController {
	@GetMapping
    public String getName() {
    	return "Flature";
    }
    
    @GetMapping(value = "/{variable}")
    public String getVariable(@PathVariable String variable) {
    	return variable;
	}
    
    @GetMapping("/param")
    public String getNameWithParam(@RequestParam String name) {
    	return "Hello. " + name + "!";
    }
    
    @PostMapping
    public ResponseEntity<MemeberDto> getMember (
    	@RequestBody MemberDto reqeust,
        @RequestParam String name,
        @RequestParam String email,
        @RequestParam String organization
    ) {
    	sout(request.getName());
    	sout(request.getEmail());
    	sout(reqeust.getOrganization());
    	
        MemeberDto memberDto = new MemberDto();
        memberDto.setName(name);
        memberDto.setEmail(email);
        memberDto.setOrganization(organization);
        
        return ResponseEntity.status(HttpStatus.OK).body(memberDto);
    }
    
    @PostMapping(value = "/add-header")
    public ResponseEntity<MemberDto> addHeader(@RequestHeader("my-header") String header,
    	@RequestBody MemberDto memberDto) {
        
        sout(header);
        
        return ResponseEntity.status(HttpStatus.OK).body(memberDto);
    }
}

 

12.2.2 RestTemplate 구현하기

일반적으로 RestTemplate은 별도의 유틸리티 클래스로 생성하거나 서비스 또는 비즈니스 계층에 구현됨.

앞서 생성한 서버 프로젝트에 요청을 날리기 ㅜ이해 서버의 역할을 수행하면서 다른 서버로 요청을 보내는 클라이언트의 역할도 수행하는 새로운 프로젝트 생성.

 도식화

위 그림에서 클라이언트는 서버를 대상으로 요청을 보내고 응답을 받는 역할을 하고, 12.2.1에서 구현한 서버 프ㅗ젝트는 서버2가 됨.

 

RestTemplate을 포함하는 프로젝트 생성

 

- GET 형식의 RestTemplate 작성

@Service
public class RestTemplateService {
	public String getName() {
    	URI uri = UriConponentBuilder
        	.fromUriString("http://localhost:9090")
            .path("/api/v1/crud-api")
            .encode()
            .build()
            .toUri();
            
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
        
        return responseEntity.getBody();
    }
    
    public String getNameWithPathVariable() {
    	URI uri = UriConponentBuilder
        	.fromUriString("http://localhost:9090")
            .path("/api/v1/crud-api/{name}")
            .encode()
            .build()
            .expand("Flature") //복수의 값을 넣어야 하는 경우 ,를 추가하여 구분
            .toUri();
            
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
        
        return responseEntity.getBody();
    }

    public String getNameWithPathVariable() {
    	URI uri = UriConponentBuilder
        	.fromUriString("http://localhost:9090")
            .path("/api/v1/crud-api/param")
            .queryParam("name", "Flature")
            .encode()
            .build()
            .toUri();
            
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
        
        return responseEntity.getBody();
    }
}

 

RestTemplate을 생성하고 사용하는 방법은 다양함.

가장 보편적인 방법 : UriComponentsBuilder를 사용하는 방법.

 

UriComponentsBuilder는 스프링 프레임워크에서 제공하는 클래스로서 여러 파라미터를 연결해서 URI 형식으로 만드는 기능 수행.

 

12.2.3 RestTemplate 커스텀 설정

RestTemplate은 HTTPClient를 추상화하고 있음. HttpClient의 종류에 따라 기능에 차이가 다소 있는데 가장 큰 차이는 커넥션 풀임.

 

RestTemplate은 기본적으로 커넥션 풀을 지원하지 않음.

이 기능을 지원하지 않으면 매번 호출할 때마다 포트를 열어 커넥션을 생성하게 되는데, TIME_WAIT 상태가 된 소켓을 다시 사용하려고 접근한다면 재사용하지 못 하게 됨.

-> 방지하기 위해 커넥션 풀 기능을 활성화해서 재사용할 수 있게 하는 것이 좋음.

 

이 기능을 활성화하는 대표적인 방법은 아파치에서 제공하는 httpClient로 대체해서 사용하는 방식

  *의존성을 추가해야함.

 


12.3 WebClient란 ?

일반적으로 실제 운영환경에 적용되는 애플리케이션은 정식 버전으로 출시된 스프링 부트의 버전보다 낮은 경우가 많음. 그래서 RestTemplate을 많이 사용하고 있으나 최신버전에서는 RestTemplate이 지원 중단되어 WebClient를 사용 권장.

 

Spring WebFlus는 HTTP 요청을 수행하는 클라이언트로 WebClient를 제공.

WebClient는 리액터 기반으로 동작하는 API.

리액터 기반이므로 스레드와 동시성 문제를 벗어나 비동기 형식으로 사용 가능.

 

- WebClient의 특징

· 논블로킹 I/O 지원

· 리액티브 스트림의 백 프레셔를 지원

· 적은 하드웨어 리소스로 동시성을 지원

· 함수형 API를 지원

· 동기, 비동기 상호작용을 지원

· 스트리밍 지원

 

12.3.1 WebClient 구성

WebClient를 사용하려면 WebFlux 모듈에 대한 의존성을 추가해야함.

 

WebFlux는 클라이언트와 서버 간 리액티브 애플리케이션 개발을 지원하기 위해 스프링 프레임워크 5에서 새롭게 추가된 모듈. 

 


12.4 WebClient 사용하기

12.4.1 WebClient 구현

구현하는 방법 2가지

 1. create() 메서드를 이용한 생성

 2. builder()를 이용한 생성.

 

- WebClient 생성

@Service
public class WebClient {
	public String getName() {
    	WebClient webClient = WebClient.builder()
        	.baseUrl("http://localhost:9090")
            .defaultHeader(HttpHeader.CONTENT_TYPE, MediaType.APPLICATION_JOWN_VALUE)
            .build();
        
        return webClient.get()
        	.uri("/api/v1/crud-api")
            .retrieve()
            .bodyToMono(String.class)
            .block();
    }
    
    public String getNameWithPathVariable() {
    	WebClient webClient = WebClient.create("http://localhost:9090");
        
        ResponseEntity<String> responseEntity = webClient.get();
        	.uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api/{name}")
            	.build("Flature"))
            .retrieve().toEntity(String.class).block();
            
        return responseEntity.getBody();
    }
    
    public String getNameWithParameter() {
    	WebClient webClient = WebClient.create("http://localhost:9090");
        
        return webClient.get().uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api")
        	.queryParam("name", "Flature")
            .build())
          .exchangeToMono(clientResponse -> {
          	if (clientResponse.statusCode(0.equals(HttpStatus.OK)) {
            	return clientResponse.bodyToMono(String.class);
            } else {
            	return ClientResponse.createException().flatMap(Mono::error);
            }
          })
          .block();
    }
}

4~15번 줄의 getName() 메서드는 builder()를 활용해 WebClient 를 만들고 다른 두 개의 메서드에서는 create() 를 활용해 WebClient 를 생성.

 

WebClient 는 우선 객체를 생성한 후 요청을 전달하는 방식으로 동작.

5~8 줄을 보면 builder()를 통해 baseUrl() 메서드에서 기본 URL을 설정하고 defaultHeader() 메서드로 헤더의 값을 설정.

일반적으로 WebClient 객체를 이용할 때는 이처럼 WebClient 객체를 생성한 후 재사용하는 방식으로 구현하는 것이 좋음.

 

- 예제에서 소개된 메서드 외 builder()를 사용할 경우 활장할 수 있는 메서드

  defaultHeader() : WebClient의 기본 헤더 설정

  defaultCookie() : WebClient의 기본 쿠키 설정

  defaultUriVariable : WebClient의 기본 URI 확장 값 설정

  filter() : WebClient에서 발생하는 요청에 대한 필터 설정

 

빌드된 WebClient는 변경할 수 없음. 다만 복사해서 사용할 수는 있음.

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

 

 

위위 코드에서 WebClient 는 HTTP 메서드를 get(), post(), put(), delete() 등의 네이밍이 명확한 메서드로 설정할 수 있음. 

URI를 확장하는 방법으로 uri() 메서드 사용할 수 있음.

 

retrieve() 메서드는 요청에 대한 응답을 받았을 때 그 값을 추출하는 방법 중 하나. retrieve() 는 bodyToMono() 메서드를 통해 리턴 타입을 설정해서 문자열 객체를 받아오게 되어있음.

  * Mono는 리액티브 스트림에 대한 선행 학습이 필요한 개념이며 Flux와 비교되는 개념.

   Flux와 Mono는 리액티브 스트림에서 데이터를 제공하는 발행자 역할을 수행하는 publisher의 구현체.

 

WebClient 는 기본적으로 논블로킹 방식으로 동작하기 때문에 기존에 사용하던 코드의 구조를 블로키 구조로 바꿔 줄 필요가 있음.

예제에서는 14번 째 줄에 block()메서드를 추가해서 블로킹 형식으로 동작하게끔 설정.

 

17~26번에 getnameWithPathVariable() 메서드는 pathVariable 값을 추가해 요청을 보내는 예제.

21번 줄처럼 uri() 메서드 내부에서 uriBuilder를 사용해 path를 설정하고 build() 메서드에 추가할 값을 넣는 것으로 pathVariable을 추가할 수 있음. 좀 더 간략한 작성도 가능

 

17~26 번 줄은 bodyToMono()가 아닌 toEntity()를 사용하는 예제.

toEntity()를 사용하면 ResponseEntity 타입으로 응답을 전달 받을 수 있음.

 

28~42번 줄의 getNameWithParameter() 메서드는 쿼리 파라미터를 함께 전달하는 방법을 제시.

쿼리 파라미터를 요청에 담기 위해서는 31번 줄과 같이 uriBuilder를 사용하며, queryParam() 메서드를 사용해 전달하려는 값을 설정. 예제에서는 retrieve() 대신 exchange() 메서드를 사용함.

exchange() 메서드는 지원 중단됐기 때문에 exchangeToMono() 또는 exchangeToFlux()를 사용해야 함.

exchange() 메서드는 응답 결과 코드에 따라 다르게 응답을 설정할 수 있음.

 


12.5 정리

웹 통신을 위해 RestTemplate과 WebClient를 사용하는 방법 정리.

실무에서 다른 서버의 리소스에 접근하는 상황은 자주 발생함.

이러한 겨웅 대체로 이번 장의 통신 모듈 이용해 기능 구현하면 해결됨.

 

책에서 소개한 방법 익힌 후 통신하는 횟수나, 접근하는 서버의 특성에 맞게 커넥션 풀이나 타임아웃 등의 설정을 최적화하는 작업으로 심화 학습 진행.