본문 바로가기

프로젝트2 : 개발 관련 유틸리티

CSV file parser

csv 파일을 읽어 2가지 타입으로 저장할 수 있는 파서가 필요해졌다.

 

json파서나 csv파서나 나와있는 오픈소스들이 있지만 (rapidjson 같은...) 필요 이상의 기능이 많거나, 최소 기능뿐이라 결국 어느 정도 커스텀을 해야 했다. 그래서 직접 만들기로 했다.

 

그래서 이후 사용할 간단한 파서를 하나 만들었고, 필요에 의해

  • 2차원 벡터를 이용해 csv 모양 그대로 저장하는 것
  • 첫 번째 행을 key로 갖는 map의 벡터로 저장하는 것

두 가지 타입으로 준비했다.

 

메모리를 csv 파일로 출력할 때, 두 번째 타입은 key의 순서가 깨진다는 단점과,  key의 충돌 등 해결할 문제가 조금 남아있으므로 현재 진행 중인 토이 프로젝트를 간단히 마무리한 이후 천천히 수정할 예정이다.

 

 

GitHub - YoungWoo93/csvParser

Contribute to YoungWoo93/csvParser development by creating an account on GitHub.

github.com

 

 

2022 06 30 추가

두가지 문제를 해결하려 한다.
1. hash map 형태로 저장하는 상황에서 key가 충돌하는 경우
2. hash map 형태로 저장하는 상황에서 file out 시 key 순서가 뒤죽박죽이 되는 경우

우선 1번 문제는 파일 오픈 단계에서 첫 라인 (key 리스트를 받아오는 단계)에서 충돌 상황을 감지 가능하다.
이때, 충돌이 발생했다면 해당 파일을 아예 처리하지 않는 것이 맞는 것 인가? 아니면 key를 수정하여 저장하는 것이 맞는 것인가?

몇 가지 상황을 상정하고 해당 문제의 정책을 결정해보자.

우선 RDB에서 CSV를 획득한 상황이라면, 애초 key가 충돌하는 상황이 존재할 수 없다.
반대로 CSV 파일을 DB에 넣는 상황이라면 CSV 파일 자체는 사용자가 임의로 만든 상황이기 때문에 충돌 가능하다.
그런데 mysql 등의 dbms에서는 csv 파일의 key를 어떤 컬럼으로 매핑할지 수정 가능하기 때문에, 임의로 변경 한 상황에서도 추가가 가능하다. 역으로 임의로 변경하지 않아도 추가가 가능하다, 머리가 복잡해서 착각한듯...

즉 DB와의 연동을 고려한다면 key 충돌 시 임의의 변경 과정 (아마 뒤쪽에 인덱스 번호를 넣으면 되지 않을까?) 후에 저장하는 형태가 낫다고 생각된다. (외부 에디터와의 연동을 생각하면, 파일 출력 시 인코딩 방식을 결정할 수 있게 해야겠다.) 임의로 변경하지 않는 편이 훨씬 낫다, 왜냐하면 임의로 변경되면 변경된 사실조차 사용자가 모른 채로 문제가 감춰질 수 있다. 그것이 더 큰 잠재적 문제를 유발할 가능성이 있다.

 


사람이 엑셀 등으로 CSV 파일을 만들고, 직접 확인하는 경우를 생각해보자.
이경우는 키 충돌 여부를 별도 처리할 필요가 없다. 충돌되었다는 경고를 던진 뒤, 어떻게든 처리해주면 이후 경고를 보고 사용자가 수정할 것 이기 때문에 임의 수정 절차를 두어도 되고.
에러를 띄우고 아예 처리를 하지 않아도, 사용자가 그것을 보고 수정을 할 것 이기 때문에 문제가 없다고 생각되었다.

파일의 포맷이 중요한 상황이라면 에러를 띄우고 수정을 강제해야 하는 것 이 맞겠지만 현재 프로젝트에서 사용할 도구로써는 그러한 강제성의 장점이 없다고 생각한다. 
현재 프로젝트에서 그러한 강제성의 장점이 없는 것은 맞다. 하지만 위의 DB 연동 시나리오에서 치명적인 문제를 발생시킬 수 있기 때문에 프로그램적 수정을 하면 안 된다.

프로그램이 CSV 파일을 만들고, 사람이 확인해야 하는 경우를 생각해보자.
이경우 충돌처리는 꼭 필요해진다. 같은 키를 등록하려는 시점에 충돌 여부를 체크해서 존재한다면 에러를 반환해주는 것이 맞다고 생각된다.
개발자가 (= 내가) 실수로 인하여 같은 의미의 키를 중복 등록했을 수 도 있고, 다른 의미지만 이미 등록된 키를 확인 안 하여 실수로 중복 등록하는 경우에 구분이 불가능하다.

즉 프로그래밍 방식으로 새로운 키를 등록할 때에 중복은 에러로 처리되어야 한다.



이상의 생각을 정리한 결과
hash map 형태로 저장하는 상황에서 key가 충돌하는 경우에 대해서는

1. read file 단계에서는 중복되는 key 뒤에 괄호를 이용한 넘버링을 하여 이후에 구분한다 (ex : key, key(1), key(2)...) 이렇게 하면 안 된다. 중복이 발생해도 그냥 처리한다.
2. 프로그래밍 방식으로 key를 추가하는 상황에서 중복이 발생하면 예외를 던진다.
3. 위의 규칙에 의하면 file write 단계에서는 중복되는 키가 발생할 수 없다. 대신 키의 중복이 발생했음을 CSV 자료구조에서 인지는 해야 한다. 그래서 프로그램으로 특정 키를 이용해 값에 접근할 때, 그 키가 중복된 키 일 경우 모호한 접근에 대한 예외를 던진다.

1,2번 해법에 의해 삽입 도중 예외가 발생할 수 있지만 그건 입력 파일의 포맷이 이상한 상황이므로 (= 모호한 상황에서의 입력 이므로) 처리를 하면 안 된다고 생각한다.

 

2022 07 13 추가

키의 순서를 저장해서 처리하고자 하는데 그렇게 되면 CSV 파일(또는 파서) 마다 key의 순서 정보를 가지고 있어야 한다. 결국 key - value 맵과, key 순서 리스트를 가진 새로운 자료구조를 만드는 것과 사실상 동일해져 버렸다.

그런 이유로 2차원 벡터 형태로 내부에서 데이터를 저장하고, [] 연산자를 오버라이딩하는 형태의 CSV 클래스를 만드는 방향으로 전환하였다.

이경우 내부에서 O(1) 연산을 지원하기 위해 key - index 맵을 만들고, 이 index를 key의 순서로 하기로 하였다. 그리고 field 메소드를 이용해 key에 해당하는 index를 획득할 수 있게 처리하고자 한다. (예를 들어 유저 리스트 1번 유저의 이름에 접근하고자 한다면 -> user[1][user.field("name")] 형태로 접근)

그런데 이렇게 내부에 데이터를 저장하게 되면 이 데이터를 2차원 벡터 형태로 저장할지, map 형태로 저장할지 에 따라 분기가 생긴다. 왜냐하면 2차원 벡터의 첫 번째 행은 중복 데이터를 허용하고, 맵 형태는 허용하지 않는 특성 때문이다.

1. 중복을 에러 처리해 버리면 -> 2차원 벡터로 쓰려했는데 에러 처리됨
2. 중복을 넘버링해 버리면 -> 2차원 벡터로 쓰려했는데 값이 바뀌어버림
3. 중복을 무시해 버리면 -> 맵으로 쓰려했는데 예상하지 못한 동작을 유발하거나,
                                       -> 2차원 벡터로 쓰려했어도 key - index 맵에서 에러가 발생하거나,
                                       -> 맵으로 쓰려했는데 실수를 복원하지 못하거나

등의 문제가 예상된다.

가능한 해결 방안으로는
1. vector CSV 와 map CSV 클래스를 분리한다.
2. readVector, readMap 함수를 분리한다.
3. 우선 중복을 무시하고, key - index map을 만들 때, 중복된 키가 들어오면 index를 -1 등으로 설정한 뒤 이후 모호한 키에 대한 접근을 하려 할 때 예외를 발생시킨다.

등의 해결법이 떠오른다.

해결법 1의 장점은 가장 성능이 좋을 것이다, 또한 vector 형태로 읽은 CSV 파일에 key - index 맵을 만들 필요가 없어서 개념상 올바르다. 하지만 굳이 두 개의 클래스로 나누어야 하는지에 대한 찜찜함이 남아있다.

해결법 2의 장점은 하나의 클래스로 갈 수 있다는 점이다. 또한 key-index 맵을 위한 공간은 할당되어도 실제로 해당 맵을 구성 하지는 않기 때문에 정말 정말 약간의 속도 향상이 있을 것이다. (어차피 map 형태이므로 없다고 봐도 무방할 것이다.), 단점으로는 vector 타입의 CSV 파일에 field 메소드를 노출시켜야 한다는 점이다. 또한 map 형태의 CSV 파일에 랜덤 엑세스 접근을 노출해야 한다.

해결법 3의 장점은 사용자가 이용할 때 용도를 구분하지 않아도 된다는 아름다움(?) 이 있다. 하지만 해결법 2의 단점을 공유한다


이상의 생각을 정리한 결과 하루 만에 결론을 내리기 힘들었다.
잠시 있고 지내다가 신선한 머리로 위의 고민을 다시 읽어보고 더 매력적인 방법을 선택하기로 한다.

 

2022 07 15 추가

7월 13일에 추가된 내용에 대해서 3번이 가장 올바르다고 생각되었다.
mysql 등에서도 컬럼의 인덱스를 이용할 수 있다는 점 때문에 해결법 2의 단점이 사라졌고. 그렇게 되면 해결법 3번이 가장 nice하다고 생각되기 때문이다.

추가로 기존의 csvParser 로서의 기능이었던 parsing, making(이후 stringify로 이름 변경 예정) 함수는 전역 함수로 빼고, csvParser 클래스는 위에서 이야기했듯 CSV 포멧의 데이터를 저장하는 컨테이너로 변경하는 것이 더 맞는 것 같다.

마지막으로 vector를 typedef 하여 CSV에 맞는 이름을 만들어주면 (예를 들면 vector<vector<string>> 을 row로 바꿔준다던지...) 기본적인 구현은 대강 끝날 것 같다.


몇 가지 수정사항과, 성능 테스트(메모리 누수 여부 라던지.... 없겠지 아마?)를 한 뒤 릴리즈 브렌치를 파서 버전 1.0으로 하면 될 것 같다.

std::string 을 이용함으로써 발생하는 오버헤드는 당장은 무시할 예정이다. 이후 조금 더 큰 프로젝트에 사용될 때 속도 영향이 크다고 느껴지면 그때 손보자.


메모리 & 속도 테스트 및 제어 함수 (add, insert, erase)  추가 후 release 브렌치 분기 완료

 

'프로젝트2 : 개발 관련 유틸리티' 카테고리의 다른 글

performance profiler  (0) 2022.07.16