ComputerLanguage/Java

[Java] 데이터 읽기 & 읽은 데이터 원하는 데이터 형식으로 가공해보기

th42500 2022. 10. 8. 23:03

지난 게시글에서는 Java를 이용하여 원하는 텍스트 파일 내의 내용을 원하는 만큼 읽어오는 방법을 알아보았다.

https://ichijeochi.tistory.com/820

 

[Java] 텍스트 파일 읽기 (한 글자, N 글자, 한 줄, N개의 줄)

Java를 이용하여 텍스트 파일 내의 내용을 원하는 만큼 읽어와 콘솔에 뿌려주느 코드를 작성해보고자 한다. 해당 코드는 후에 우리가 csv파일을 읽고 데이터 처리를 하는 프로젝트를 하는데에 하

ichijeochi.tistory.com

 

그럼 대용량의 데이터를 읽어와서 처리하는 것도 할 수 있지 않을까?

백엔드 개발자로서 대용량 데이터를 다뤄보는 것은 좋은 기회이니 이번 기회에 직접 경험해보았다.

 

 

🛠 실습환경

✔ JDK : AdoptOpenJDK 11

✔ IntelliJ : 2022.2.3

✔ 실습 데이터

   👉 https://www.data.go.kr/data/15021148/standard.do

 

전국초중등학교위치표준데이터

국가에서 보유하고 있는 다양한 데이터를『공공데이터의 제공 및 이용 활성화에 관한 법률(제11956호)』에 따라 개방하여 국민들이 보다 쉽고 용이하게 공유•활용할 수 있도록 공공데이터(Datase

www.data.go.kr

 

현재 프로젝트의 모습

 

📌 구현할 기능 (Method 설계)

✔ 파일을 읽는 기능

✔ 읽은 파일을 , 기준으로 split 하는 기능

✔ 한 줄의 데이터를 객체로 만드는 기능

✔ 객체를 이용하여 원하는 파일로 데이터를 가공하는 기능

✔ 새로운 파일을 생성하여 원하는 내용을 저장하는 기능

 

 

📌 구현하기

1️⃣ 대용량 데이터를 읽기 위한 Parser인터페이스, SchoolParser, FileController 구현하기

먼저, 다형성을 위해 Parser 인터페이스를 생성하고 이를 구현하는 SchoolParser 클래스를 생성하였다.

SchoolParser 내의 코드는 2️⃣ 의 과정에서 구현할 예정이다.

package com.bigdata.parser;

import com.bigdata.domain.School;

public interface Parser<T> {
    T parse(String str);
}
package com.bigdata.parser;

import com.bigdata.domain.School;

public class SchoolParser implements Parser<School>{

    @Override
    public School parse(String str) {
        System.out.println(str);

        return new School();
    }
}

그리고 이를 이용하여 대용량 파일을 다루는 클래스인 FileController 클래스를 생성하여 파일을 읽어오는 메서드를 구현하였다.

이 FileController를 구현할 때  java.nio.charset.MalformedInputException : Input length = 1 에러가 발생해서 많은 구글링을 하게 되었고 구글링 끝에 다음과 같은 코드를 구현할 수 있게 되었다.

👉 java.nio.charset.MalformedInputException 해결방법 보러가기 : https://ichijeochi.tistory.com/822

package com.bigdata.parser;

import org.mozilla.universalchardet.UniversalDetector;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class FileController<T> {

    Parser<T> parser;

    public FileController(Parser<T> parser) {
        this.parser = parser;
    }

    public String findFileEncoding(String path) throws IOException {
        byte[] buf = new byte[4096];
        FileInputStream fis = new FileInputStream(new File(path));

        UniversalDetector detector = new UniversalDetector(null);

        int bufSize;
        while ((bufSize = fis.read(buf)) > 0 && !detector.isDone()) {
            detector.handleData(buf, 0, bufSize);
        }

        detector.dataEnd();

        String encoding = detector.getDetectedCharset();
        if (encoding != null) {
            System.out.println("Detected encoding = " + encoding);
        } else {
            System.out.println("No encoding detected.");
        }

        detector.reset();

        return encoding;
    }

   	public List<T> readLines(String path, String encoding) throws IOException {
        List<T> fileContents = new ArrayList<>();

        BufferedReader br;


        br = new BufferedReader(new InputStreamReader(new FileInputStream(path), encoding));
        String line;
        br.readLine();
        while((line = br.readLine()) != null) {
            fileContents.add(parser.parse(line));
        }

        return fileContents;
    }
}

 

그리고 테스트 코드까지 구현하고 테스트를 해보았다.

package com.bigdata.parser;

import com.bigdata.domain.School;
import org.junit.jupiter.api.*;

import java.io.IOException;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class FileControllerTest {
    FileController<School> fileController;
    String path;


    @Test
    @Order(1)
    @DisplayName("인코딩 확인 테스트")
    void findFlieEncoding() throws IOException {
        path = "C:\\pjt\\전국초중등학교위치표준데이터.csv";
        fileController = new FileController(new SchoolParser());

        String encoding = fileController.findFileEncoding(path);
        System.out.println(encoding);  // 현재 파일 인코딩이 무엇인지 출력해보기
        assertNotEquals("UTF-8", encoding);
    }

    @Test
    @Order(2)
    @DisplayName("대용량 데이터 파일을 잘 읽어오는지 테스트")
    void readLinesTest() throws IOException {
        FileController<School> schoolFileController = new FileController<>(new SchoolParser());
        String filename = "C:\\pjt\\전국초중등학교위치표준데이터.csv";
        String encoding = schoolFileController.findFileEncoding(filename);
        List<School> schoolsList = schoolFileController.readLines(filename, encoding);

        assertEquals(23948, schoolsList.size());
    }
}

💡 실행 결과

 

 

2️⃣ 대용량 데이터를 School 객체로 만들기 (split() 메서드 활용)

위의 실행결과를 보면 각 데이터 행은 `,`로 구분되어 있는 것을 알 수 있다.

이를 이용하기 위해 Java의 String 클래스의 split() 메서드를 사용하여 데이터들을 구분하고 School 클래스를 이용하여 각각의 객체로 만들어보았다.

👇 School.java

package com.bigdata.domain;

import java.time.LocalDateTime;

public class School {
    private String id;
    private String name;
    private String grade;
    private LocalDateTime anniversary;
    private String establishForm;
    private boolean mainSchool;
    private String operationalStatus;
    private String jiBunAddress;
    private String roadNameAddress;
    private String sidoOfficeOfEducationCode;
    private String sidoOfficeOfEducation;
    private String smallOfficeOfEducationCode;
    private String smallOfficeOfEducation;
    private Double latitude;  // 위도
    private Double longitude;  // 경도

    public School() {
    }

    public School(String id, String name, String grade, LocalDateTime anniversary, String establishForm, boolean mainSchool, String operationalStatus, String jiBunAddress, String roadNameAddress, String sidoOfficeOfEducationCode, String sidoOfficeOfEducation, String smallOfficeOfEducationCode, String smallOfficeOfEducation, Double latitude, Double longitude) {
        this.id = id;
        this.name = name;
        this.grade = grade;
        this.anniversary = anniversary;
        this.establishForm = establishForm;
        this.mainSchool = mainSchool;
        this.operationalStatus = operationalStatus;
        this.jiBunAddress = jiBunAddress;
        this.roadNameAddress = roadNameAddress;
        this.sidoOfficeOfEducationCode = sidoOfficeOfEducationCode;
        this.sidoOfficeOfEducation = sidoOfficeOfEducation;
        this.smallOfficeOfEducationCode = smallOfficeOfEducationCode;
        this.smallOfficeOfEducation = smallOfficeOfEducation;
        this.latitude = latitude;
        this.longitude = longitude;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getGrade() {
        return grade;
    }

    public LocalDateTime getAnniversary() {
        return anniversary;
    }

    public String getEstablishForm() {
        return establishForm;
    }

    public boolean getMainSchool() {
        return mainSchool;
    }

    public String getOperationalStatus() {
        return operationalStatus;
    }

    public String getJiBunAddress() {
        return jiBunAddress;
    }

    public String getRoadNameAddress() {
        return roadNameAddress;
    }

    public String getSidoOfficeOfEducationCode() {
        return sidoOfficeOfEducationCode;
    }

    public String getSidoOfficeOfEducation() {
        return sidoOfficeOfEducation;
    }

    public String getSmallOfficeOfEducationCode() {
        return smallOfficeOfEducationCode;
    }

    public String getSmallOfficeOfEducation() {
        return smallOfficeOfEducation;
    }

    public Double getLatitude() {
        return latitude;
    }

    public Double getLongitude() {
        return longitude;
    }

}

👇 SchoolParser.java 

package com.bigdata.parser;

import com.bigdata.domain.School;

import java.time.LocalDateTime;
import java.util.Arrays;

public class SchoolParser implements Parser<School>{

    @Override
    public School parse(String str) {
        String[] splitted = str.split(",");
        System.out.println(Arrays.toString(splitted));
        String date = splitted[3];
        int year = Integer.parseInt(date.substring(0, 4));
        int month = Integer.parseInt(date.substring(5, 7));
        int day = Integer.parseInt(date.substring(8));
        boolean isMainSchool = (splitted[5].equals("본교")) ? true : false;
        return new School(splitted[0], splitted[1], splitted[2], LocalDateTime.of(year, month, day,0, 0, 0), splitted[4], isMainSchool, splitted[6],
                splitted[7], splitted[8], splitted[9], splitted[10], splitted[11], splitted[12], Double.parseDouble(splitted[15]), Double.parseDouble(splitted[16]));
    }
}

Oracle Help Center JAVA 11버전 - String 클래스의 split()

위의 문서에 의하면 split("정규식 표현") 을 이용하여 문자열을 나눌 수 있으므로, 나는 ,를 기준으로 문자열을 나누어 String 배열인 splitted에 담아주었다.

또한, LocalDateTime 타입의 anniversary 변수와 boolean 타입의 mainSchool 변수를 위해 위의 코드처럼 따로 가공하여 School의 생성자 매개변수로 전달하였다.

 

👇 SchoolParserTest.java

package com.bigdata.parser;

import com.bigdata.domain.School;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.time.LocalDateTime;

import static org.junit.jupiter.api.Assertions.*;

class SchoolParserTest {
    SchoolParser schoolParser;

    @Test
    @DisplayName("원하는 형태로 데이터가 잘 분리되어 배열에 저장되는지 테스트")
    public void splitTest() {
        schoolParser = new SchoolParser();
        String line = "B000003406,대전비래초등학교,초등학교,2005-03-01,공립,본교,운영,대전광역시 대덕구 비래동 245-1,대전광역시 대덕구 우암로492번안길 65,7430000,대전광역시교육청,7441000,대전광역시동부교육지원청,2013-11-29,2021-07-05,36.363531243,127.45289438,2021-09-15,7001220,한국교원대학교";
        School school = schoolParser.parse(line);
        assertEquals("B000003406", school.getId());
        assertEquals("대전비래초등학교", school.getName());
        assertEquals(LocalDateTime.of(2005, 03, 01, 0, 0, 0), school.getAnniversary());
        assertEquals("공립", school.getEstablishForm());
        assertTrue(school.getMainSchool());
        assertEquals("운영", school.getOperationalStatus());
        assertEquals("대전광역시 대덕구 비래동 245-1", school.getJiBunAddress());
        assertEquals("대전광역시 대덕구 우암로492번안길 65", school.getRoadNameAddress());
        assertEquals("7430000", school.getSidoOfficeOfEducationCode());
        assertEquals("대전광역시교육청", school.getSidoOfficeOfEducation());
        assertEquals("7441000", school.getSmallOfficeOfEducationCode());
        assertEquals("대전광역시동부교육지원청", school.getSmallOfficeOfEducation());
        assertEquals(36.363531243, school.getLatitude());
        assertEquals(127.45289438, school.getLongitude());
    }
}

 

💡 실행 결과

SchoolParserTest의 splitTest() 결과

테스트를 위해 실제 csv 파일에 있는 데이터 중 1개를 문자열에 담아 테스트를 진행하였고 그 결과 모든 데이터가 내가 원하는대로 잘 담긴 것을 확인할 수 있었다.

 

 

3️⃣ 추출한 데이터를 원하는 데이터 형태로 가공하기

👇 School.java

package com.bigdata.domain;

import java.time.LocalDateTime;

public class School {
    private String id;
    private String name;
    private String grade;
    private LocalDateTime anniversary;
    private String establishForm;
    private boolean mainSchool;
    private String operationalStatus;
    private String jiBunAddress;
    private String roadNameAddress;
    private String sidoOfficeOfEducationCode;
    private String sidoOfficeOfEducation;
    private String smallOfficeOfEducationCode;
    private String smallOfficeOfEducation;
    private Double latitude;  // 위도
    private Double longitude;  // 경도

    public School() {
    }

    public School(String id, String name, String grade, LocalDateTime anniversary, String establishForm, boolean mainSchool, String operationalStatus, String jiBunAddress, String roadNameAddress, String sidoOfficeOfEducationCode, String sidoOfficeOfEducation, String smallOfficeOfEducationCode, String smallOfficeOfEducation, Double latitude, Double longitude) {
        this.id = id;
        this.name = name;
        this.grade = grade;
        this.anniversary = anniversary;
        this.establishForm = establishForm;
        this.mainSchool = mainSchool;
        this.operationalStatus = operationalStatus;
        this.jiBunAddress = jiBunAddress;
        this.roadNameAddress = roadNameAddress;
        this.sidoOfficeOfEducationCode = sidoOfficeOfEducationCode;
        this.sidoOfficeOfEducation = sidoOfficeOfEducation;
        this.smallOfficeOfEducationCode = smallOfficeOfEducationCode;
        this.smallOfficeOfEducation = smallOfficeOfEducation;
        this.latitude = latitude;
        this.longitude = longitude;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getGrade() {
        return grade;
    }

    public LocalDateTime getAnniversary() {
        return anniversary;
    }

    public String getEstablishForm() {
        return establishForm;
    }

    public boolean getMainSchool() {
        return mainSchool;
    }

    public String getOperationalStatus() {
        return operationalStatus;
    }

    public String getJiBunAddress() {
        return jiBunAddress;
    }

    public String getRoadNameAddress() {
        return roadNameAddress;
    }

    public String getSidoOfficeOfEducationCode() {
        return sidoOfficeOfEducationCode;
    }

    public String getSidoOfficeOfEducation() {
        return sidoOfficeOfEducation;
    }

    public String getSmallOfficeOfEducationCode() {
        return smallOfficeOfEducationCode;
    }

    public String getSmallOfficeOfEducation() {
        return smallOfficeOfEducation;
    }

    public Double getLatitude() {
        return latitude;
    }

    public Double getLongitude() {
        return longitude;
    }

    public String getSqlInsertQuery() {
        String sql = String.format("INSERT INTO `test-db`.`nation_wide_school`\n" +
                "VALUES\n" +
                "(\"%s\", \"%s\", \"%s\", \"%s\", \"%s\", \"%s\", \"%s\", \"%s\",\n" +
                "\"%s\", \"%s\", \"%s\", \"%s\", \"%s\", \"%s\", \"%s\")",
                this.id, this.name, this.grade, this.anniversary, this.establishForm, this.mainSchool, this.operationalStatus, this.jiBunAddress,
                this.roadNameAddress, this.sidoOfficeOfEducationCode, this.sidoOfficeOfEducation, this.smallOfficeOfEducationCode, this.smallOfficeOfEducation, this.latitude, this.longitude);
        return sql;
    }

    public String getTupleString() {
        String sql = String.format("(\"%s\", \"%s\", \"%s\", \"%s\", \"%s\", \"%s\", \"%s\", \"%s\",\n" +
                "\"%s\", \"%s\", \"%s\", \"%s\", \"%s\", \"%s\", \"%s\")",
                this.id, this.name, this.grade, this.anniversary, this.establishForm, this.mainSchool, this.operationalStatus, this.jiBunAddress,
                this.roadNameAddress, this.sidoOfficeOfEducationCode, this.sidoOfficeOfEducation, this.smallOfficeOfEducationCode, this.smallOfficeOfEducation, this.latitude, this.longitude);

        return sql;
    }
}

SQL의 INSERT문은 다음의 형식과 같다.

INSERT INTO `스키마명`.`데이터 테이블명`
VALUES
(각 컬럼에 넣을 데이터들)

그리고 여러 데이터를 넣는 경우에 다음과 같이 작성할 수 있다.

INSERT INTO `스키마명`.`데이터 테이블명`
VALUES
(각 컬럼에 넣을 데이터들1),
(각 컬럼에 넣을 데이터들2),
			.
            .
            .

이렇게 작성하면 번거롭게 한줄씩 데이터를 테이블에 넣지 않아도 되어 시간이 절약된다.

그래서 JAVA로 SQL의 INSERT문을 작성할 때에도 getSqlInsertQuery() 메서드와 getTupleString() 메서드를 별도로 구현하여 하나의 데이터를 넣을 때는 getSqlInsertQuery()만 이용하고 여러 개의 데이터를 넣을 때는 getTupleString() 메서드까지 사용하여 한번에 INSERT 문을 작성할 수 있도록 코드를 구현하였다.

 

👇 SchoolTest.java

package com.bigdata.domain;

import com.bigdata.parser.Parser;
import com.bigdata.parser.SchoolParser;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class SchoolTest {

    @Test
    @DisplayName("SQL INSERT문이 잘 작성되는지 확인")
    public void makeSqlInsertTest() {
        Parser<School> schoolParser = new SchoolParser();
        String line1 = "B000003406,대전비래초등학교,초등학교,2005-03-01,공립,본교,운영,대전광역시 대덕구 비래동 245-1,대전광역시 대덕구 우암로492번안길 65,7430000,대전광역시교육청,7441000,대전광역시동부교육지원청,2013-11-29,2021-07-05,36.363531243,127.45289438,2021-09-15,7001220,한국교원대학교";
        String line2 = "B000004955,북삼초등학교,초등학교,1946-01-24,공립,본교,운영,강원도 동해시 지흥동 124-1,강원도 동해시 지양길 77,7800000,강원도교육청,7841000,강원도동해교육지원청,2013-11-29,2021-09-30,37.4956557859,129.1009681324,2022-03-24,C738100,청주대학교 지방교육재정연구원";
        School school1 = schoolParser.parse(line1);
        StringBuilder sql = new StringBuilder();
        sql.append(school1.getSqlInsertQuery());
        sql.append(",\n");
        School school2 = schoolParser.parse(line2);
        sql.append(school2.getTupleString());
        String result = "INSERT INTO `test-db`.`nation_wide_school`\n" +
                "VALUES\n" +
                "(\"B000003406\", \"대전비래초등학교\", \"초등학교\", \"2005-03-01T00:00\", \"공립\", \"true\", \"운영\", \"대전광역시 대덕구 비래동 245-1\",\n" +
                "\"대전광역시 대덕구 우암로492번안길 65\", \"7430000\", \"대전광역시교육청\", \"7441000\", \"대전광역시동부교육지원청\", \"36.363531243\", \"127.45289438\"),\n" +
                "(\"B000004955\", \"북삼초등학교\", \"초등학교\", \"1946-01-24T00:00\", \"공립\", \"true\", \"운영\", \"강원도 동해시 지흥동 124-1\",\n" +
                "\"강원도 동해시 지양길 77\", \"7800000\", \"강원도교육청\", \"7841000\", \"강원도동해교육지원청\", \"37.4956557859\", \"129.1009681324\")";
        assertEquals(result, sql.toString());
    }
}

 

💡 실행 결과

Sql INSERT문 생성 테스트 실행 결과

INSERT문도 잘 생성된 것을 확인할 수 있었다.

 

다음에는 일부분이 아닌 전체 데이터를 바꿔보아야겠다.