2500만 건 데이터 정규화

태량·2023년 7월 14일
0

어떤 상황이였는가?

MapBook Project에는 도서가 주변 도서관에서 대출 가능한지 지도에 보여주는 기능이 있다.

대출 가능 여부 데이터를 얻기 위해서 OpneAPI를 활용 하는데 API 연결은 단일 요청만 지원 한다. Java Concurrent 패키지를 활용하여 비효율적인 단일 요청 방식을 병렬 요청으로 바꿔 80% 이상의 성능 개선이 있었지만 한편으로는 불필요한 요청에 따른 비효율이 발견되어 이것을 제거 해야만 했다.

불필요한 요청이란?

  • 대출 가능 여부는 일단 그 도서관이 해당 도서를 소장하고 있어야 의미가 있다. 기존의 방식은 이런 점을 간과하고 소장하고 있지도 않은 주변 도서관에 불필요한 요청을 보내고 있었다.

전국 도서관 별 소장 도서

공공 데이터 플랫폼에서 전국의 도서관 별 소장 도서 데이터 셋을 발견 했다. 데이터 셋은 전국 1400여개 도서관 별로 평균 10만개의 도서 데이터 셋을 Csv파일 형태로 제공 하고 있었다. 이 데이터를 내부 DB에 저장하고 API 요청 전에 이 데이터를 통해 전처리 한 뒤 불필요한 요청을 제거하면 되겠다라는 생각이 들었다.

그래서 어떤 조치를 내렸나?

소장 도서 데이터 셋의 칼럼명은 아래와 같다.

우리가 기존에 가지고 있는 400만 건 도서 상세 정보 데이터와 비교 했을 때, [도서명,저자,출판사,발행년도]의 데이터는 중복이 되는 것을 발견 했다.

정규화를 위해 아래와 같이 ISBN, 대출건수, 등록일자 데이터 만을 추출하고, 기존 데이터 셋에 없는 LBRRY_NO와 Area_cd를 삽입하기 위해 Java를 활용해 작업을 수행 했다.

정규화 과정을 거친 도서관 별 소장 도서 Table

이 데이터는 어떻게 사용되고 있나?

위 Sequence Diagram은 지도 기반 대출 도서 서비스에 대한 Diagram이다. 정규화된 테이블은 LibraryFinder의 메소드 getNearByLibraries에서 사용되게 된다.

List<LibraryDto> getNearByHasBookLibraries(String isbn13,Integer areaCd) {

        log.info("This is support Area");

        return libraryHasBookRepo.findHasBookLibraries(isbn13, areaCd)
            .stream()
            .map(l -> new LibraryDto(l, "Y",true))
            .toList();
    }
@Query("select l.library from LibraryHasBook l where l.isbn13 = :isbn13 and l.areaCd = :areaCd")
    List<Library> findHasBookLibraries(@Param("isbn13") String isbn13,@Param("areaCd") int areaCd);

Data JPA에 메소드로 정의된 JPQL 이다. 사용자가 요청한 isbn13과 사용자 위치 코드를 매칭하여 소장 도서 테이블인 lib_hasbook을 조회하여 소장하는 도서관 데이터를 반환한다.

소장하는 도서관 데이터를 전달 받은 ConnGenerator가 소장하는 도서관에 대해서만 API 요청 인스턴스를 만들어서 사용한다.

작업 과정

  1. Csv File을 다운 받아 하나의 폴더에 모은다.

  2. Csv File 이름 중 앞부분에 ' 00 도서관' 형태를 파악하고, 이것을 기존의 Library_NO와 매칭 하는 작업을 한다.

  • 기존에 DB에 lib_info 테이블로 1400여개의 도서관에 대한 상세 정보를 가지고 있다. 이 테이블에서 primary Key인 LBRRY_NO을 Csv File 이름의 도서관 정보와 매칭하여 제공 받은 Csv File에 존재하지 않은 LBRRY_NO을 추가 했다.
String libraryName = extractLibraryName(file);

Optional<LibraryDto> libraryOpt = 
libraries.stream().filter(l -> l.getLibNm().contains(libraryName)).findAny();
                
// File Name : 구성도서관 장서 대출목록 (2023년 04월)에서  '구성도서관'만 추출

private String extractLibraryName(File file) {
        return file.getName().split(" ", 3)[0];
    }
  1. Csv File에서는 [ISBN,대출 횟수,등록일자], 도서관 정보 Table에선 [도서관 번호, 지역 코드]를 활용하여 새로운 Csv File을 만든다.
private void processFiles(File[] files, BufferedWriter writer, List<LibraryDto> libraries)
        throws IOException {

        boolean headerSaved = false;

        for (File file : files) {

            String libraryName = extractLibraryName(file);

            Optional<LibraryDto> libraryOpt =
                libraries.stream().filter(l -> l.getLibNm().contains(libraryName)).findAny();

            if (libraryOpt.isEmpty()) {
                log.info("Library not found: " + libraryName);

            } else {
                LibraryDto library = libraryOpt.get();

                try (Reader reader = Files.newBufferedReader(
                    file.toPath(), Charset.forName("EUC-KR"))) {

                    CSVParser csvParser = new CSVParser(reader, CSVFormat.DEFAULT);

                    if (!headerSaved) {
                        writer.write("ISBN,LOAN_CNT,LBRRY_CD,REGIS_DATA,AREA_CD");
                        writer.newLine();
                        headerSaved = true;
                    }

                    for (CSVRecord record : csvParser) {
                        normalizeData(writer, record, library);
                    }
                }
            }
        }
    }
private void normalizeData(BufferedWriter writer, CSVRecord record, LibraryDto library)
        throws IOException {

        String isbn = record.get(5);
        String loanCount = record.get(11);
        String regisDate = record.get(12);

        if (isValidIsbn(isbn)) {
            writer.write(buildCsvLine(isbn, loanCount, library, regisDate));
            writer.newLine();
        } else {
            log.info("Problematic line: " + String.join(",", record));
        }
    }
  1. 통합된 Csv File을 DB에 import한다.
profile
좋은 영향력과 교류를 위하여

0개의 댓글