Open API로 데이터 자동 업데이트

윤준혁·2024년 8월 16일

문제 상황

  • 외부의 데이터셋을 이용해 프로젝트를 만들었는데, 외부의 데이터셋을 지속적으로 업로드해줘야 결과값이 더 정교해짐
  • 데이터셋이 업데이트 되는 상황마다 추가된 데이터를 수동으로 업데이트해야 함
  • 프로젝트에서 자동으로 업데이트할 수 있으면 신경쓸 부분이 크게 줄어듬

해결 방법

  • 다행히 해당 데이터셋은 특정 URL과 변수를 통해 JSON형태의 데이터를 반환하고 있었음
  • 내가 할 일은 데이터를 받아오고, 해당 데이터를 내가 가진 데이터셋에 추가해주는 작업만 하면 됨

신경 써야 할 부분

  1. 데이터를 정상적으로 받아왔는지 여부 : JSON의 Header에 ReturnValue에 Success or Failed가 뜸
  2. 받아온 데이터가 중복값인지 여부 : 기존의 arff 데이터셋에 Index값이 날짜정보로써 고유값의 역할을 하게끔 작성
  3. 중복이 아닌 데이터라면 기존의 arff 데이터셋에 업데이트
  4. 스케줄링을 통해 작업을 자동화

과정

  1. 먼저, API로부터 데이터를 받아오기 위해 응답 데이터를 매핑할 DTO 클래스를 작성(API에서 반환된 JSON 데이터를 Java 객체로 변환)
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class DataResponse {
    private int data;
    private String drwNoDate;
    private String returnValue;
    
    public int getFormattedDrawDate() {
        return Integer.parseInt(drwNoDate.replace("-", ""));
    }
}

@JsonIgnoreProperties(ignoreUnknown = true) -> DataResponse 객체를 매핑할 때, JSON에 정의되지 않은 필드를 무시

  1. API와 통신하여 데이터를 가져오고, 마지막으로 처리된 회차의 번호를 관리하는 'UpdateService' 클래스를 작성(API를 호출하여 데이터를 가져오고, 파일에 기록)
@Service
public class DataUpdateService {
    private static final String API_URL = ${API_URL};
    private static final String LATEST_DRAW_NO_FILE = "latestDrawNo.txt";

	// 파일 경로를 반환(resources폴더에 저장되도록)
    private String getResourceFilePath() throws IOException {
        Path path = Paths.get("src/main/resources/" + LATEST_DRAW_NO_FILE);
        File file = new File(path.toString());
        if (!file.exists()) {
            if (file.createNewFile()) {
                System.out.println("Created new file: " + file.getAbsolutePath());
            } else {
                throw new IOException("Failed to create file: " + file.getAbsolutePath());
            }
        }
        return file.getAbsolutePath();
    }

	// 가장 최근에 처리된 회차 번호를 반환
    public int getLatestDrawNo() throws IOException {
        String filePath = getResourceFilePath();
        File file = new File(filePath);

        if (!file.exists()) {
            FileWriter writer = new FileWriter(file);
            writer.write("1");
            writer.close();
            return 1;
        }

        BufferedReader reader = new BufferedReader(new FileReader(file));
        String latestDrawNoStr = reader.readLine();
        reader.close();

        if (latestDrawNoStr == null || latestDrawNoStr.isEmpty()) {
            return 1;
        }

        return Integer.parseInt(latestDrawNoStr);
    }

	// 회차 번호를 파일에 저장
    public void saveLatestDrawNo(int drawNo) throws IOException {
        String filePath = getResourceFilePath();
        try (FileWriter writer = new FileWriter(filePath)) {
            writer.write(Integer.toString(drawNo));
            writer.flush();  // 즉시 데이터를 파일에 씁니다.
            System.out.println("Saved latest draw number: " + drawNo);
        }
    }

	// API를 통해 데이터를 가져오는 메소드
    public DataResponse getDataInfoByDrawNumber(int drawNo) {
        RestTemplate restTemplate = new RestTemplate();
        String url = API_URL + drawNo;

        try {
            ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
            String responseBody = response.getBody();

            if (responseBody != null && responseBody.startsWith("{")) {
                ObjectMapper objectMapper = new ObjectMapper();
                return objectMapper.readValue(responseBody, DataResponse.class);
            } else {
                System.out.println("Unexpected response format: " + responseBody);
                return null;
            }
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}
  1. 가져온 데이터를 ARFF 파일에 저장하는 메소드를 기존의 WekaService에 추가
	// 데이터를 ARFF 파일에 저장하는 메서드
    // 초안에서는 단순히 데이터를 업데이트하는 로직만 있었지만, API 통신과 파일 업데이트가 함께 이루어지도록 통합
	public void updateArffFileFromStartDraw(DataUpdateService dataUpdateService) throws Exception {
        int latestDrawNo = dataUpdateService.getLatestDrawNo();

        while (true) {
            DataResponse dataResponse = dataUpdateService.getDataInfoByDrawNumber(latestDrawNo);

            if (dataResponse == null || !"success".equals(dataResponse.getReturnValue())) {
                System.out.println("No more data to fetch. Stopping at draw number: " + latestDrawNo);
                break;
            }

            boolean updated = updateArffFile(dataResponse);
            dataUpdateService.saveLatestDrawNo(latestDrawNo);

            if (updated) {
                System.out.println("Updated draw number: " + latestDrawNo);
            }

            latestDrawNo++;
        }
    }

	// ARFF 파일을 업데이트하는 메서드
    // 초안에서는 중복 확인만 했지만, 중복이 아닌 경우 데이터를 추가하는 로직을 통합
    public boolean updateArffFile(DataResponse dataResponse) throws Exception {
        if (!"success".equals(dataResponse.getReturnValue())) {
            System.out.println("Lotto API request was not successful.");
            return false;
        }

        DataSource source = new DataSource(ARFF_FILE_PATH);
        Instances data = source.getDataSet();

        Attribute drawDateAttribute = data.attribute("drwNoDate");

        if (drawDateAttribute == null) {
            throw new IllegalArgumentException("Attribute 'drwNoDate' not found in ARFF file.");
        }

        boolean isDuplicate = false;
        int formattedDrawDate = dataResponse.getFormattedDrawDate();
        for (Instance instance : data) {
            if ((int) instance.value(drawDateAttribute) == formattedDrawDate) {
                isDuplicate = true;
                break;
            }
        }

        if (!isDuplicate) {
            double[] instanceValues = new double[data.numAttributes()];
            instanceValues[0] = dataResponse.getData();
            instanceValues[2] = formattedDrawDate;

            Instance newInstance = new DenseInstance(1.0, instanceValues);
            data.add(newInstance);

            ArffSaver saver = new ArffSaver();
            saver.setInstances(data);
            saver.setFile(new File("src/main/resources/" + ARFF_FILE_PATH));
            saver.writeBatch();
            System.out.println("ARFF file updated with draw date: " + dataResponse.getDrwNoDate());

            return true;
        } else {
            System.out.println("Duplicate draw date detected: " + dataResponse.getDrwNoDate());
            return false;
        }
    }
  1. 스케줄링에 해당 작업 등록
@Scheduled(cron = "0 0 2 ? * SUN")
    public void updateDataArff() throws Exception {
        int latestDrawNo = dataUpdateService.getLatestDrawNo();

        while (true) {
            DataResponse latestDataInfo = dataUpdateService.getDataInfoByDrawNumber(latestDrawNo);

            if (latestDataInfo == null || !"success".equals(latestDataInfo.getReturnValue())) {
                System.out.println("No more data to fetch. Stopping at draw number: " + latestDrawNo);

                dataUpdateService.saveLatestDrawNo(latestDrawNo - 1);
                break;
            }

            boolean updated = wekaService.updateArffFile(latestDataInfo);

            dataUpdateService.saveLatestDrawNo(latestDrawNo);

            if (updated) {
                System.out.println("Updated draw number: " + latestDrawNo);
            }

            latestDrawNo++;
        }
    }

결과

  • 자동으로 데이터를 수집하고, 업데이트하는 작업을 통해 중복 확인과 검증만 잘하면 시간과 노력을 아낄 수 있음
  • 데이터셋의 크기가 커지거나 비정상적인 상황에 대한 대응 전략을 염두해 둬야할 듯(모니터링, 알림 등)
  • Open API를 제공하지 않는 경우를 생각해 웹 크롤링이나 스크린 스크래핑같은 기술도 공부해보고 싶어짐

0개의 댓글