오늘은 서버 간 통신에 관한 이야기이다. 이번에 캡스톤 프로젝트를 진행하며 Python 서버와 Spring 서버 간 통신을 해야할 일들이 많이 생길 것 같은데 이와 관련된 내용이라 주의 깊게 살펴 보았다. Spring 에서는 어떻게 서버 간 통신을 하는지 알아보자.
RestTemplate은 스프링에서 HTTP 통신 기능을 손쉽게 사용하도록 설계된 템플릿이다.
애플리케이션은 우리가 직접 작성하는 애플리케이션 코드 구현부이다.
외부 API로 요청을 보내게 되면 RestTemplate에서 HttpMessageConverter
를 통해 RequestEntity
를 요청 메세지로 변환한다.
RestTemplate에서는 변환된 메세지를 ClientHttpRequestFactory
를 통해 ClientHttpRequest
로 가져온 후 외부 API로 요청을 보낸다.
외부에서 응답을 받으면 RestTemplate은 ResponseErrorHandler
로 오류를 확인하고, 오류가 있다면 ClientHttpResponse
에서 응답 데이터를 처리한다.
받은 응답 데이터가 정상이라면, 다시 한번 HttpMessageConverter
를 거쳐 자바 객체로 변환해 애플리케이션으로 반환한다.
분량 상 요청을 받는 서버는 구현되어 있다고 가정하고 RestTemplate을 이용하여 요청을 보내는 서버 위주로 설명하겠다.
RestTemplate은 별도의 유틸리티 클래스로 생성하거나 서비스 또는 비즈니스 계층에 구현된다.
위 그림은 RestTemplate이 어떻게 다른 서버로 요청을 보내는지에 대한 간단한 도식화이다.
@Service
public class RestTemplateService {
//별도의 파라미터 없이 GET 요청
public String getName() {
URI uri = UriComponentsBuilder
.formUriString("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();
}
//PathVariable을 통한 GET 요청
public String getNameWithPathVariable() {
URI uir = UriComponentsBuilder
.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();
}
//queryParam을 이용한 GET 요청
public String getNameWithParameter() {
URI uri = UriComponentsBuilder
.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();
}
}
UriComponentsBuilder
는 Builder 형식으로 객체를 생성한다.
fromUriString()
에서는 호출부의 URL을 입력한다.
path()
에서는 세부 경로를 입력한다.
encode()
에서는 인자를 전달하지 않으면 기본적으로 UTF-8로 설정된다.
build()
메서드를 통해 빌더 생성을 종료하면 UriComponents
타입이 리턴되는데, toUri()
메서드를 통해 URI 타입으로 리턴 받는다.
생성된 Uri는 getForEntity()
에 파라미터로 전달된다. getForEntity()
는 URI와 응답 받는 타입을 매개변수로 한다.
즉, UriComponentsBuilder
를 통해 어디로 어떤 데이터를 요청할 것인지 정하고, getForEntity()
로 요청을 보낸다. getForEntity()
이외에도 여러 메서드들이 존재한다.
//POST 형식의 Body와 파라미터 값을 담는 메서드
public ResponseEntity<MemberDto> postWithParamAndBody() {
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:9090")
.path("/api/v1/crud-api")
.queryParam("name", "Flature") //파라미터 추가
.queryParam("email", "flature@wikibooks.co.kr")
.queryParam("organization", "Wikibooks")
.encode()
.build()
.toUri();
MemberDto memberDto = new MemberDto();
memberDto.setName("falture!");
memberDto.setEmail("flature@gmail.com");
memberDto.setOrganization("Around Hub Studio");
RestTemplate restTemplate = new RestTemplate();
//RequestBody에 값 담기 - PostForEntity 사용 시, 파라미터에 데이터를 넣으면 된다.
ResponseEntity<MemberDto> responseEntity = restTemplate.postForEntity(uri, memberDto, MemberDto.class);
return responseEntity;
}
//헤더를 추가하여 POST 형식의 요청 보내기
public ResponseEntity<MemberDto> postWithHeader() {
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:9090")
.path("/api/v1/crud-api/add-header")
.encode()
.build()
.toUri();
MemberDto memberDto = new MemberDto();
memberDto.setName("falture!");
memberDto.setEmail("flature@gmail.com");
memberDto.setOrganization("Around Hub Studio");
RequestEntity<MemberDto> requestEntity = RequestEntity
.post(uri) //POST 형식으로 지정
.header("my-header", "Wikibooks API") //헤더를 추가 - 예) 토큰 값을 넣어 보낼 수 있음
.body(memberDto);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<MemberDto> responseEntity = restTemplate.exchange(requestEntity, MemberDto.class);
return responseEntity;
}
postWithHeader()
메서드에서 exchange()
메서드는 모든 형식의 HTTP 요청을 생성할 수 있다.
RequestEntity
설정에서 post()
대신 다른 형식의 메서드로 정의하면 exchange()
메서드로 쉽게 사용 가능하다.RestTemplate은 HTTPClient
를 추상화하고 있다. HttpClient
의 종류에 따라 기능에 차이가 있는데, 가장 큰 차이는 커넥션 풀(Connection Pool)이다.
RestTemplate은 기본적으로 커넥션 풀을 지원하지 않는다. 따라서 매번 호출할 때마다 포트를 열어 커넥션을 생성하게 되는데, TIME_WAIT
상태가 된 소켓을 다시 사용하려 접근하면 재사용이 불가능하다.
따라서 이 기능을 활성화 하는 방법은 Apache에서 제공하는 HttpClient
로 대체해서 사용하는 방식이다.
아래는 커스텀 RestTemplate 객체를 생성해주는 메서드이다.
public RestTemplate restTemplate() {
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
//동시에 처리 가능한 연결 수 설정
HttpClient client = HttpClientBuilder.create()
.setMaxConnTotal(500) //최대 연결 수 500으로 지정
.setMaxConnPerRoute(500) //경로 당 연결 수 500으로 지정
.build();
CloseableHttpClient httpClient = HttpClients.custom()
.setMaxConnTotal(500)
.setMaxConnPerRoute(500)
.build();
factory.setHttpClient(httpClient); //생성한 httpClient 지정
factory.setConnectTimeout(2000); //연결 시도 시 Timeout 시간을 2000ms 로 지정
factory.setReadTimeout(5000); //데이터 읽기 시 Timeout 시간을 5000ms 로 지정
RestTemplate restTemplate = new RestTemplate(factory);
return restTemplate;
}
위에서는 HttpClient
, CloseableHttpClient
두개를 생성하고 CloseableHttpClient
를 등록하였다. CloseableHttpClient
는 HttpClient
인터페이스를 확장하는 추상 클래스여서 둘 중 어떤 것을 등록하더라도 상관 없다.
하지만 CloseableHttpClient
는 HttpClient
의 모든 기능을 포함하지만 추가적으로 close()
를 통한 안전한 연결 해제가 가능하다.
이후 책에서는 WebClient에 대해서 다룬다. 왜냐하면 RestTemplate은 지원이 중단된 상태여서 WebClient 사용을 지향하기 때문이다. 하지만 많은 레퍼런스가 RestTemplate을 사용 중이고 WebClient의 동작 방식도 RestTemplate과 유사하여 RestTemplate의 동작 원리를 잘 알면 WebClient를 사용하는 것은 어렵지 않다고 생각한다.