스터디

[SpringBoot] [Java] OpenCSV 사용해서 CSV 파일 파싱하기!

PgmJUN 2021. 12. 16. 12:30
728x90
반응형

 

 

 

오늘은 CSV 파일 파싱을 손쉽게 할 수 있도록 도와주는 자바의 OpenCSV 라는 라이브러리를 사용해서, 이전에 길고 복잡하게 수행했던 save() 함수를 바꿔보려한다.

 

우선 저번에 만들었던 JpaParkRepository.java save() 함수를 기억해야한다.

 

save() 함수는 CSV 파일에 있는 데이터를 BufferedReader 로 한 라인씩 가져온 뒤 ,(콤마) 를 기준으로 컬럼들은 구분해서 DB의 각 컬럼에 저장시켜주는 동작을 수행했다.

 

하지만 ,(콤마) 를 기준으로 했을 때 컬럼의 데이터 안에 ',' 라는 문자가 포함되어 있을 때 파싱이 제대로 수행되지 않는 오류가 발생하여

 

windows 제어판에서 구분자를 다른 걸로 바꿔주는 귀찮은 작업을 수행하기도 했다.

 

 

[어따세워] CSV 파일 ,(콤마) 말고 다른 구분자로 변경하기!

오늘 포스팅할 내용은 프로젝트 중에 발견한 애로사항과 그 해결 과정을 공유해서 같은 문제를 겪는 사람들에게 도움이 되고자 하는 마음에 올리는 포스팅이다! 바로 본론으로 들어가겠다. C

pgmjun.tistory.com

 

 

OpenCSV 는 이러한 문제점들을 해결한 아주 효율적인 라이브러리다.

 

OpenCSV 를 통해 CSV파일을 파싱하면 쌍따옴표("") 안에 있는 ,(콤마) 는 건드리지 않아 위와 같이 구분자를 변경할 필요가 없다.

 

또한 CSV 파일의 형식이 무엇이든 한글이 깨지지 않도록 변환할 수 있어 간단하게 파싱할 수 있다.

 

바로 시작해보도록 하자.

 

 

 


CsvParser 만들기

 

 

우선 OpenCSV 라이브러리 사용을 위해 Pom.xml 에 dependency 추가하고 시작하자.

<dependency>
   <groupId>com.opencsv</groupId>
   <artifactId>opencsv</artifactId>
   <version>5.5.2</version>
</dependency>

 

 

내가 하려는 것은 save() 함수의 코드 길이를 줄이기 위해 CsvParser 클래스를 따로 만들어 '파싱 기능' 과 '저장 기능' 을 구분하는 것이기 때문에 아래와 같이 클래스를 새로 만들었다.

 

 

CsvParser.java

public class CsvParser {

    private final EntityManager em;
    boolean isDuplicate = false;

    public CsvParser(EntityManager em) {
        this.em = em;
    }


    public void read() {

        //csv파일의 절대경로 구하기
        String path = System.getProperty("user.dir");   //csv파일 path 저장
        System.out.println("path = " + path);

        //저장했던 Park 객체를 저장하는 리스트 ( 중복 검사에 사용 )
        ArrayList<Park> parkList = new ArrayList<>();

        String[] parkInfo;

        try {
            //utf-8 형태로 csv 파일 파싱
            CSVReader csvReader = new CSVReader(new InputStreamReader(new FileInputStream(path + "\\src\\main\\java\\jh\\ParkingService\\repository\\park\\data.csv"), "EUC-KR"));

            csvReader.readNext(); // 컬럼명은 저장되지 않도록 한 줄 읽기

            do {    //파일에서 데이터를 읽어 파싱하고 Park 객체로 만들어 ArrayList 에 넣는다.
                parkInfo = csvReader.readNext();    //한 라인 읽기 (자동으로 콤마 분리해서 배열에 저장 됌)

                if (parkInfo != null) {
                    if (parkInfo[28] == null || parkInfo[29] == null || parkInfo[28].isEmpty() || parkInfo[29].isEmpty())  //읽어온 데이터의 위도, 경도 값이 없거나 null 이면 저장하지 않고 넘김
                        continue;
                    else if (checkDuplicate(parkInfo[0], parkList))  //주차장 코드가 중복(checkDuplicate 값이 true)이면 저장하지 않고 넘김
                        continue;
                    else {  //위의 두 조건에 해당사항이 없으면 데이터를 객체에 저장 후 임시 저장 ArrayList에 삽입
                        Park park = new Park();   //Park 객체 생성하기

                        park.setPrkplceNo(parkInfo[0]);      //객체에 값 저장하기
                        park.setPrkplceNm(parkInfo[1]);
                        park.setPrkplceSe(parkInfo[2]);
                        park.setPrkplceType(parkInfo[3]);
                        park.setRdnmadr(parkInfo[4]);
                        park.setLnmadr(parkInfo[5]);
                        park.setPrkcmprt(parkInfo[6]);
                        park.setFeedingSe(parkInfo[7]);
                        park.setEnforceSe(parkInfo[8]);
                        park.setOperDay(parkInfo[9]);
                        park.setWeekdayOperOpenHhmm(parkInfo[10]);
                        park.setWeekdayOperCloseHhmm(parkInfo[11]);
                        park.setSatOperOperOpenHhmm(parkInfo[12]);
                        park.setSatOperCloseHhmm(parkInfo[13]);
                        park.setHolidayOperOpenHhmm(parkInfo[14]);
                        park.setHolidayCloseOpenHhmm(parkInfo[15]);
                        park.setParkingchrgeInfo(parkInfo[16]);
                        park.setBasicTime(parkInfo[17]);
                        park.setBasicCharge(parkInfo[18]);
                        park.setAddUnitTime(parkInfo[19]);
                        park.setAddUnitCharge(parkInfo[20]);
                        park.setDayCmmtktAdjTime(parkInfo[21]);
                        park.setDayCmmtkt(parkInfo[22]);
                        park.setMonthCmmtkt(parkInfo[23]);
                        park.setMetpay(parkInfo[24]);
                        park.setSpcmnt(parkInfo[25]);
                        park.setInstitutionNm(parkInfo[26]);
                        park.setPhoneNumber(parkInfo[27].replace("-", ""));
                        park.setLatitude(parkInfo[28]);
                        park.setLongitude(parkInfo[29]);
                        park.setReferenceDate(parkInfo[30]);
                        park.setInsttCode(parkInfo[31]);
                        park.setInsttNm(parkInfo[32]);

                        parkList.add(park);   //리스트에 Park 객체 저장하기
                        em.persist(park);   //park 객체를 DB에 INSERT
                    }
                }
            } while (parkInfo != null);
        } catch (IOException e) {
            System.out.println(e.getMessage());
        } catch (EntityExistsException | CsvValidationException e) {
            e.printStackTrace();
        }
    }


    private boolean checkDuplicate(String prkplceNo, List<Park> list) {
        for (Park data : list) {
            isDuplicate = (data.getPrkplceNo().equals(prkplceNo));
            if (isDuplicate) {  //중복 발견시 반복문 종료
                break;
            }
        }
        System.out.print("prkplceNo = " + prkplceNo);
        System.out.println(" isDuplicate = " + isDuplicate);
        return isDuplicate; //중복 여부 리턴
    }

}

 

변경 1.

 

파싱작업은 BufferedReader를 통해 String 형태로 받아온 데이터를 split(",") 로 구분하는 것에서

 

CsvReader 를 사용해 CSV 파일의 각 컬럼을 String[] 배열에 바로 넣어주는 것으로 변경했다.

 

CsvReader가 ,(콤마)를 기준으로 데이터를 알아서 분리해주고, readNext() 함수로 한 Row 씩 배열에 저장해서 쓸 수 있기 때문에 split(",") 같은 코드를 작성할 필요가 없다.

 

 

변경 2.

 

 Park 객체의 Setter 부분에서 삼항연산자를 사용하지 않는다.

 

CsvReader가 빈 데이터는 자동으로 "" 처리 해주기 때문에, 굳이 삼항 연산자로 비어있는 데이터를 "" 로 바꿔주지 않아도 된다.

 

 

변경 3.

 

CsvReader 를 사용할 때

 

//euc-kr 형태로 csv 파일 파싱
CSVReader csvReader = new CSVReader(new InputStreamReader(new FileInputStream(path + "\\src\\main\\java\\jh\\ParkingService\\repository\\park\\data.csv"), "EUC-KR"));

 

CSVReader 생성 코드에 "EUC-KR" 을 포함하여 작성해주면 한글 깨짐을 방지해준다. ("UTF-8" 로 지정 시 한글깨짐)

 

다운받은 ANSI 인코딩 형태의 CSV 파일을 굳이 번거롭게 UTF-8 인코딩으로 변경하여 사용하지 않아도 된다는 소리다.

 

 

변경 4.

 

CheckDuplicate() 함수의 ForEach 문을 For 문으로 변경하였다.

 

자세한 이유는 모르겠으나 ForEach 문이 OpenCSV 라이브러리를 사용하자 갑자기 동작이 제대로 되지 않는 문제가 발생했다.

 

For 문과 ForEach 문의 차이를 공부해야겠으며 아직 배울 것이 많다는 생각이 들었다.

 

이제 CsvReader 를 만들었으니 JpaParkRepository 의 save() 함수를 수정해보자.

 

 


JpaParkRepository.save() 수정

 

 

엄청나게 길었던 save() 함수가 엄청나게 짧아졌다.

 

JpaParkRepository.save() 

@Override
public void save() {
    //PARK_DATA 초기화
    Query q = em.createQuery("DELETE FROM Park");
    q.executeUpdate();

    CsvParser parser = new CsvParser(em);
    parser.read();
}

 

이래서 기능 별로 클래스를 만들어 구분하라는 말이 괜히 있는 것이 아닌 것 같다.

 

코드가 깔끔하니 정말 보기 좋다.

 

 


실행 테스트

 

 

제대로 저장된 DB 화면

 

실행 테스트 결과 DB의 Table 에 모든 데이터들이 제대로 저장되었다.

 

구분자를 제어판에서 변경하지 않아도 컬럼 내부의 ,(콤마) 까지 제대로 처리되어 저장되었다.

 

 

 

 

이렇게 OpenCSV 라이브러리를 사용해 CSV 파일 Parsing 수행을 해보았습니다.

 

내가 겪은 불편함은 이미 위에 있는 프로그래머들이 먼저 겪은 것이고 그것을 해결하기 위한 방법은 이미 다 마련되어져 있다는 말이 정말 틀린 말이 아닌 것 같습니다.

 

이번에 새로 알게된 OpenCSV를 공부해보며 우리는 더 나은 기술을 쓰는 개발자가 되기 위해 계속 공부하고 찾아야한다고 제대로 느꼈습니다.

 

 

다음 시간에는 하루마다 주차장 데이터를 자동으로 최신화 하도록 코드를 작성해보겠습니다. 읽어주셔서 감사합니다.

728x90
반응형