Skip to content

fix: EgovEscapableDelimitedLineTokenizer의 후행 빈 컬럼 유실과 생성자 구분자 초기화 누락 수정 - #301

Open
z3rotig4r wants to merge 1 commit into
eGovFramework:mainfrom
z3rotig4r:fix/escapable-tokenizer-trailing-empty-columns
Open

fix: EgovEscapableDelimitedLineTokenizer의 후행 빈 컬럼 유실과 생성자 구분자 초기화 누락 수정#301
z3rotig4r wants to merge 1 commit into
eGovFramework:mainfrom
z3rotig4r:fix/escapable-tokenizer-trailing-empty-columns

Conversation

@z3rotig4r

Copy link
Copy Markdown
Contributor

변경 이유

EgovEscapableDelimitedLineTokenizer에서 결함 세 가지를 확인해 한 커밋으로 묶었습니다. 셋 다 doTokenize의 같은 구간에서 나오고 수정 범위도 서로 겹칩니다. 따로 올리면 오히려 리뷰가 어려워집니다.

결함 A — 후행 빈 컬럼 유실

line.split(this.regexDelimiter)가 limit을 지정하지 않아 자바 split의 기본 동작대로 후행 빈 문자열이 잘려나갑니다. 토큰이 줄어든 채로 넘어가면 EgovObjectMapper.mapObject가 토큰 수와 names 길이가 다르다며 IncorrectTokenCountException을 던집니다. 마지막 컬럼이 빈 CSV 한 행 때문에 배치 스텝이 실패합니다. split(regex, -1)로 고쳤습니다.

결함 B — 생성자 구분자 초기화 누락

EgovEscapableDelimitedLineTokenizer(String delimiter) 생성자가 this.delimiter에 값만 넣고 setDelimiter()를 거치지 않아 regexDelimiter가 null인 채 남습니다. 생성 직후 doTokenize를 호출하면 곧바로 NullPointerException입니다. 생성자 javadoc은 "기본 delimiter로 콤마를 사용하도록 설정한다"고 적혀 있어 의도와도 어긋납니다. 저장소 안 모든 사용처가 생성 직후 setDelimiter()를 다시 호출하고 있어서 그동안 결함이 가려져 있었습니다. 초기화 로직을 private initDelimiter()로 빼고 생성자와 setDelimiter()가 함께 쓰도록 했습니다. 생성자에서 오버라이드 가능한 setDelimiter()를 부르지 않으려고 분리했습니다.

결함 C — 따옴표 필드가 뒤 컬럼을 삼킴

escape 상태머신이 따옴표 필드를 여러 셀에 걸쳐 이어붙일 때, 닫는 따옴표 검색 시작점으로 직전 셀에서 구한 quoteIndex + 1을 씁니다. 따옴표 필드의 내용이 구분자로 끝나면("b,"를 콤마로 나누면 "b"가 됩니다) 닫는 따옴표가 셀의 0번에 오는데 시작점이 1이라 닫힘을 찾지 못합니다. 그래서 "b,",c가 한 토큰으로 뒤 컬럼을 삼켰습니다. 검색 시작점을 셀 처음으로 바꿔 ["b,", "c"]로 나뉩니다. 후행 빈 컬럼 보존과 같은 지점의 결함이라 함께 고쳤습니다.

변경 내용

Batch/org.egovframe.rte.bat.core/src/main/java/org/egovframe/rte/bat/core/item/file/transform/EgovEscapableDelimitedLineTokenizer.java (13줄)

  • doTokenizeline.split(this.regexDelimiter)line.split(this.regexDelimiter, -1)로 바꿔 후행 빈 컬럼을 남깁니다.
  • setDelimiter()의 본문을 private initDelimiter()로 옮겼습니다. 생성자와 setDelimiter()가 같은 초기화를 씁니다.
  • 이어붙이는 셀에서 닫는 따옴표를 찾을 때 시작 위치를 quoteIndex + 1에서 셀 처음으로 바꿨습니다. 그에 맞춰 판정 조건도 closeIndex > 0에서 closeIndex >= 0으로 맞췄습니다.

Batch/org.egovframe.rte.bat.core/src/test/java/org/egovframe/rte/bat/core/item/file/transform/EgovEscapableDelimitedLineTokenizerEmptyColumnTest.java (신규 119줄)

  • 후행 빈 컬럼 보존, Spring Batch DelimitedLineTokenizer와의 결과 비교, EgovObjectMapper 매핑 성공, 생성자 직후 토큰화, escape 컬럼 처리, 따옴표 필드가 구분자로 끝나는 경우까지 6건입니다.

동작 변경(하위 호환성)

후행 빈 컬럼을 보존하면서 토큰 개수가 달라집니다.

입력 수정 전 수정 후
a,b,c, 3개 4개
a,b,, 2개 4개
,,, 0개 4개

지금까지 잘려나간 개수에 맞춰 names를 설정해 둔 잡은 이제 IncorrectTokenCountException을 만납니다. 기준은 Spring Batch 표준 DelimitedLineTokenizer와 같은 패키지의 EgovDelimitedLineTokenizer이며, 둘 다 후행 빈 컬럼을 보존하므로 이번 수정으로 세 구현의 동작이 일치합니다. 빈 라인은 수정 전후 모두 토큰 1개로 같습니다. setEscape(false) 경로는 후행 빈 컬럼 보존 말고 달라지는 것이 없습니다.

테스트 방법

mvn -o -pl Batch/org.egovframe.rte.bat.core test
  • 모듈 전체 67건 통과했습니다.
  • 정상 CSV 28행(따옴표 안 구분자, 이스케이프 이중따옴표, 빈 따옴표 필드, 따옴표가 마지막 셀에만 있는 행, 전체가 한 따옴표 필드인 행 등 조합)으로 대조하면 컬럼 수가 정확한 행이 12/28에서 28/28로 늘었습니다. 정상 행에서 나빠진 경우는 없습니다.
  • setEscape(false) 경로는 입력 9,841건 전수 대조에서 차이가 전부 후행 빈 컬럼 보존이며 값이 바뀌거나 토큰이 줄어든 사례는 없습니다.

테스트 변별력도 확인했습니다. 수정 hunk를 하나씩 되돌리면 각각 다른 테스트가 실패합니다. split(..., -1)만 되돌리면 4건, 닫는 따옴표 검색 시작점만 되돌리면 quotedTrailingDelimiter 1건, 생성자 초기화만 되돌리면 constructorInitializedDelimiter 1건이 깨져 세 결함이 따로 잡힙니다.

영향 범위

  • 배치 실행환경의 EgovEscapableDelimitedLineTokenizer 한 클래스입니다. 공개 API 시그니처는 그대로이고 initDelimiter()는 private입니다.
  • 구분자로 끝나는 행이나 마지막 컬럼이 빈 행을 읽는 CSV 배치 잡의 토큰 개수가 달라집니다. 위 하위 호환성 항목을 함께 봐 주십시오.
  • 다른 모듈의 코드는 건드리지 않았습니다.

doTokenize()가 String.split(regex)를 limit 없이 호출해 후행 빈 문자열을 잘라내고 있었다.
그 결과 CSV 마지막 컬럼이 빈 값이면 토큰이 통째로 사라진다.
"a,b,c," -> 3개, "a,b,," -> 2개, ",,," -> 0개.
EgovObjectMapper.mapObject()는 names 개수와 토큰 개수가 다르면
IncorrectTokenCountException을 던지므로 배치 스텝이 그대로 실패한다.
같은 패키지의 EgovDelimitedLineTokenizer와 Spring Batch 표준 DelimitedLineTokenizer는
둘 다 후행 빈 컬럼을 보존한다. split의 limit을 -1로 지정해 동작을 일치시켰다.

이 과정에서 escape 상태머신의 결함도 함께 드러났다.
따옴표 필드를 여러 셀에 걸쳐 이어붙이는 분기가 닫는 따옴표를 찾을 때
직전 셀에서 구한 quoteIndex + 1을 검색 시작점으로 쓰고 있었다.
닫는 따옴표가 셀의 0번에 오는 형태(`"b,"`는 콤마로 나누면 `"b`와 `"`가 된다)에서는
닫힘을 영영 찾지 못한다. 지금까지는 후행 빈 셀이 split에 잘려나가 결과가 우연히 맞았을 뿐이고,
빈 셀이 보존되는 순간 `a,"b,",`가 [a, `"b,",`]로 값이 오염된다.
검색 시작점을 셀 처음(0)으로 바꾸고 닫힘 판정을 closeIndex >= 0으로 완화했다.
이로써 `a,"b,",`는 [a, "b,", ""]로, 뒤 컬럼을 통째로 삼키던 `"b,",c`도 ["b,", "c"]로 정상화된다.

또한 생성자가 this.delimiter만 대입하고 regexDelimiter를 초기화하지 않아,
생성 직후 doTokenize()를 호출하면 정규식이 null이라 NullPointerException이 발생했다.
Javadoc은 기본 구분자로 콤마를 사용하도록 설정한다고 명시하지만 실제로는 동작하지 않았고,
기존 사용처가 모두 생성 직후 setDelimiter()를 다시 호출해 결함이 가려져 있었다.
setDelimiter()는 오버라이드 가능한 public 메서드라 생성자에서 직접 부르지 않고,
private initDelimiter()로 분리해 생성자와 setDelimiter() 양쪽에서 호출하도록 했다.
setQuoteCharacter()가 이미 final로 선언된 것과 같은 취지다.

회귀 테스트로 EgovEscapableDelimitedLineTokenizerEmptyColumnTest를 추가했다.
빈 컬럼 6개 케이스, Spring Batch 표준 토크나이저와의 결과 대조, EgovObjectMapper 매핑 성공,
따옴표 필드가 구분자로 끝나는 4개 케이스, 생성자 직후 토큰화, 기존 escape 동작 보존을 검증한다.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant