전력 데이터 API 저장

김태성·2024년 8월 21일

개인 프로젝트-1

목록 보기
23/53
post-thumbnail

이전 글에서 위치 데이터를 업데이트 했다.

그래서 이번에는 전력 사용량을 업데이트 해보려고 한다.
이유는 다음과 같다.

  1. 다른 데이터에서 키 값 받아오는것을 연습하기 위해
  2. 관련 데이터가 위치값 하나밖에 없어서
  3. 이 데이터만 가져오면 프론트로 표시할 데이터가 생기기 때문

따라서 이러한 근거로 전력 사용량 데이터를 업데이트 하기로 했다.

데이터 확인

  • 날짜 데이터
  • 지역
  • 인구
  • 전력 사용량
  • 전기세

로 이루어져 있다.
모든 데이터를 저장할 건데, metro와 city를 활용해 LocationKey를 받아올 생각이다.

지난 글에 Excel에서부터 받아온 데이터들이다.
이 데이터에서 city/country 데이터는 각각 metro/city 데이터에 대응한다.

하지만 예외가 있는데,

다음과 같이 띄어쓰기로 들어오는 데이터들은 띄어쓰기를 붙여줘야 한다.

Entity

@Getter
@NoArgsConstructor
@Entity
public class Electricity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long electricity_key;

    private String electricityYear;
    private String electricityMonth;
    private int electricityPopulation;
    private double electricityAverageUsage;
    private int electricityAverageBill;
    private Long locationKey;

    // 빌더 패턴을 위한 생성자
    @Builder
    public Electricity(String electricityYear, String electricityMonth, int electricityPopulation, double electricityAverageUsage, int electricityAverageBill, Long locationKey) {
        this.electricityYear = electricityYear;
        this.electricityMonth = electricityMonth;
        this.electricityPopulation = electricityPopulation;
        this.electricityAverageUsage = electricityAverageUsage;
        this.electricityAverageBill = electricityAverageBill;
        this.locationKey = locationKey;
    }
}

만들어 뒀던 레퍼런스를 가지고 만들었다.
특이점이라고 하면 locationKey를 가진다는 것이다.

여기서 이전 코드와 차이점이 있다면 snake ->carmel 로 바꿨다.

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'update_controller' defined in file []: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'location_CSV' defined in file []: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'locationRepository' defined in com.projectharpseal.APIcall.repository.LocationRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration: Could not create query for public abstract java.util.Optional com.projectharpseal.APIcall.repository.LocationRepository.findBylocation_cityAndlocation_country(java.lang.String,java.lang.String); Reason: Failed to create query for method public abstract java.util.Optional com.projectharpseal.APIcall.repository.LocationRepository.findBylocation_cityAndlocation_country(java.lang.String,java.lang.String); No property 'location' found for type 'Location'; Did you mean 'location_x','location_y'

정말 긴 오류 메시지이다.
gpt에게 물어보니 naming case에 관련된 문제니 전부 camelcase로 바꾸면 해결될 것이라고 한다.
궁금해서 찾아보니


hibernate는 java의 기본 명명법을 따르고, java는 camelcase를 쓴다는 것이다.

snake 케이스로 짜고 있었는데, 싹다 camel케이스로 바꿨다...

Repository

public interface ElectricityRepository extends JpaRepository<Electricity, Long> {

}

}

Service

public void parseApiCall() {
        for (String code : KEPCO_Code) {
            Mono<String> apiCallResult = publicService.makeApiCall(
                    "KEPCO_URL",
                    "KEPCO_Path",
                    "KEPCO_Key",
                    "year=" + LY.getYear(),
                    "month=" + LY.getMonth(),
                    "metroCd=" + code,
                    "apiKey="
            );
       }
 }

기본적인 API 호출 코드이다.
하지만 여기에 추가되는 코드가 2개 있다.
바로 날짜 코드와 metroCd 인데,

//Date_return Dto

@Getter
@Setter
public class Date_Return{


    private String year;
    private String month;
    private String day;
    public Date_Return(String year, String month, String day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }
}


//Date_return method
    public static Date_Return calculateLastYearMonth(){

        LocalDate today = LocalDate.now();
        LocalDate LY = today.minusYears(1);
        String Year = LY.format(DateTimeFormatter.ofPattern("yyyy"));
        String Month = LY.format(DateTimeFormatter.ofPattern("MM"));
        String Day = "Null";
        return new Date_Return(Year, Month, Day);
    }
    
    
//metroCd
private static final String[] KEPCO_Code = {"11", "26", "27", "28", "29", "30", "31", "36", "41", "43", "44", "45", "46", "47", "48", "50", "51"};

변경점은 다음과 같다.

  • Date_Return 코드를 추가해 년/월/일 데이터를 묶어서 보낼 수 잇다.
  • properties에 있던 metroCd를 KECPO 파일에서 직접 가져온다.

특히나 properties에 있는 데이터를 넣은 이유가 무엇인고 하니

  • properties에 있는 데이터를 Service에 주입해 버리면 쓸대없는 의존성이 더 생겨버린다(properties는 API Call Method에서만 호출)
  • API Call Method에서 주입한다면 Flux로 API를 호출하기 때문에 코드가 쓸대없이 복잡해졌다.

따라서 위의 단점들 때문에 코드에서 냄새가 나니 1줄 그냥 하드코딩 하고 복잡한 코드를 지워버렸다.

apiCallResult.subscribe(response -> {
    try {
        JsonNode rootNode = objectMapper.readTree(response);

        String metro = rootNode.path("metro").asText();
        String city = rootNode.path("city").asText().replace(" ", "");
        Optional<Location> LocationValue = locationRepository.findByLocationCityAndLocationCountry(city, metro);

        if (LocationValue.isPresent()) {
            Location location = LocationValue.get();

            // Electricity 엔티티 생성 및 데이터베이스 저장
            Electricity electricity = Electricity.builder()
                    .electricityYear(rootNode.path("year").toString())
                    .electricityMonth(rootNode.path("month").toString())
                    .electricityPopulation(rootNode.path("houseCnt").asInt())
                    .electricityAverageUsage(rootNode.path("powerUsage").asDouble())
                    .electricityAverageBill(rootNode.path("bill").asInt())
                    .locationKey(location.getLocationKey())
                    .build();

            electricityRepository.save(electricity);
        } else {
            logger.warn("metro, city에서 오류 발생: {} , {}", metro, city);
        }
    } catch (Exception e) {
        logger.error("Error parsing JSON response", e);
    }
}

다음 코드는 데이터를 파싱하는 코드이다.
레퍼런스 : https://projectreactor.io/docs/core/release/reference/#mono

이거 찾느라 정말 힘들었다..
다름이 아닌 Mono의 데이터를 파싱하는 방법인데,
Mono는 .subscribe를 통해 데이터를 받아내야 한다.
자세한 내용은 추후 다루도록 하고, 일단 코드를 보자.(솔직히 아직 생각의 정리가 덜 되었다)


```java apiCallResult.subscribe(response -> { ``` apiCallResult를 subscribe 해서 데이터를 받아온다.

레퍼런스 : https://www.baeldung.com/jackson-json-node-tree-model

JsonNode rootNode = objectMapper.readTree(response);

이후 ObjectMapper.readTree를 통해 response 값을 json 객체로 변환시키고

String metro = rootNode.path("metro").asText();
        String city = rootNode.path("city").asText().replace(" ", "");
        Optional<Location> LocationValue = locationRepository.findByLocationCityAndLocationCountry(city, metro);

metro, city 값과 같은 location의 key값을 찾는다.
이를 통해 location과 Electricity가 연결된다.

if (LocationValue.isPresent()) {
            Location location = LocationValue.get();
        } else {
            logger.warn("metro, city에서 오류 발생: {} , {}", metro, city);
        }
            

if~ else로 예외 처리를 하면 끝!

null error1

@RestController
public class update_controller {

    private final Location_CSV locationCsv;
    private final KEPCO kepco;

    public update_controller(Location_CSV locationCsv, KEPCO kepco) {
        this.locationCsv = locationCsv;
        this.kepco = kepco;
    }

    @PostMapping("/update_Location")
    public void data(@RequestParam("file") MultipartFile file) throws IOException {
        locationCsv.ExcelData(file);
    }
    @GetMapping("/update_KEPCO")
    public void KEPCO_update(){
        kepco.parseApiCall();
    }

그리고 기존 controller를 update 파일로 한꺼번에 관리하기로 했다.
이제 코드를 실행시켜보자.

api 호출도 정상적으로 되고, 코드도 실행이 됐는데...
어쩐지 DB에 저장이 되지 않는다.
뭔가 이상해서 logger를 찍어봤는데

어쩐지 데이터가 null이 뜬다..

분명히 url은 정상이 맞다.
살짝 이전에 해봤던 React 비동기 처리의 PTSD가 살짝 나서

.block으로 동기적 호출로 바꿔줬더니

그런건 없고 다시 터졌다.

이전 프로젝트를 곰곰히 생각해봤다.
React를 사용할때 발생했던 문제와 일치한다.
문제의 핵심은

정확한 요청과 값을 확인했음에도 바로 사용하려니까 null값이 뜬다.

이는 요청을 보낸 직후 return 값을 null로 줬고,
그로 인해서 요청의 값이 돌아오기 전에 null로 미리 연산을 해버리는 것이라 생각했다.

따라서

        return webClient.get()
                .uri(URI.create(fullUrl))
                .retrieve()
                .bodyToMono(String.class)
                .flatMap(data -> {
                    if (data == null || data.isEmpty()) {
                        return Mono.empty();  
                    } else {
                        return Mono.just(data);
                    }
                })
                .doOnError(e -> logger.error("Error during API call", e));

webClient에 flatMat을 활용하였다.
레퍼런스 : https://manish-dixit.medium.com/reactive-tools-map-flatmap-flatmapmany-subscribe-514460b4ccd9

이후 데이터를 확인해 보니

성공적으로 들어오는걸 확인할 수 있었다!
(사실 여기서 정말 중요한것은 이게 .flatMap을 통해 데이터가 들어오는건지 파악이 안된다는 것이다. null error3를 보자.)

하지만 metro 값이 전부 오류가 뜨길레 확인을 했다.

            String response = apiCallResult.block();
            try {
                    JsonNode rootNode = objectMapper.readTree(response);
                    logger.info("node data = {}", rootNode);
                    String metro = rootNode.path("metro").asText();
                    String city = rootNode.path("city").asText().replace(" ", "");
                    Optional<Location> LocationValue = locationRepository.findByLocationCityAndLocationCountry(city, metro);
                    logger.info("metro: {}, city: {}, key: {}", metro, city, LocationValue);

그래서 logger로 전부 확인 해 보니

data가 한번 더 씌워져 있었다.
아까 데이터가 안들어와져서 구조를 뜯어보다가 response 값을 그대로 들고온 게 잘못되었나 보다..

null error 2

그런데 갑자기

같은 코드에서 잘되던 api가 갑자기 안되기 시작한다.

그리고 시간이 지나니 갑자기 또 된다. 한두번 그런게 아니라 계속 이런다.
정말 이해 할 수 없지만.. 일단은 넘어가자.(진짜 미칠거같다 왜이러는지 모르겠다)

어쨌든 데이터는 들어왔는데,

오류가 난다.
또 한참을 해맨 뒤..

                        Optional<Location> LocationValue = locationRepository.findByLocationCityAndLocationCountryAndLocationTown(metro, city, town);

코드에 metro/city가 반대로 적혀있었다..
이후에 드디어..!

데이터가 들어왔다!

null error3



위에 적었던 flatMap은 아무리 봐도 실질적인 해결방안이 아니라고 생각했다.
그래서 지우고 다시 해봤더니 역시나 그냥 된다.
위에서 api 호출이 안되던것은 null error2와 연관이 있다고 판단을 내렸다.
갑자기 안되는 상황에서 추측할 수 있는것은

  1. 진짜 비동기 과정에서 어디가 꼬여서 터지고 있다.
  2. 반복적인 api 호출로 인해서 springboot에서 자체적으로 방어하고 있다
  3. 한전 데이터포털 측에서 공격 의심을 해서 방어하고 있다.

가 되겠다. 코드가 전혀 바뀌지 않았음에도 되다 안되다 하는것이라 내 상상력으로는 여기가 한계인거 같다.

하지만 1,3은 말이 안되는 것이
똑같은 코드가 어쩔때는 되다가 갑자기 null이 뜨고 그러는건 말도 안되고(심지어 비동기 처리가 문제였던것이 아니다!)
PostMan으로 따로 호출했을때는 잘만 돌아가기 때문이다.

여전히 이 문제는 원인조차 파악하지 못하지만 어찌 손쓸 방법이 없다.
프레임워크에 문제가 있는것도 아니고
코드에 문제가 있는것도 아니고
그렇다고 호출을 잘못하고 있는것도 아니고
참 난감한 상황이다.

다시한번 느끼지만

  1. 비동기에 대한 지식이 전혀 없었다면
  2. flatMap이 진짜 효과가 있다고 생각했다면

남들이 api 호출 실패할때 flatmap 쓰라고 진지하게 말했을거 같다..
끔찍한 상상이고 공부가 더 필요함을 느낀다.


거의 하루 종일 삽질한 후 겨우 완성했는데 너무 감격스러웠다.
잘 되다가 갑자기 터져버리는 api
어떻게 관리해야 될지도 모르겠는 비동기처리
갑자기 naming 때문에 멈춰버리는 코드 등등
정말 말도 안되는 방법으로 계속 막혔던거 같다....


이후 metro 이름 바뀌는거(강원도 -> 강원특별자치도) 등등의 예외처리만 해 주면 끝이 날 거 같다.
일단 오늘은 여기까지..!

profile
닭이 되고싶은 병아리

0개의 댓글