CSV파서 만들어서 기능연동해보기
Description: CSV파서 만들어서 기능연동해보기
현재 노트: csv 파서 JAVA로 만들어보기
상위 분류: [[]]
#Java #CSV #Swing #DataParsing #Troubleshooting
## [Java] 공공데이터 CSV 파싱 및 Swing UI 구현 - 트러블슈팅 기록
### 프로젝트 개요
목표: 공공데이터 포털에서 제공하는 '지역 축제 정보' CSV 파일을 파싱하여 Java Swing UI에 표시하는 애플리케이션을 구현한다. 데이터 소스: 문화체육관광부 전국 지역축제 개최계획
단순한 CSV 파싱 작업으로 예상했으나, 데이터의 비정형성으로 인해 여러 기술적 문제에 직면했다. 이 글은 그 해결 과정을 기록한 트러블슈팅 문서이다.
### 1. 데이터 전처리: 파싱 이전에 필요한 작업
초기 CSV 데이터는 셀 병합, 여러 줄의 헤더, 불필요한 메타데이터가 포함되어 있어 프로그래밍으로 즉시 처리하기 어려운 상태였다.
▼ 원본 CSV 헤더의 모습
2025년 연간 지역축제 개최 계획,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
연번,광역자치단체명,기초자치단체명,축제명,축제 유형,개최 장소,,,,,개최기간,,,,,,,,개최 주기,개최 방식,"최초
개최연도",,예산(백만원),,,방문객수(前년),,,전담조직(축제사무국),,,"국비지원
부처명",담당,,
,,,,,장소명,축제 유형,지자체명,,,시작일,,,종료일,,,"총
일수",비고,,,,합계,국비,지방비,"기타
(민간)",전체, 내국인(명) ,외국인(명),전담조직명,조직형태,설립근거,,기관명,부서명,연락처
,,,,,,,시도,시군구,읍면동,년,월,일,년,월,일,,,,,,,,,,,,,,,,,,,
따라서 파싱 로직의 복잡성을 줄이기 위해, 프로그래밍에 앞서 다음과 같은 데이터 전처리 원칙을 세웠다.
-
헤더 단일화: 여러 줄로 나뉜 헤더를 분석하여, 프로그래밍에 사용할 단 한 줄의 명확한 헤더로 통합했다.
-
컬럼 선택: 모든 데이터가 필요한 것은 아니므로, 사용할 열을 미리 선별하고 나머지는 과감히 삭제했다.
-
데이터 포맷 정리: 줄바꿈이 포함된 셀(
"최초\n개최연도"
)이나 후행 쉼표 문제를 해결하여 데이터의 일관성을 확보했다.
### 2. 애플리케이션 설계: 역할의 분리
파싱 로직과 UI 로직, 데이터 관리를 분리하기 위해 다음과 같이 계층을 나누어 설계했다. 각 계층의 역할을 명확히 하기 위해 '광산 채굴'을 메타포로 사용했다.
-
DTO: 데이터 구조를 정의하는 '광석(鑛石)'.
-
Parser: 원본 데이터를 DTO 형태로 가공하는 '도구(道具)'.
-
DAO: Parser를 이용해 데이터 소스에서 DTO 목록을 가져오는 '광부(鑛夫)'.
-
Service: DAO의 데이터를 조합/가공하는 '세공사(細工師)'.
-
View: 최종 결과물을 사용자에게 보여주는 '판매원(販賣員)'.
### 3. 파서 구현: 단계별 트러블슈팅
#### 3-1. 문제: 불필요한 후행 쉼표
엑셀에서 불필요한 열을 삭제했음에도, CSV 파일에 수많은 후행 쉼표가 남아있는 문제가 발생했다.
축제명,광역자치단체명,...,연락처,,,,,,,,,,,,,,,,,,,,,
춘천 감자페스타,강원,춘천시,...,033-250-3070,,,,,,,,,,,,,,,,,,,,,
-
원인: 엑셀에서
Delete
키로 '내용만 지웠기' 때문이었다. 셀 서식이 남아있어 빈 컬럼으로 인식된 것이다. -
해결: 열 알파벳을 직접 선택하여 마우스 우클릭 -> **'열 삭제'**를 통해 열 자체를 완전히 제거하여 해결했다.
#### 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=õ, ... ]
-
원인: CSV 파일이
ANSI 혹은 EUC-KR
등 다른 방식으로 인코딩되어 있었고, Java가 이를 잘못 해석하여 발생한 문제였다. -
해결: 파일을
UTF-8
인코딩으로 다시 저장하고, Java 코드에서도new FileReader(filePath, StandardCharsets.UTF_8)
로 인코딩을 명시하여 해결했다.
#### 3-3. 문제: 데이터 내 쉼표와 String.split()
의 한계
"주 행사장(공지천일원), 권역별 행사장(...)"
과 같이 따옴표로 묶인 필드 내에 쉼표가 포함된 경우, 단순 line.split(",")
은 컬럼 인덱스를 밀리게 했다.
- 기존:
String[] columns = line.split(",", -1);
- 시도: 정규 표현식
split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1)
을 사용해 임시 해결했다.
이 복잡한 정규표현식의 핵심 아이디어는, split의 구분자인 쉼표(,)를 만날 때마다 그 뒤의 문자열 전체를 미리 훑어보는 '전방탐색 (?=...)' 기법을 사용하는 것입니다. 미리보기를 통해 "이 쉼표 뒤에 남아있는 쌍따옴표의 총개수가 짝수인가?"를 확인하고, 짝수일 때만(즉, 쌍따옴표 밖에 있을 때만) 분리하는 매우 스마트한 방식입니다.
이 검사 과정을 **'보안팀'**에 비유하면 쉽게 이해할 수 있습니다.
-
보안팀장 (?=...): 쉼표를 발견할 때마다 "지금부터 내가 지정하는 패턴으로 쉼표 뒤 전체 문자열을 검사하라!"고 지시합니다.
-
1차 검사원 (?:[^"]"[^"]")*: **'쌍따옴표 묶음 처리 전문가'입니다. 4단계의 내부 검사([^"] → " → [^"] → ")를 통해 쌍따옴표 묶음을 찾아내고, * 덕분에 이 작업을 여러 번 반복할 수 있습니다. 자신이 처리(소비)한 부분을 제외한 **'남은 것'**을 2차 검사원에게 넘깁니다.
-
**2차 검사원 [^"]*
)까지 무사히 도착하는지"를 한 글자씩 엄격하게 검사합니다. -
최종 판정: 1차, 2차 검사원이 모두 임무를 성공해야만(AND 조건), 보안팀장은 해당 쉼표를 '안전한 분리 지점'으로 판단합니다.
이 로직이 실제 데이터에서 어떻게 동작하는지 두 가지 케이스로 비교해 보겠습니다.
-
사과, "배, 딸기", 포도 에서 사과 뒤의 쉼표를 만난 경우:
-
1차 검사원이 "배, 딸기" 덩어리를 성공적으로 "소비"하고, 2차 검사원에게 남은 , 포도를 넘깁니다.
-
2차 검사원은 , 포도에 더 이상 쌍따옴표가 없으므로 '합격' 판정을 내립니다.
-
최종 결과: 분리 성공!
-
-
사과, "배, 딸기", 포도 에서 배 뒤의 쉼표를 만난 경우:
-
1차 검사원은 자신의 목표물('여는 쌍따옴표')이 보이지 않으므로, 아무것도 소비하지 않고 그대로 2차 검사원에게 딸기", 포도 전체를 넘깁니다.
-
2차 검사원은 딸기", 포도를 검사하다 중간에 있는 "를 발견하는 순간 '불합격' 판정을 내립니다.
-
최종 결과: 분리 실패! (의도된 동작)
-
-
한계와 교훈: 이 정규표현식은 매우 강력했지만, 모든 예외 케이스(예: 줄바꿈이 포함된 쌍따옴표)를 처리하기엔 한계가 명확했고 가독성도 떨어졌습니다. 이 경험을 통해 "정규표현식은 만능 해결책이 아니며, 때로는 더 구조적인 접근이 필요하다"는 교훈을 얻었습니다. 이는 이후 '헤더 맵'이라는 더 안정적인 방식으로 파서를 개선하는 계기가 되었습니다.
#### 3-4. 문제: 불완전한 데이터와 예외 처리
특정 행에 컬럼이 누락되어 ArrayIndexOutOfBoundsException
이 발생하거나, 숫자여야 할 필드에 문자열이 들어가 NumberFormatException
이 발생하는 등 데이터 품질 문제가 있었다.
-
고민: 불완전한 데이터를 버릴 것인가, 아니면 기본값이라도 채워 넣을 것인가?
-
해결: '헤더 맵' 방식을 도입하여 컬럼 이름 기반으로 데이터를 추출하고, 각 필드를 채워 넣기 전에
if (columns.length > index)
와try-catch
를 사용해 방어 코드를 작성했다. 데이터가 없으면String
은""
로,int
는0
으로 기본값을 설정하여 안정성을 높였다.
#### 3-5. 문제: 중복된 헤더 이름
시작일,월,일,종료일,월,일
처럼 헤더 이름이 중복되어 '헤더 맵'에서 Key 값이 덮어씌워지는 문제가 발생했다.
- 해결: CSV 파일의 헤더를
시작월
,시작일자
,종료월
,종료일자
등으로 고유하게 직접 수정했다. 규모가 큰 프로젝트라면 Enum 등으로 헤더 이름을 상수로 관리하여 호출부의 변경을 최소화하는 방식도 고려해볼 만하다.
### 4. 리팩토링 및 기능 연동: 두 데이터를 연결하기
지금까지는 축제 정보를 독립적으로 보여주는 기능까지만 구현했다. 하지만 프로젝트의 최종 목표는 기존 관광지 정보와 축제 정보를 연동하는 것이었다. 이를 위해 아키텍처를 완성하고 View를 수정하는 작업을 진행했다.
#### 4-1. 고민: Parser와 DAO의 역할 분리
초기 Parser는 CSV를 읽는 동시에, 파싱된 결과를 List
형태로 직접 보관하고 있었다. 이는 Parser가 DAO의 역할까지 침범한 상태였다. 연동 기능을 구현하기에 앞서, 각 클래스의 역할을 명확히 하고자 Parser에서 DAO의 기능을 분리하는 리팩토링을 수행했다.
- Parser: 순수하게 CSV 파일을
List<DTO>
로 변환만 해주는 **'도구'**로 수정했다. - DAO: Parser를 호출하여 얻은 데이터 목록을 보관하고,
findAll()
,searchByCity()
등 데이터 접근 메소드를 제공하는 **'관리자'**로 역할을 명확히 했다.
#### 4-2. Service 계층 구현
DAO를 통해 얻은 데이터를 가지고 실제 비즈니스 로직을 처리할 FestivalService
를 구현했다. 특히 핵심 기능인 searchByCity(TripDto tripDto)
메소드는 아래와 같은 책임을 갖는다.
TripDto
(관광지 정보)를 입력받는다.tripDto.getStreetAddress()
로 주소를 추출한다.- 추출한 주소에 포함된 도시명으로 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
에 새로운 기능을 통합했다.
-
고민: 새로운
FestivalInfoView
를 만들까, 기존TripInfoView
를 재활용할까? -
결론: 새로운 창을 띄우는 것보다, 기존 화면의 목록 패널만 동적으로 교체하는 것이 더 나은 사용자 경험을 제공한다고 판단했다.
-
구현:
TripInfoView
의 상세 정보 패널에 '주변 축제 보기' 버튼을 추가했다.- 버튼 클릭 시, 현재 선택된
curTrip
의 주소를 기반으로festivalService.searchByCity(curTrip)
를 호출했다. - 반환된
List<FestivalDto>
를 사용해 기존JTable
의 헤더와 데이터를 축제 정보로 교체하는showFestivals()
메소드를 구현하고 호출했다.
### 5. 회고
단순한 CSV 파싱 작업으로 시작했지만, 결국 기존 시스템에 새로운 데이터 소스를 '안전하게' 통합하는 설계 경험으로 이어졌다. 특히 '헤더 맵'을 이용한 유연한 파서 구현과, 각 계층(Parser, DAO, Service, View)의 역할을 명확히 분리하는 과정은 유지보수와 확장에 강한 코드를 만드는 것이 왜 중요한지 체감하는 계기가 되었다. 이 글이 나와 같이 실제 데이터를 다루며 아키텍처를 고민하는 개발자들에게 도움이 되기를 바란다.
===
심화기능 분석
CSV 데이터 전처리
필요한것 DTO 생성
DAO 생성
Service 생성
View 수정
한번더만들떄 parser전부 다시 수정해야함. 일반화 필요
변수명 비슷한거 좀 일반화해보기 예를들어 대부분 비슷한 FestivalDAO나 FestvalDAOImpl이나 FestivalService에서 이 Festival대신 Shop만 바꾸면 되는 데 일일히 바꿔줘야함
이미지 깨지는 것
깃헙 이슈올릴떄, 올리고난뒤
깃랩 이슈올릴