WebClient)API 호출코드 리팩토링

김태성·2024년 8월 13일

개인 프로젝트-1

목록 보기
18/53
post-thumbnail

코드를 보다가

    private Mono<String> makeApiCall(String URL, String path, String queryParams) {
        String fullUrl = URL + path + queryParams;
        return webClient.get()
                .uri(URI.create(fullUrl))
                .retrieve()
                .bodyToMono(String.class)
                .doOnError(e -> logger.error("Error during API call", e));
    }

    public Mono<String> getProductPriceInfo() {
        return makeApiCall(
                env.getProperty("KCA_URL"),
                env.getProperty("KCA_ProdPrice_Path"),
                "goodInspectDay=" + LF + "&entpId=100&ServiceKey=" + env.getProperty("DATA_Key")
        );
    }

    public Mono<String> getProdInfo() {
        return makeApiCall(
                env.getProperty("KCA_URL"),
                env.getProperty("KCA_ProdInfo_Path"),
                "ServiceKey=" + env.getProperty("DATA_Key")
        );
    }

    public Mono<String> getStoreInfo() {
        return makeApiCall(
                env.getProperty("KCA_URL"),
                env.getProperty("KCA_StoreInfo_Path"),
                "ServiceKey=" + env.getProperty("DATA_Key"));
    }

    public Flux<String> getKEPCO() {
        return Flux.fromIterable(kepcoCode)
                .flatMap(code -> makeApiCall(
                        env.getProperty("KEPCO_URL"),
                        env.getProperty("KEPCO_Path"),
                        "year=" + LY + "&month=" + MM + "&metroCd=" + code + "&apiKey=" + env.getProperty("KEPCO_Key")
                ));
    }
    public Mono<String> getKEPCO_2() {
        return makeApiCall(
                env.getProperty("KEPCO_URL"),
                env.getProperty("KEPCO_Path"),
                "year=" + LY + "&month=" + MM +  "&apiKey=" + env.getProperty("KEPCO_Key"));
    }

    public Mono<String> getHIRAInfo() {
        return makeApiCall(
                env.getProperty("HIRA_URL"),
                env.getProperty("HIRA_HospBase_Path"),
                "ServiceKey=" + env.getProperty("DATA_Key"));
    }

    public Mono<String> getShortKMA() {
        return makeApiCall(
                env.getProperty("KMA_URL"),
                env.getProperty("KMA_Short_Path"),
                "ServiceKey=" + env.getProperty("DATA_Key"));
    }

    public Mono<String> getMidKMA() {
        return makeApiCall(
                env.getProperty("KMA_URL"),
                env.getProperty("KMA_Mid_Path"),
                "ServiceKey=" + env.getProperty("DATA_Key"));
    }



}

API 호출 Service가 계속 반복되는걸 봤다.
곰곰히 생각해보니 URL, Path, Key의 반복이고 파라미터값만 좀 바꾸면 되니
코드를 압축시킬 수는 없을까 싶어서 리팩토링을 해봤다.


    private Mono<String> makeApiCall(String URL, String path, String queryParams) {
        String fullUrl = URL + path + queryParams;
        return webClient.get()
                .uri(URI.create(fullUrl))
                .retrieve()
                .bodyToMono(String.class)
                .doOnError(e -> logger.error("Error during API call", e));
    }

    public Flux<String> getAllApiResponses() {
        return Flux.range(0, urlList.size())
                .flatMap(i -> {
                    String url = urlList.get(i);
                    String path = pathList.get(i);
                    String key = keyList.get(i);
                    String queryParams = "ServiceKey=" + key;

                    // 필요한 경우 각 API에 맞는 추가적인 쿼리 파라미터를 설정
                    if (url.contains("KEPCO")) {
                        queryParams = "year=" + LY + "&month=" + MM + "&metroCd=" + kepcoCode.get(0) + "&apiKey=" + key;
                    } else if (url.contains("KCA")) {
                        queryParams += "&goodInspectDay=" + LF + "&entpId=100";
                    }

                    return makeApiCall(url, path, queryParams);
                });
    }
}

확실히 장난아니게 줄어들었다.
기존에는 96줄짜리 코드가 60줄로 줄어들었다.
이는 반복코드를 제외한 변수 설정까지 다 합한 것이기 때문에,
실질적으로는 56줄짜리 코드가 17줄로 변한 것이다.

여기까지는 좋다. 정말 좋은데 문제가 발생했다.

Properties 파일의 변수가 감당이 안된다..
이렇게 되면 문제가 발생하는게

  • 버그가 터지면 어느 URL에서 문제가 발생했는지 하나하나 다 까봐야한다
  • List에 데이터를 추가할려고 해도 생각해야 할게 많아진다.
  • List에 데이터를 추가했더라도 상황에 따라 Parameter를 따로 설정해야 한다.
  • Parameter를 설정하는것도 Properties와 다른 파일에서 관리된다.


기존에 쓰던 Properties 형식이다. 용도에 따라 분류해놓았다.
Location Code는 인코딩 설정이 잘못되서 한글이 다 깨졌는데, 설정을 바꾸고 다시 적어야 한다.

그래서 나에게는 2가지 조건이 생겨버렸다.

  • 하나는 위의 Properties 형식에 맞게 데이터를 정리하기
  • 반복되는 구문을 최소한으로 하고, 반복구문이 한눈에 들어오게 바꾸기

그럼 내가 가진 코드중에서 반복되는 구문이 뭐가있을까?

String fullUrl = URL + path + queryParams;

이 코드이다.
API 호출과 변수 설정은 고정된 값이지만
API URL은 호출 개수가 늘어나는 만큼 계속해서 증가한다.
따라서, URL 호출 코드를 한줄로 바꿀 수 있다면 획기적으로 코드를 간소화 시킬수 있다고 생각했고,

레퍼런스 : https://d2.naver.com/helloworld/2771091

Mono.zip라는 코드를 알게 되었다.
위 코드는 여러가지의 Mono들을 하나로 묶어주는 역할을 한다!

    private Mono<String> makeApiCall(String baseUrlKey, String pathKey, String... queryParams) {
        String baseUrl = env.getProperty(baseUrlKey);
        String path = env.getProperty(pathKey);
        String fullUrl = baseUrl + path + String.join("&", queryParams);

        return webClient.get()
                .uri(URI.create(fullUrl))
                .retrieve()
                .bodyToMono(String.class)
                .doOnError(e -> logger.error("Error during API call", e));
    }

    public Mono<Tuple6<String, String, String, String, String, String>> All_Api_URI() {
        return Mono.zip(
                makeApiCall("KCA_URL", "KCA_ProdPrice_Path", "goodInspectDay=" + LF, "entpId=100", "ServiceKey=" + env.getProperty("DATA_Key")),
                makeApiCall("KCA_URL", "KCA_ProdInfo_Path", "ServiceKey=" + env.getProperty("DATA_Key")),
                makeApiCall("KCA_URL", "KCA_StoreInfo_Path", "ServiceKey=" + env.getProperty("DATA_Key")),
                makeApiCall("HIRA_URL", "HIRA_HospBase_Path", "ServiceKey=" + env.getProperty("DATA_Key")),
                makeApiCall("KMA_URL", "KMA_Short_Path", "ServiceKey=" + env.getProperty("DATA_Key")),
                makeApiCall("KMA_URL", "KMA_Mid_Path", "ServiceKey=" + env.getProperty("DATA_Key"))
        );
    }

데이터 불러오는 코드가 단 10줄로 변해버린 기적이 발생했다!
57줄에서 10줄로 줄어든거면 20프로 이하로 압축이 되었다는 것이다.

데이터 호출 오류 1

지금 내가 적은 코드는 문제점이 하나 있는데,
그것은 Mono.zip의 구성요소 중 하나라도 오류가 나면 에러가 발생한다는 것이다.

그래서 왜이런지 Postman으로 확인을 해봤는데

아.. 데이터의 업로드 주기가 일정하지 않는것이다.
분명히 저번에 코드를 적을때는 전주 데이터가 잘 들어왔었는데,
이번에는 데이터 업로드가 느린지 6일이 지나도 데이터가 업데이트 되지 않았다.

따라서, 2주전 데이터를 가져오기로 했다.

데이터 호출 오류 2

또 에러가 뜬다.

뭔가 싶어서 코드를 봤더니
"message": "200 OK from GET http://openapi.price.go.kr/openApiImpl/ProductPriceInfoService/getStoreInfoSvc.do",

200OK를 받았고,

*__checkpoint ⇢ Body from GET http://openapi.price.go.kr/openApiImpl/Produ~~

각각의 API 통신도 재대로 이루어진걸 확인할 수 있다.
어이가없어서 코드를 자세히 확인하니까

 Exceeded limit on max bytes to buffer : 262144

이상한 오류가 떴다.
버퍼가 터졌다고 한다.
레퍼런스 : https://garyj.tistory.com/22

데이터의 용량이 너무 커서 오류가 났다고 한다.

    @Autowired
    public Public_service(WebClient.Builder webClientBuilder, Environment env) {
        this.webClient = webClientBuilder
                .exchangeStrategies(ExchangeStrategies.builder()
                        .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024)) // 10MB로 설정
                        .build())
                .build();
        this.env = env;
    }

WebClient 버퍼 설정을 해줬고

데이터가 잘 들어오는걸 확인할 수 있다.
사이즈 크기를 보고 놀랐는데, 뭔 text 파일 크기가 650KB나 된다
버퍼가 터질 만했다..

그리고 눈치가 좀 빠르신분들은 알아차리셨겠지만
데이터가 4개밖에 들어오지 않는다.

사실, API 요청인자 때문에 한전 전력데이터 개방 포털시스템쪽에 문의를 넣었다.

metroCd의 세부내용을 보면 ※미선택시 모든 시도 응답이라고 되어있는데,


그런거없고 얄짤없이 400이 뜬다.
그래서 이 데이터는

KEPCO_Code=11,26,27,28,29,30,31,36,41,43,44,45,46,47,48,50,51

Code를 활용하여 리스트 형식으로 받아올 것이다.
근데 막상 코드를 짜보니

    public Mono<Tuple4<String, String, String, String>> KEPCO_Api_URI() {
        return Mono.zip(
                makeApiCall("KEPCO_URL", "KEPCO_Path", "year=" + LY, "month=" + MM, "metroCd=" + kepcoCode.get(0), "apiKey=" + env.getProperty("KEPCO_Key")),
                makeApiCall("KEPCO_URL", "KEPCO_Path", "year=" + LY, "month=" + MM, "metroCd=" + kepcoCode.get(1), "apiKey=" + env.getProperty("KEPCO_Key")),
                makeApiCall("KEPCO_URL", "KEPCO_Path", "year=" + LY, "month=" + MM, "metroCd=" + kepcoCode.get(2), "apiKey=" + env.getProperty("KEPCO_Key")),
                makeApiCall("KEPCO_URL", "KEPCO_Path", "year=" + LY, "month=" + MM, "metroCd=" + kepcoCode.get(3), "apiKey=" + env.getProperty("KEPCO_Key")),
                makeApiCall("KEPCO_URL", "KEPCO_Path", "year=" + LY, "month=" + MM, "metroCd=" + kepcoCode.get(4), "apiKey=" + env.getProperty("KEPCO_Key")),
                makeApiCall("KEPCO_URL", "KEPCO_Path", "year=" + LY, "month=" + MM, "metroCd=" + kepcoCode.get(5), "apiKey=" + env.getProperty("KEPCO_Key")),
                makeApiCall("KEPCO_URL", "KEPCO_Path", "year=" + LY, "month=" + MM, "metroCd=" + kepcoCode.get(6), "apiKey=" + env.getProperty("KEPCO_Key")),
                makeApiCall("KEPCO_URL", "KEPCO_Path", "year=" + LY, "month=" + MM, "metroCd=" + kepcoCode.get(7), "apiKey=" + env.getProperty("KEPCO_Key")),
                makeApiCall("KEPCO_URL", "KEPCO_Path", "year=" + LY, "month=" + MM, "metroCd=" + kepcoCode.get(8), "apiKey=" + env.getProperty("KEPCO_Key")),
                makeApiCall("KEPCO_URL", "KEPCO_Path", "year=" + LY, "month=" + MM, "metroCd=" + kepcoCode.get(9), "apiKey=" + env.getProperty("KEPCO_Key")),
                makeApiCall("KEPCO_URL", "KEPCO_Path", "year=" + LY, "month=" + MM, "metroCd=" + kepcoCode.get(10), "apiKey=" + env.getProperty("KEPCO_Key")),
                makeApiCall("KEPCO_URL", "KEPCO_Path", "year=" + LY, "month=" + MM, "metroCd=" + kepcoCode.get(11), "apiKey=" + env.getProperty("KEPCO_Key")),
                makeApiCall("KEPCO_URL", "KEPCO_Path", "year=" + LY, "month=" + MM, "metroCd=" + kepcoCode.get(12), "apiKey=" + env.getProperty("KEPCO_Key")),
                makeApiCall("KEPCO_URL", "KEPCO_Path", "year=" + LY, "month=" + MM, "metroCd=" + kepcoCode.get(13), "apiKey=" + env.getProperty("KEPCO_Key")),
                makeApiCall("KEPCO_URL", "KEPCO_Path", "year=" + LY, "month=" + MM, "metroCd=" + kepcoCode.get(14), "apiKey=" + env.getProperty("KEPCO_Key")),
                makeApiCall("KEPCO_URL", "KEPCO_Path", "year=" + LY, "month=" + MM, "metroCd=" + kepcoCode.get(15), "apiKey=" + env.getProperty("KEPCO_Key")),
                makeApiCall("KEPCO_URL", "KEPCO_Path", "year=" + LY, "month=" + MM, "metroCd=" + kepcoCode.get(16), "apiKey=" + env.getProperty("KEPCO_Key")),
                );
    }

이런.. 모양이 나와버렸다.
도저히 볼 수가 없어서 Flux로 바꿨다.

    public Flux<String> KEPCO_Api_URI() {
        return Flux.fromIterable(kepcoCode)
                .flatMap(code -> makeApiCall("KEPCO_URL", "KEPCO_Path", "year=" + LY, "month=" + MM, "metroCd=" + code, "apiKey=" + env.getProperty("KEPCO_Key")));
    }

가로로 길어졌지만, 관심이 있으면 한번 읽고 파악할 수 있고,
관심이 없다면 한전 데이터 호출코드구나~ 하고 넘어갈 것이다.

그런데 또 터졌다 ㅋㅋ

org.springframework.web.reactive.function.client.WebClientRequestException: connection timed out after 30000 ms: bigdata.kepco.co.kr/

요약을 하자면, 호출을 하고 30초가 넘었는데도 응답이 안된다는 것이다.
뭔가 싶어서 properties를 줄이면서 실험을 했는데



Path 뒤에 "?"가 2개 붙은거였다
근데 더 골때리는거는

KEPCO 제외 KCA / HIRA 쪽은 호출할때 ??가 붙어도 오류가 안난다.

아니 왜 여기는 되고 저기는 안되는건지 도저히 이해가 안됐지만 어쨌든 고쳤는데


또터졌다 ㅋㅋ

한번더 자세하게 살펴보니

Making request to URL: https://bigdata.kepco.co.kr/openapi/v1/powerUsage/houseAve.do??year=2023&month=08&metroCd=11&apiKey=0m82zd7ldQA89hapC6WOZocz08R1ut5fg7UJ68rb
Making request to URL: https://bigdata.kepco.co.kr/openapi/v1/powerUsage/houseAve.do??year=2023&month=08&metroCd=26&apiKey=0m82zd7ldQA89hapC6WOZocz08R1ut5fg7UJ68rb
.....

아니 왜 https로 보내고있지? 하고 Properties를 확인하니

KEPCO_URL=https://bigdata.kepco.co.kr

바로 바꾸고 API 응답을 확인했다.
그런데 이번에는 데이터가 나오질 않는다.

뭔가 싶어서 찾아보니
레퍼런스 : https://www.reddit.com/r/SpringBoot/comments/wlxz8a/flux_not_returning_any_response/

sse 설정을 하라는 것이었고, 이를 반영해서

    @GetMapping(value = "/KEPCO", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> getKEPCO_Data() {
        return publicService.KEPCO_Api_URI();
    }

코드를 설정하니

데이터가 신기한 방식으로 들어오는걸 확인할 수 있었다.

전체코드는 다음과 같다.

    @Autowired
    public Public_service(WebClient.Builder webClientBuilder, Environment env) {
        this.webClient = webClientBuilder
                .exchangeStrategies(ExchangeStrategies.builder()
                        .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024)) // 10MB로 설정
                        .build())
                .build();
        this.env = env;
    }

    private Mono<String> makeApiCall(String baseUrlKey, String pathKey, String... queryParams) {
        String baseUrl = env.getProperty(baseUrlKey);
        String path = env.getProperty(pathKey);
        String fullUrl = baseUrl + path + "?" + String.join("&", queryParams);

        return webClient.get()
                .uri(URI.create(fullUrl))
                .retrieve()
                .bodyToMono(String.class)
                .doOnError(e -> logger.error("Error during API call", e));
    }

    public Mono<Tuple4<String, String, String, String>> KCA_HIRA_Api_URI() {
        return Mono.zip(
                makeApiCall("KCA_URL", "KCA_ProdPrice_Path", "goodInspectDay=" + LF, "entpId=100", "ServiceKey=" + env.getProperty("DATA_Key")),
                makeApiCall("KCA_URL", "KCA_ProdInfo_Path", "ServiceKey=" + env.getProperty("DATA_Key")),
                makeApiCall("KCA_URL", "KCA_StoreInfo_Path", "ServiceKey=" + env.getProperty("DATA_Key")),
                makeApiCall("HIRA_URL", "HIRA_HospBase_Path", "ServiceKey=" + env.getProperty("DATA_Key"))
        );
    }

    public Flux<String> KEPCO_Api_URI() {
        return Flux.fromIterable(kepcoCode)
                .concatMap(code -> {
                    logger.info("Making API call for metroCd: {}", code);
                    return makeApiCall("KEPCO_URL", "KEPCO_Path", "year=" + LY, "month=" + MM, "metroCd=" + code, "apiKey=" + env.getProperty("KEPCO_Key"))
                            .doOnNext(response -> logger.info("Received response: {}", response))
                            .doOnError(error -> logger.error("Error during KEPCO API call", error));
                });
    }

이전의 복잡했던 코드에 비해서
훨씬 깔끔하게 짜여진것 같다.

profile
닭이 되고싶은 병아리

0개의 댓글