csv 파서 JAVA로 만들어보기

현재 노트: csv 파서 JAVA로 만들어보기
상위 분류: [[]]

#A

재 노트: Java-Project-002 CSV 파싱 프로젝트 회고 상위 분류: Java-Projects

#Java #CSV #Swing #DataParsing #Troubleshooting

## [Java] 공공데이터 CSV 파싱 및 Swing UI 구현 - 트러블슈팅 기록

### 프로젝트 개요

목표: 공공데이터 포털에서 제공하는 '지역 축제 정보' CSV 파일을 파싱하여 Java Swing UI에 표시하는 애플리케이션을 구현한다. 데이터 소스: 문화체육관광부 전국 지역축제 개최계획

단순한 CSV 파싱 작업으로 예상했으나, 데이터의 비정형성으로 인해 여러 기술적 문제에 직면했다. 이 글은 그 해결 과정을 기록한 트러블슈팅 문서이다.


### 1. 데이터 전처리: 파싱 이전에 필요한 작업

초기 CSV 데이터는 셀 병합, 여러 줄의 헤더, 불필요한 메타데이터가 포함되어 있어 프로그래밍으로 즉시 처리하기 어려운 상태였다.

▼ 원본 CSV 헤더의 모습

2025년 연간 지역축제 개최 계획,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
연번,광역자치단체명,기초자치단체명,축제명,축제 유형,개최 장소,,,,,개최기간,,,,,,,,개최 주기,개최 방식,"최초
개최연도",,예산(백만원),,,방문객수(前년),,,전담조직(축제사무국),,,"국비지원
부처명",담당,,
,,,,,장소명,축제 유형,지자체명,,,시작일,,,종료일,,,"총
일수",비고,,,,합계,국비,지방비,"기타
(민간)",전체, 내국인(명) ,외국인(명),전담조직명,조직형태,설립근거,,기관명,부서명,연락처
,,,,,,,시도,시군구,읍면동,년,월,일,년,월,일,,,,,,,,,,,,,,,,,,,

따라서 파싱 로직의 복잡성을 줄이기 위해, 프로그래밍에 앞서 다음과 같은 데이터 전처리 원칙을 세웠다.

  1. 헤더 단일화: 여러 줄로 나뉜 헤더를 분석하여, 프로그래밍에 사용할 단 한 줄의 명확한 헤더로 통합했다.

  2. 컬럼 선택: 모든 데이터가 필요한 것은 아니므로, 사용할 열을 미리 선별하고 나머지는 과감히 삭제했다.

  3. 데이터 포맷 정리: 줄바꿈이 포함된 셀("최초\n개최연도")이나 후행 쉼표 문제를 해결하여 데이터의 일관성을 확보했다.


### 2. 애플리케이션 설계: 역할의 분리

파싱 로직과 UI 로직, 데이터 관리를 분리하기 위해 다음과 같이 계층을 나누어 설계했다. 각 계층의 역할을 명확히 하기 위해 '광산 채굴'을 메타포로 사용했다.


### 3. 파서 구현: 단계별 트러블슈팅

#### 3-1. 문제: 불필요한 후행 쉼표

엑셀에서 불필요한 열을 삭제했음에도, CSV 파일에 수많은 후행 쉼표가 남아있는 문제가 발생했다.

축제명,광역자치단체명,...,연락처,,,,,,,,,,,,,,,,,,,,,
춘천 감자페스타,강원,춘천시,...,033-250-3070,,,,,,,,,,,,,,,,,,,,,
#### 3-2. 문제: 인코딩 및 숫자 변환 오류

파서 실행 시 한글이 깨지고 NumberFormatException이 발생했다.

CSV 파싱 중 오류가 발생했습니다.
java.lang.NumberFormatException: For input string: " "
	at java.base/java.lang.Integer.parseInt(Integer.java:778)
	at com.example.trip.util.FestivalCsvParser.parse(FestivalCsvParser.java:50)
	at com.example.trip.util.FestivalCsvParser.main(FestivalCsvParser.java:74)
...
FestivalDTO [festivaltitle=õ 佺Ÿ, province=, city=õ, ... ]
#### 3-3. 문제: 데이터 내 쉼표와 String.split()의 한계

"주 행사장(공지천일원), 권역별 행사장(...)"과 같이 따옴표로 묶인 필드 내에 쉼표가 포함된 경우, 단순 line.split(",")은 컬럼 인덱스를 밀리게 했다.

이 복잡한 정규표현식의 핵심 아이디어는, split의 구분자인 쉼표(,)를 만날 때마다 그 뒤의 문자열 전체를 미리 훑어보는 '전방탐색 (?=...)' 기법을 사용하는 것입니다. 미리보기를 통해 "이 쉼표 뒤에 남아있는 쌍따옴표의 총개수가 짝수인가?"를 확인하고, 짝수일 때만(즉, 쌍따옴표 밖에 있을 때만) 분리하는 매우 스마트한 방식입니다.

이 검사 과정을 **'보안팀'**에 비유하면 쉽게 이해할 수 있습니다.

  1. 보안팀장 (?=...): 쉼표를 발견할 때마다 "지금부터 내가 지정하는 패턴으로 쉼표 뒤 전체 문자열을 검사하라!"고 지시합니다.

  2. 1차 검사원 (?:[^"]"[^"]")*: **'쌍따옴표 묶음 처리 전문가'입니다. 4단계의 내부 검사([^"] → " → [^"] → ")를 통해 쌍따옴표 묶음을 찾아내고, * 덕분에 이 작업을 여러 번 반복할 수 있습니다. 자신이 처리(소비)한 부분을 제외한 **'남은 것'**을 2차 검사원에게 넘깁니다.

  3. **2차 검사원 [^"]*:.1,"""()까지 무사히 도착하는지"를 한 글자씩 엄격하게 검사합니다.

  4. 최종 판정: 1차, 2차 검사원이 모두 임무를 성공해야만(AND 조건), 보안팀장은 해당 쉼표를 '안전한 분리 지점'으로 판단합니다.

이 로직이 실제 데이터에서 어떻게 동작하는지 두 가지 케이스로 비교해 보겠습니다.


#### 3-4. 문제: 불완전한 데이터와 예외 처리

특정 행에 컬럼이 누락되어 ArrayIndexOutOfBoundsException이 발생하거나, 숫자여야 할 필드에 문자열이 들어가 NumberFormatException이 발생하는 등 데이터 품질 문제가 있었다.

#### 3-5. 문제: 중복된 헤더 이름

시작일,월,일,종료일,월,일 처럼 헤더 이름이 중복되어 '헤더 맵'에서 Key 값이 덮어씌워지는 문제가 발생했다.


### 4. 리팩토링 및 기능 연동: 두 데이터를 연결하기

지금까지는 축제 정보를 독립적으로 보여주는 기능까지만 구현했다. 하지만 프로젝트의 최종 목표는 기존 관광지 정보와 축제 정보를 연동하는 것이었다. 이를 위해 아키텍처를 완성하고 View를 수정하는 작업을 진행했다.

#### 4-1. 고민: Parser와 DAO의 역할 분리

초기 Parser는 CSV를 읽는 동시에, 파싱된 결과를 List 형태로 직접 보관하고 있었다. 이는 Parser가 DAO의 역할까지 침범한 상태였다. 연동 기능을 구현하기에 앞서, 각 클래스의 역할을 명확히 하고자 Parser에서 DAO의 기능을 분리하는 리팩토링을 수행했다.

#### 4-2. Service 계층 구현

DAO를 통해 얻은 데이터를 가지고 실제 비즈니스 로직을 처리할 FestivalService를 구현했다. 특히 핵심 기능인 searchByCity(TripDto tripDto) 메소드는 아래와 같은 책임을 갖는다.

  1. TripDto(관광지 정보)를 입력받는다.
  2. tripDto.getStreetAddress()로 주소를 추출한다.
  3. 추출한 주소에 포함된 도시명으로 DAO에 축제 목록을 요청(festivalDao.searchByCity(city))한다.

이처럼 Service는 View로부터 상위 수준의 데이터(TripDto)를 받아, DAO가 이해할 수 있는 하위 수준의 데이터(String city)로 가공하여 전달하는 중요한 역할을 수행했다.
이번 기능은 단순하게 contains를 통해 구현

        for (FestivalDto festival : allFestivals) {
            String festivalCity = festival.getCity();
            
            // 관광지 주소(tripAddress)에 축제의 도시명(festivalCity)이 포함되어 있는지 확인
            if (festivalCity != null && !festivalCity.isEmpty() && tripAddress.contains(festivalCity)) {
                nearbyFestivals.add(festival);
            }
        }
#### 4-3. View 연동: 기존 UI에 기능 통합

최종적으로 기존 TripInfoView에 새로운 기능을 통합했다.


### 5. 회고

단순한 CSV 파싱 작업으로 시작했지만, 결국 기존 시스템에 새로운 데이터 소스를 '안전하게' 통합하는 설계 경험으로 이어졌다. 특히 '헤더 맵'을 이용한 유연한 파서 구현과, 각 계층(Parser, DAO, Service, View)의 역할을 명확히 분리하는 과정은 유지보수와 확장에 강한 코드를 만드는 것이 왜 중요한지 체감하는 계기가 되었다. 이 글이 나와 같이 실제 데이터를 다루며 아키텍처를 고민하는 개발자들에게 도움이 되기를 바란다.

===
심화기능 분석
CSV 데이터 전처리
필요한것 DTO 생성
DAO 생성
Service 생성
View 수정

한번더만들떄 parser전부 다시 수정해야함. 일반화 필요
변수명 비슷한거 좀 일반화해보기 예를들어 대부분 비슷한 FestivalDAO나 FestvalDAOImpl이나 FestivalService에서 이 Festival대신 Shop만 바꾸면 되는 데 일일히 바꿔줘야함

이미지 깨지는 것
깃헙 이슈올릴떄, 올리고난뒤
깃랩 이슈올릴