Spring Boot

[Spring WebFlux] WebClient 통해 공공데이터 Open API 호출하기 + 유의점(SERVICE KEY IS NOT REGISTERED ERROR)

HEY__ 2023. 8. 23. 16:51
728x90

이 글은 공부를 하면서 알게 된 내용들을 기록하는 글 입니다. 오류나 고쳐야 할 사항들이 있다면 지적 부탁드립니다!

WebFlux를 이용한 이유

혼자 공부 겸 진행하고 있는 프로젝트에서 공공데이터포탈에서 기상청 단기예보 API를 신청해서 사용을 했다.  공공데이터 Open API 사용을 위해 구글에 Http 통신을 찾아보니 다양한 방법들이 나왔다.

  1. HttpURLConnection
  2. RestTemplete
  3. 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

 

[WebFlux] WebClient를 사용하여 외부 Api를 호출할 땐 인코딩을 주의해야 한다

문제의 배경 프로젝트 진행 중에 입력값의 유효성을 검사하기 위해 외부 Api를 호출할 일이 있었다. 그런데 포스트맨이나 크롬 개발자 도구에서는 문제 없이 잘 호출되는 Api가 막상 WebClient를 사

colabear754.tistory.com

https://hannah06.tistory.com/entry/WebClient-%EA%B3%B5%EA%B3%B5%EB%8D%B0%EC%9D%B4%ED%84%B0-openAPI-%ED%98%B8%EC%B6%9C-WebClient-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%84%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%ED%95%98%EA%B8%B0

 

728x90