[Spring WebFlux] WebClient 통해 공공데이터 Open API 호출하기 + 유의점(SERVICE KEY IS NOT REGISTERED ERROR)
이 글은 공부를 하면서 알게 된 내용들을 기록하는 글 입니다. 오류나 고쳐야 할 사항들이 있다면 지적 부탁드립니다!
✅ WebFlux를 이용한 이유
혼자 공부 겸 진행하고 있는 프로젝트에서 공공데이터포탈에서 기상청 단기예보 API를 신청해서 사용을 했다. 공공데이터 Open API 사용을 위해 구글에 Http 통신을 찾아보니 다양한 방법들이 나왔다.
- HttpURLConnection
- RestTemplete
- Webflux - WebClient
제일 처음 구현은 StringBuilder와 HttpURLConnection을 이용한 구현이었다.
자료를 계속 찾다보니 RestTemplete으로 많이 구현하길래 RestTemplete으로 구현을 하려고 자료를 또 찾아봤는데…!! RestTemplete는 Spring 5.0 이후부터 Deprecated 되어 WebFlux를 권장한다고 한다.
에잇 이왕 하는 김에 새로운걸로! 재미있는걸로! 하자! 해서 WebFlux 공부도 해볼 겸 WebClient를 이용해보기로 했다.
✅ 어질어질한 이전 코드
@Service
@Transactional(readOnly = true)
class WeatherService {
@Value("\${kma.callback-url}")
lateinit var url:String;
@Value("\${kma.service-key}")
lateinit var serviceKey:String;
/**
* 기상청
* url 변동 사항: base_date, base_time, nx, ny
*/
fun searchWeather(): String {
val baseDate = getBaseDate();
val baseTime = getBaseTime(LocalTime.now());
val urlBuilder = getCallbackURL(12, baseDate, baseTime, 60, 126);
val url = URL(urlBuilder);
val conn = url.openConnection() as HttpURLConnection;
conn.requestMethod= "GET";
conn.setRequestProperty("Content-type", "application/json");
val rd = BufferedReader(InputStreamReader(conn.inputStream));
val sb = java.lang.StringBuilder();
var line: String?
while (rd.readLine().also { line = it } != null) {
sb.append(line);
}
rd.close();
conn.disconnect();
val result = sb.toString();
return result;
}
/**
* request url에 들어갈 base_date를 구함
* -> 현재의 날짜를 yyyyMMdd 형식으로 바꾸는 코드
*/
fun getBaseDate(): String {
val curTime = LocalDateTime.now()
val formatter = DateTimeFormatter.ofPattern("yyyyMMdd")
return curTime.format(formatter);
}
/**
* request url에 들어갈 base_time을 구함
* -> 현재 시간에서 30분을 뺀 후, 범위에 맞는 base_time을 구한 후 반환
*/
fun getBaseTime(curTime:LocalTime): String {
val hours = curTime.hour;
val minutes = curTime.minute;
var convertedHour = "";
if (minutes < 30) {
convertedHour = (hours - 1).toString() + "00";
}else{
convertedHour = hours.toString() + "00";
}
return when(convertedHour){
"200", "300", "400" -> "0200";
"500", "600", "700" -> "0500";
"800", "900", "1000" -> "0800";
"1100", "1200", "1300" -> "1100";
"1400", "1500", "1600", -> "1400";
"1700", "1800", "1900" -> "1700";
"2000", "2100", "2200"-> "2000";
"2300", "2400", "000", "0000", "100"-> "2300";
else -> {"0000"}
}
}
// 기상청 단기예보 request url 생성
fun getCallbackURL(numOfRaws: Int, baseDate: String, baseTime: String, nx: Int, ny: Int): String {
val urlBuilder = StringBuilder(url).also {
it.append("?serviceKey=$serviceKey")
.append("&dataType=JSON")
.append("&numOfRaws=$numOfRaws")
.append("&pageNo=1")
.append("&base_date=$baseDate&base_time=$baseTime")
.append("&nx=$nx&ny=$ny")
};
return urlBuilder.toString();
}
}
✅ WebClient 라이브러리를 이용한 Open API 요청
이전 코드를 보면 알겠지만, 동작은 하지만 뭔가 상당히 어지럽다. 구현을 하면서도 Kotlin에서 이렇게 코드를 짜야한다고…? 하는 의문점이 계속 생겼고, 구글링을 더 하다보니 Spring WebFlux의 WebClient를 이용하면 더 깔끔하게 구현이 가능하다는 것을 알게 되었다.
WebFlux에는 다양한 라이브러리들이 있는데, Open API를 호출하기 위해서는 WebClient를 이용해야 한다.
RestTemplete은 동기적으로 동작하기 때문에 요청/응답 동안 스레드가 차단되는 문제점이 있는데, 이와는 달리 WebFlux(WebClient)는 논블로킹에 기반을 두기 때문에 RestTemplete에 비해 스레스 차단을 최소화할 수 있는 장점이 있다고 한다.
(WebFlux에 대해서는 이후에 더 자세히 알아보는 시간을 가져보려고 한다!)
WebClient를 통한 Http 통신은 "WebClient의 인스턴스를 builder를 통해 생성 -> url, query parameter등을 설정 -> 결과 값이 받아 처리"의 과정을 갖는다.
WebClient.Builder를 Bean으로 의존성 주입이 가능하기 때문에 생성자를 통해 의존성을 주입받는다.
그 다음 url, query parameter를 설정하는데, uriBuilder를 이용하면 좀 더 깔끔한 방식으로 설정이 가능하다.
.retrieve() 메서드는 WebClient의 주요 메서드 중 하나인데, 해당 메서드를 호출하면 HTTP 요청을 보내고 그에 대한 응답을 받을 수 있다.
이후에는 .subscribe() 혹은 .block()을 통해 응답을 받을 수 있는데, .block()을 사용하면 응답을 동기적으로 대기하고 받아온다.
.bodyToMono()를 통해 응답을 원하는 형태의 객체로 받을 수 있는데, 나는 Response 객체를 따로 만들어서 값을 받았다.
@Service
@Transactional(readOnly = true)
class WeatherService(private val webBuilder: WebClient.Builder){
@Value("\${kma.callback-url}")
lateinit var BASE_URL:String;
@Value("\${kma.service-key}")
lateinit var SERVICE_KEY:String;
fun requestWeatherAPI() : Response{
val factory = DefaultUriBuilderFactory(BASE_URL)
.apply {
this.encodingMode = DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY
};
val webClient = webBuilder
.uriBuilderFactory(factory)
.baseUrl(BASE_URL)
.build();
val response = webClient.get()
.uri { uriBuilder: UriBuilder ->
uriBuilder
.queryParam("serviceKey", SERVICE_KEY)
.queryParam("dataType", "JSON")
.queryParam("base_date", getBaseDate())
.queryParam("base_time", getBaseTime(LocalTime.now()))
.queryParam("nx", 120)
.queryParam("ny", 60)
.build()
}
.retrieve()
.bodyToMono(Response::class.java)
.block();
return requireNotNull(response) {
throw BusinessException(ErrorCode.API_SEND_FAILURE)
};
}
...
}
✅ WebClient를 통해 Open API 요청 시 주의사항
HttpUrlConnection에서 WebClient로 코드를 리팩토링 한 이후, 이상하게 postman이나 HttpUrlConnection 방식에서는 API 요청이 문제없이 됐었는데 WebClient로 리팩토링 한 이후에는 계속 서버 오류가 발생했다.
SERVICE KEY IS NOT REGISTERED ERROR 에러가 계속 발생했다. 공공데이터포탈에서 제공해주는 API 활용 가이드 문서를 보니 서비스 키가 등록되어있지 않을 때 발생하는 에러라고 한다. 하지만 서비스 키는 yaml 파일로 관리하기 때문에 변경되지 않았는데 참으로 이상할 일이다.
구글링을 해보았더니, WebClient를 이용해서 HTTP 요청을 할 때 service key를 queryParam으로 전달했는데, WebClient가 queryParam을 UriComponentsBuilder#encode() 방식을 이용해서 인코딩하기 때문에 service key의 값이 달라져서 생기는 문제였다.
따라서 인코딩 방식을 다르게 하거나, 인코딩을 하지 않으면 해당 문제를 해결할 수 있다.
DefaultUriBuilderFactory() 객체를 생성하여 인코딩 모드를 NONE이나 VALUES_ONLY로 변경한 후, WebClientBuilder에 적용하면 API 통신이 정상적으로 되는 것을 확인할 수 있다.
참고 링크
https://colabear754.tistory.com/122