공공데이터 활용(1)

개나뇽·2025년 1월 11일

개요

저는 커피를 매우 좋아합니다. 커피의 맛을 따지는 것보다는 그저 즐기는 것을 좋아하는 사람입니다. 최근 진행 중인 프로젝트에서 소상공인 상가 정보 공공 데이터를 활용하는 기능을 추가하고자 했습니다. 이 블로그 포스트에서는 데이터 처리 과정과 개선 방안을 공유하고자 합니다.

단계

  1. 필요 데이터 결정
  2. 필요 기술 조사
  3. 구현
  4. 결과

1. 필요 데이터 결정

  • 목표: 서울, 경기 소재의 카페 데이터 확보
  • 데이터 출처: 공공데이터포털
  • 필드 추출: 필요한 필드를 확인하고 클래스 설계

image.png

2. 필요 기술 조사

문제 상황

  • 다운로드 받은 자료에는 여러 부가 정보가 많아 필요 데이터만 남기고 없애고자 시도
  • CSV 파일 형태이기에 관리및 수정이 편한 엑셀 파일로 전환 시도
    • 파일이 너무 큰 관계로 스프레드 시트, Notion에 업로드 되지 않는 문제 발생
  • CSV 파일에서 수정을 진행하려 해도 데이터의 수가 많아(서울시만 약 43만건) 필터 및 변경 반영 외에도 파일 로드에 많은 시간이 소요됨.
  • CSV 파일 자체를 읽어와 데이터를 필터링하여 필요 속성 RDB에 저장

접근 과정

  • 엑셀 파일을 읽는 라이브러리 경험을 바탕으로, opencsv 라이브러리를 사용하여 CSV 파일을 읽기로 결정했습니다.
  • implementation 'com.opencsv:opencsv:5.9' 라이브러리 사용

3. 구현

  1. @EventListener(ApplicationReadyEvent.class) 를 통해 애플리케이션 실행시 애노테이션이 적용된 메서드를 실행
  2. CSV를 읽는 CSVReader 객체 생성
  3. CSV는 Comma-Sperarted- Values로 ‘,’ 로 구분된 파일 형식
  4. List<String[]> , List<Cafe>의 반환용 리스트 생성
  5. 반복문을 수행하면서 [i][8]에 있는 업종 분류가 “카페”인 데이터만 convertCsvToCafe() 메서드 실행
  6. onvertCsvToCafe() 을 통해 row에 있는 데이터를 조작해 Cafe 객체를 생성
  7. cafeListsaveAll(); 로 저장
"상가업소번호","상호명","지점명","상권업종대분류코드","상권업종대분류명","상권업종중분류코드","상권업종중분류명","상권업종소분류코드","상권업종소분류명","표준산업분류코드","표준산업분류명","시도코드","시도명","시군구코드","시군구명","행정동코드","행정동명","법정동코드","법정동명","지번코드","대지구분코드","대지구분명","지번본번지","지번부번지","지번주소","도로명코드","도로명","건물본번지","건물부번지","건물관리번호","건물명","도로명주소","구우편번호","신우편번호","동정보","층정보","호정보","경도","위도"
"MA0101202210A0093845","이상한스냅","","M1","과학·기술","M113","사진 촬영","M11301","사진촬영업","M73303","사진 처리업","11","서울특별시","11470","양천구","11470600","신월5동","1147010300","신월동","1147010300100090004","1","대지",9,4,"서울특별시 양천구 신월동 9-4","114703005067","서울특별시 양천구 월정로",283,,"1147010300100090004000001","백송주택","서울특별시 양천구 월정로 283","158822","07902","","","",126.828831598706,37.5421174398055
@Transactional
@EventListener(ApplicationReadyEvent.class)
public void readCsv() {
    log.info("데이터 저장 시작");
    CSVReader reader = null;
    long startTime = System.currentTimeMillis();
    try {
        reader = new CSVReader(new FileReader(PATH));
        List<String[]> rows = reader.readAll();
        List<Cafe> cafeList = new ArrayList<>();
        for (int i = 0; i < rows.size(); i++) {
            if ("카페".equals(rows.get(i)[8])) {
                Cafe cafe = convertCsvToCafe(rows.get(i));
                cafeList.add(cafe);
            }
        }
        long elapsedTime = System.currentTimeMillis() - startTime; // 소요 시간 계산
        log.info("총 데이터 수 : {}, 데이터 수: {}, 소요 시간: {} ms", rows.size(), cafeList.size(),
            elapsedTime);
        cafeRepository.saveAll(cafeList);
    } catch (FileNotFoundException | CsvValidationException e) {
        throw new RuntimeException(e);
    } catch (IOException e) {
        throw new RuntimeException(e);
    } catch (CsvException e) {
        throw new RuntimeException(e);
    } finally {
        if (reader != null) {
            try {
                reader.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public Cafe convertCsvToCafe(String[] row) {
    String name = ""; // [1] + [2]
    String roadNamedAddress = ""; //[31] + [30] + [35] + [36]
    String zipCode = ""; // [33]
    double longitude = 0; // [37]
    double latitude = 0; // [38]
    for (int i = 1; i < row.length; i++) {
        name = row[1] + " " + row[2];
        roadNamedAddress = row[31] + " " + row[30];
        zipCode = row[33];
        longitude = Double.valueOf(row[37]);
        latitude = Double.valueOf(row[38]);
    }
    return Cafe.builder()
        .name(name)
        .roadNamedAddress(roadNamedAddress)
        .zipCode(zipCode)
        .longitude(longitude)
        .latitude(latitude)
        .build();
}

4. 결과

  • 공공 데이터를 원하는 형식으로 RDB에 저장 성공.

문제점

  • 가독성: 코드의 가독성이 좋지 않습니다.
  • saveAll(): BulkInsert를 생각했으나 insert 쿼리가 1개씩 나가는 문제가 발생했습니다.
  • 성능 저하: 43만 건의 데이터를 읽고 2만 건을 저장하는 데 약 6시간 30분이 소요되었습니다.

개선 방법

  1. 파일 리팩토링: 한 번 파일을 읽어 필요한 데이터만 남긴 후 리팩토링된 파일을 생성하여 이후 작업에서 이를 읽어오도록 합니다.
  2. 가독성 향상: stream 과 람다를 이용해 가독성을 상승
  3. 스레드 수 증가: 현재의 로직은 1개 의 스레드만 사용하므로 스레드 사용을 늘리자.

참고

https://www.data.go.kr/data/15083033/fileData.do#/

profile
정신차려 이 각박한 세상속에서!!!

2개의 댓글

comment-user-thumbnail
2025년 1월 11일

관련해서 질문이 있습니다.
1. saveAll() 은 BulkInsert 용으로 쓰신걸까요 ?

그렇다면 DB에 Insert 할 때, SaveAll()할 때, 채번 방식에 따라서 insert 쿼리가 1개씩 나갈 수 있어요.
그 부분 확인해보세요.

  1. DB에 Insert 하는걸 스프링 App띄우는 타이밍에 넣는 방식이 올바른걸까요?
    -> 굳이 Spring Boot를 띄울 필요가 없는데, 느낌상 스프링을 띄운거 같아요.
    -> 이 부분에 대해서 고민해봐도 좋을거 같아요.
    -> CSV파일이 있다면 DBeaver WorkBench를 통해서 그냥 밀어넣는 방법도 있을거에요.
1개의 답글