개발을 하다보니 서버→서버로 HTTP 요청을 보내야 하는 상황이 생겼다.스프링에서 기본적으로 제공하는 HTTP 전송용 라이브러리가 몇가지 있는데 그중 하나인 RestTemplate
을 이용해서 서버간 통신을 구현해봤다. (RestTemplate에 관한 설명)
공공데이터를 사용한 기능 예시
주변 가게에 할인정보를 조회하는 프로젝트에서 사용했다. 사용자가 아닌 요식업자가 본인의 가게를 등록할 수 있는데 여기서 사업자번호와 요식업영업신고조회를 통해 검증을 한다. 사업자번호 검증 API는 국세청에서, 영업신고조회는 식품의약품안전처에서 api key를 발급 받을 수 있다.
💡 스프링 3.0에서부터 지원하는 RestTemplate
은 HTTP 통신에 유용하게 쓸 수 있는 템플릿이다. REST 서비스를 호출하도록 설계되어 HTTP 프로토콜의 메서드 (GET
, POST
, DELETE
, PUT
)에 맞게 여러 메서드를 제공한다. RestTemplate
은 다음과 같은 특징이 있다
공공데이터 API 문서
이를 이용해서 Spring에서 공공API를 호출해 데이터를 가져오는 기능을 구현하고자했다. API문서를 읽어보니 발급받은 서비스키를 받아서 쿼리스트링에 넣고, 요청데이터는 body에 넣어서 API를 호출하는 방식이였다.
문제1 : service key is not registered error
처음에 키를 발급 받으면 1시간 정도 후에 정상적으로 이용이 가능하다고 한다.
보통은 1시간 이내에 정상적으로 작동하지만 시간이 지나도 발급받은 키로 인증이 되지 않았다.
브라우저 주소창이나 Postman으로 요청을 보내 봤을때에는 정상적으로 작동했다.
String url = "<http://api.odcloud.kr/api/nts-businessman/v1/validate?serviceKey=내서비스키>";
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
ResponseEntity<Map> resultMap = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class);
"message": "400 Bad Request:"{"code":"msg":"등록되지 않은 인증키 입니다."}""
알아본 결과 우리가 브라우저에 URL을 입력하면 브라우저는 ASCII 문자 집합에 없는 문자를 URL에 포함시키기 위해 인코딩하는 과정을 거친다.
웹 브라우저의 주소창에서 입력한 URL은 "Percent Encoding" 또는 "URL Encoding" 방식으로 인코딩된다. 이 방식은 ASCII 문자 집합에 없는 문자를 URL에 포함시키기 위해 사용됩니다.
웹브라우저나 포스트맨은 인코딩된 인증키를 서비스키에 넣어야하고, 스프링 서버에서 요청을 날릴땐 디코딩된 서비스키를 넣어야 한다……………….
근데도 안됨
UriComponent
사용
UriComponents uri = UriComponentsBuilder
.fromHttpUrl("<http://api.odcloud.kr/api/nts-businessman/v1/validate>")
.queryParam("serviceKey", "Q4N9ymIHpP/IThG3X0Mhnd/eElMoR+Zg==") //보안상 키의 일부분은 삭제했습니다.
.encode()
.build();
//요청보내기
ResponseEntity<Map> resultMap = restTemplate.exchange(uri.toUri(),
HttpMethod.POST, entity, Map.class);
하지만 이 방법도 실패했다. 원인은 URIComponent의 인코딩 방식이다.
내 키의 중간을 보면 슬래시/
기호가 있는데 URIComponen에서는 슬래시기호를 구분자로 인식해 인코딩을 하지 않았다.
로그로 출력해 보았을 때
* 키의 일부분은 보안상 삭제했습니다*
원본 키
mIHpP/IThGVuTnlj3X0Mhnd/eElMoR+ZgsO1+PpQ868CGBh1hL50rRA==
UriComponent로 인코딩한 키
mIHpP/IThGVuTnlj3X0Mhnd/eElMoR+ZgsO1+PpQ868CGBh1hL50rRA%3D%3D
위에 인코딩된 키를 출력해 보았을 때 문제점을 찾았다.
인코딩을 했을 때 마지막부분에 포함된 =
는 정상적으로 %3D
로 인코딩이 되었다. 그런데 슬래시는 인코딩 되지 않은 상태로 있다.
하지만 중간부분에 슬래쉬기호도 저기선 키의 일부분이지 URL의 구분자로서 있는게 아니기 때문에 인코딩이 되어야 하지만 인코딩이 되지 않았다.
URI클래스 사용
UriComponents는 스프링에서 제공하는 객체이다. 하지만 인코딩과정에서 슬래시를 포함하지 않으니 사용할 수 없다.
그래서 Java에서 제공하는 기본 클래스로, URI를 표현하고 구문 분석하는 데 사용되는 java.net
.URI
클래스를 사용한다.
String url = "<http://api.odcloud.kr/api/nts-businessman/v1/validate?serviceKey=서비스키>";
URI uri = new URI(url);
ResponseEntity<Map> resultMap = restTemplate.exchange(uri, HttpMethod.POST, entity, Map.class);
요청 url을 바탕으로 URI클래스를 만들어서 exchange의 파라미터로 넘겨준다.
serviceKey
는 제대로 인식하게됐다.
문제2 요청데이터 포맷
요청데이터를 보낼때 API문서에는 데이터 필드에 대한 설명만 있어서 이 필드들을 하나의 클래스로 보내면 될 줄알았다. 하지만 API의 swagger를 보고 나서야 해당 클래스들을 배열로 받는 것을 알게되었다.
처음에 단일 클래스만 보냈을 때에는 아래와 같은 에러가 났다.
500 Internal Server Error: "{"status_code":"BAD_JSON_REQUEST"}"
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
List<BusinessValidationRequestDto> businesses = new ArrayList<>();
businesses.add(requestDto);
Map<String, List<BusinessValidationRequestDto>> map = Collections.singletonMap("businesses", businesses);
HttpEntity<Map<String, List<BusinessValidationRequestDto>>> entity = new HttpEntity<>(map, headers);
{
"businesses": [
{
"b_no": "0000000000",
"start_dt": "20000101",
"p_nm": "홍길동",
"p_nm2": "홍길동",
"b_nm": "(주)테스트",
"corp_no": "0000000000000",
"b_sector": "",
"b_type": ""
}
]
}
이후 swagger에서 요청 데이터 예시를 보니까 “businesses”는 단일 클래스가 아니라 배열타입이였다.
body
안에 businesses
라는 key
를 가진 map
을 보내주는데 value
는 List<requestDto>
타입으로 수정해줬다. 해결.
문제3 : 응답클래스 dto로 파싱
응답데이터를 내가 만든 DTO로 파싱하려니 문제가 생겼다. 처음에는 응답데이터의 body를 그대로 가져오려 했으나 데이터 타입이 맞지 않았다.
BusinessValidationResponseDto responseDto =
(BusinessValidationResponseDto) resultMap.getBody();
//에러메시지 출력
java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class com.tandamzi.storeservice.dto.response.DataItem
응답 데이터는 hashMap
으로 반환되었고, 이는ObjectMapper
를 사용해서 내가 만든 dto 파싱이 된다.
BusinessValidationResponseDto responseDto =
objectMapper.convertValue(resultMap.getBody(), BusinessValidationResponseDto.class);