[Spring] RestTemplate - Java에서 HTTP 통신
[2021.12.15]
데이터베이스에 저장된 데이터를 기반으로 자동으로 http 통신을하는 기능을 구현하였다.
아직 프로젝트 기획 전이라서 해당 기능에 대항 정확한 서비스가 명시되지 않았지만 기본 플로우를 구성해 구현해두었다.
RestTemplate을 이용해 구현하였고, 정말 쉬었다! 따로 점검을 받으면서 구현한 것이 아니기 때문에 미흡한 점은 있을 것이다. 차주에 개발한 것들 점검하신다고 했는데 나는 이런 점검이 너무 좋다.. (부족한점 알고 고칠수가 있거든.. 하지만 언제까지 누군가 알려주고 고치고 이런걸 반복하면 안되겠지..)
개발환경
- Spring Boot
- Java 1.8
- gradle
1. RestTemplate
org.springframework.web.client.RestTemplate
Synchronous client to perform HTTP requests, exposing a simple, template method API over underlying HTTP client libraries such as the JDK HttpURLConnection, Apache HttpComponents, and others.
The RestTemplate offers templates for common scenarios by HTTP method, in addition to the generalized exchange and execute methods that support of less frequent cases.
Http 요청을 수행하는 동기 클라이언트는 기본 HTTP 클라이언트 라이브러리를 통해 간단한 템플릿 메소드 API를 제공한다.
RestTemplate는 HTTP 메소드를 통해 일반적인 시나리오로 템플릿을 제공하고, execute와 exchange메소드도 제공한다.
특징
- spring의 HTTP 통신 템플릿이다.
- RESTFul 형식에 맞춤
- json, xml을 쉽게 응답 받을 수 있도록 해준다
- ResponseEntity와 Server to Server 통신하는데 자주 쓰인다.
- Http request를 지원한느HttpClient를 사용한다.
동작 원리
- Application이 RestTemplate를 생성하고 URI, Method, Header를 담아 요청한다.
- RestTemplate는 HttpMessageConverter를 사용하여 요청 메시지를 RequestEntity로 변환한다.
- RestTemplate는 ClientHttpRequestRactory로 부터 ClientHttpRequest를 만든다.
- ClientHttpRequest는 요청메세지를 만들어 HTTP 프로토콜을 통해 대상 서버와 통신한다.
- RestTemplate은 ResponseErrorHandler로 오류를 확인한다.
- ResponseErrorHandler에서 오류가 존재한다면 ClientHttpResponse에서 응답데이터를 가져와서 HttpClientErrorException로 예외처리는 내보낸다. (HttpClientErrorException은 응답코드가 2xx이 아닌경우이다.)
- RestTemplate은 HttpMessageConverter를 통해 응답메시지를 Object(Class responseType)으로 변환한다.
- 변환한 응답데이터를 Application에 반환한다.
주요 메소드
RestTemplte이란 친구는 간략히 Spring에서 지원하는 RESTful 기반 HTTP 통신 템플릿인 것이다.
RestTemplte을 통해 개발자는 반복적인 코드를 줄여 간단한 방법으로 요청을 보내고 json,xml등의 응답 데이터를 쉽게 받을 수 있게 된다.
RestTemplate으로 request를 담고, 요청을 보낸때 다양한 방법과 메소드가 있는데 여기서 나는 RequestEntity로 요청 데이터를 담고 exchange 메소드로 구현했다.
2. RestTemplate 사용
구현한 로직중에 Exception은 실제 커스텀해서 처리하였다. 소스를 올리기위해 그냥 Exception으로 수정한 것이다.
2-1. gradle 추가
RestTemplate은 spring-boot-starter-web에 포함되어 있다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}
2-2. Domain
나는 request 정보를 데이터베이스에서 가져와서 구성할 것이므로, 아래와 같은 domain이 필요하다.
TbConn.java
Http 요청 대상 서버의 정보가 담겨있다.
public class TbConn
{
private Long connSid;
private String connNm;
private PROTOCOL prtlCd;
private String urlPrefix;
private String ipAddr;
private String port;
private Boolean delYn;
}
TbHttp.java
RestTemplate으로 보낼 요청 데이터가 담겨있고, 응답데이터에서 특정경로 및 특정값을 통해 성공/에러 여부를 판단할 수 있다.
public class TbHttp
{
private Long httpSid;
private Long setSid;
private String httpNm;
private Integer seq;
private String uri;
private METHOD methodCd;
private Integer tmoutSecond;
private String sucesPath;
private String sucesPathVal;
private String errPath;
private Boolean endYn;
private Integer retryCnt;
private Boolean errContinue;
private Long sucesHttpSid;
private Long errHttpSid;
private Boolean delYn;
}
TbHttpParam.java
요청 파라미터를 얻을 수 있는 도메인이다. TbHttpParam에는 path param / query param / request body가 트리형태로 담겨있다.
public class TbHttpParam
{
private Long paramSid;
private Long httpSid;
private PARAM_TYPE paramTypeCd;
private Long paramPsid;
private String key;
private String val;
private DATA_TYPE dataTypeCd;
private Boolean delYn;
}
2-3. Util
요청 파라미터를 만들고, 응답데이터에서 성공/에러 여부를 판단할때 사용하는 Util을 따로 구현했다.
HttpUtil : 요청 파라미터를 만드는 유틸리티
2021.12.19 - [IT story/JAVA] - [JAVA] HttpUtil
ObjectUtil : Object에서 특정 경로의 특정 값 추출 및 존재 여부 판단하는 유틸리티
2021.12.19 - [IT story/JAVA] - [JAVA] ObjectUtil
2-4. HttpCallHandler
HttpCallHandler는 RestTemplate의 통신만을 담당하도록 내가 구현한 클래스이다.
setRequestInfo(TbConn, TbHttp) : RequestEntity를 생성하는 메소드
/* **************************
* REQUEST INFO
* **************************
*/
private RequestEntity setRequestInfo(TbConn conn, TbHttp http)
{
String prefix = conn.getUrlPrefix();
String ipAddr = conn.getIpAddr();
String port = conn.getPort();
String uri = http.getUri();
if(ObjectUtils.isEmpty(prefix))
{
if(conn.getPrtlCd() == PROTOCOL.HTTP)
{
prefix = HttpConstant.URL_PREFIX_HTTP;
}
else if(conn.getPrtlCd() == PROTOCOL.HTTPS)
{
prefix = HttpConstant.URL_PREFIX_HTTPS;
}
else
{
prefix = HttpConstant.URL_PREFIX_HTTP;
}
}
if(ObjectUtils.isEmpty(ipAddr))
{
throw new Exception(messageProvider.getMessage("not.empty", "urlPrefix"));
}
if(ObjectUtils.isEmpty(uri))
{
throw new Exception(messageProvider.getMessage("not.empty", "uri"));
}
/* ***************************
* REQUEST 정보 세팅
* ***************************
*/
String url = null;
HttpMethod method = null;
HttpHeaders headers = null;
Map<String,Object> params = null;
// 1. URL 세팅
StringBuffer sb = new StringBuffer();
sb.append(prefix)
.append(ipAddr);
if(ObjectUtils.isNotEmpty(port))
{
sb.append(":")
.append(port);
}
sb.append(uri);
url = sb.toString();
// 2. method 세팅
switch (http.getMethodCd()) {
case GET:
method = HttpMethod.GET;
break;
case POST:
method = HttpMethod.POST;
break;
case PUT:
method = HttpMethod.PUT;
break;
case DELETE:
method = HttpMethod.DELETE;
break;
default:
throw new Exception(messageProvider.getMessage("action.http.invalid.method", String.valueOf(http.getHttpSid())));
}
// 3. param 세팅
List<TbHttpParam> listHttpParam = httpParamMapper.listTbHttpParamByHttpSid(http.getHttpSid());
if(ObjectUtils.isNotEmpty(listHttpParam))
{
// 3-1. path 세팅
url = HttpUtil.convertPathParams(url, listHttpParam);
// 3-2. query 파람 세팅
if(HttpMethod.GET == method || HttpMethod.DELETE == method)
{
url = HttpUtil.convertQueryParams(url, listHttpParam);
}
else
{
// 3-3. body 세팅
params = HttpUtil.convertBodyParams(listHttpParam);
}
}
// 4. header 세팅
headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
RequestEntity<?> requestEntity = new RequestEntity<>(params, headers, method, URI.create(url));
return requestEntity;
}
RestTemplate에 담을 요청 데이터를 RequestEntity로 생성했다.
- TbConn에 요청 대상 서버의 정보가 담겨있다.
http:// 인지 https://인지?
ip는 어떻게 되는지?
port는 몇번인지? - TbHttp과 TbHttpParam 에 자세한 요청 데이터가 들어가있다.
uri이 어떻게 되는지?
http method는 무엇인지?
요청 파라미터는 어떻게 보낼 것인지? - URL : TbConn의 정보와, TbHttp, TbHttpParam(QueryParam, PathParam)을 조합해서 만든다.
- Method : TbHttp에 담긴 메소드 정보로 만든다.
- Param : Method가 GET,DELET가 아닌 경우 TbHttpParam(ReqBody)를 통해 requestBody를 생성한다.
requestBody를 만들어주는 Util을 따로 구현했다. - Header : 헤더는 아직 기획이 내려오기 전이라서 기본 application/json으로 고정하였다.
checkResponse(ResponseEntity, TbHttp) : 응답 데이터에서 성공/에러 여부를 판단하는 메소드
public Boolean checkResponse(ResponseEntity<Object> responseEntity, TbHttp http)
{
/* ***************************
* CHECK SUCCESS OR ERROR
*
* SUCCESS
* 1. httpsStatus == 200
* 2. AND successPath, successValue
*
* ERROR
* 1. httpStatus != 200
* 2. OR errorPath, errorValue
*
* true : success
* false : error
* null : success(판단 유무 없음)
* ***************************
*/
Boolean restSuccess = null;
Object body = responseEntity.getBody();
HttpStatus httpStatus = responseEntity.getStatusCode();
String successPath = http.getSucesPath();
String successValue = http.getSucesPathVal();
String errorPath = http.getErrPath();
String findPath = null;
String findValue = null;
if(httpStatus.is2xxSuccessful())
{
// find error
if(StringUtils.isNotBlank(errorPath) && ObjectUtil.existPath(body,errorPath))
{
findPath = errorPath;
restSuccess = false;
}
// find success
else if(StringUtils.isNotBlank(successPath) )
{
findPath = successPath;
findValue = successValue;
if(StringUtils.isBlank(successValue))
{
restSuccess = ObjectUtil.existPath(body, successPath);
}
else
{
try
{
String successPathValue = ObjectUtil.getValueByPath(body,successPath).toString();
if(StringUtils.equals(successValue, successPathValue))
{
restSuccess = true;
}
else
{
restSuccess = false;
}
}
catch(Exception ex)
{
// response에 successPath가 존재하지 않으면 실패!!
restSuccess = false;
}
}
}
// error/success 조건 없음
else
{
restSuccess = null;
}
}
else
{
restSuccess = false;
}
return restSuccess;
}
RestTemplate을 통해 요청을 보내고 ResponseEntity로 응답을 받은 데이터에서 특정 경로와 특정값을 통해 해당 API가 성공인지 에러인지 판단하지 위한 메소드이다.
이 메소드는 응답코드가 2xx일때만 성공/에러 여부를 판단하고, 아닌경우 에러로 리턴한다.
- return True : 성공 판단 경로에 성공 판단 값이 존재하는 경우
- return False : 에러 판단 경로가 존재하는 경우
- return null : 성공/에러 판단할 경로 및 값이 모두 존재하지 않는 경우 (판단하지 않겠다는 의미)
exchange(TbConn, TbHttp) : RestTemplate으로 exchange 통신하는 메소드
public Boolean exchange(TbConn conn, TbHttp http, Integer connectionTimeoutSec)
{
/* ***************************
* REQUEST
* ***************************
*/
RequestEntity requestEntity = this.setRequestInfo(restRequestInfo, conn, http);
Integer conTimeoutSec = connectionTimeoutSec;
Integer timeoutSec = http.getTmoutSecond();
if(ObjectUtils.isEmpty(timeoutSec))
{
timeoutSec = HttpConstant.DF_READ_TIMEOUT_SEC;
}
if(ObjectUtils.isEmpty(conTimeoutSec))
{
conTimeoutSec = HttpConstant.DF_CONNECT_TIMEOUT_SEC;
}
HttpClient httpClient = HttpClientBuilder.create()
.setMaxConnTotal(100)
.setMaxConnPerRoute(5)
.build();
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setReadTimeout(timeoutSec * 1000); //ms
factory.setConnectTimeout(conTimeoutSec * 1000); //ms
factory.setHttpClient(httpClient);
RestTemplate restTemplate = new RestTemplate(factory);
/* ***************************
* RESPONSE
* ***************************
*/
try
{
ResponseEntity<Object> responseEntity = restTemplate.exchange(requestEntity, Object.class);
Boolean result = this.checkResponse(restRequestInfo,responseEntity, http);
return result;
}
catch(HttpClientErrorException ex)
{
return false;
}
}
- 요청 데이터 : setRequestInfo메소드를 통해서 요청 데이터를 RequestEntity로 반환받는다.
- 요청 준비 : RestTemplate으로 요청하기 위한 설정값을 세팅한다.
HttpClient에 커넥션 풀 세팅
HttpComponentsClientHttpRequestFactory에 타임아웃 세팅 및 HttpClient 주입 - 요청 및 응답 : RestTemplate.exchange(RequestEntity, Class) 메소드를 통해 요청하고 응답 받는다.
응답 데이터 타입은 Object로 세팅했다.
RestTemplate은 기본적으로 응답코드가 2xx이 아닌 경우HttpClientErrorException을 내린다. - 성공/에러 체크 : Object로 받은 응답데이터를 checkResponse(TbHttp) 메소드를 통해 성공/에러 여부를 반환받는다.
끝!! get,post,put,delete 메서드들이 디비에 저장된 데이터로 모두 잘 통신된다!!
참고 자료