Skip to content

fix: EgovEscapableDelimitedLineTokenizer 가 정규식 메타문자 구분자를 이스케이프하지 못하는 문제 수정 - #367

Merged
jei007 merged 1 commit into
eGovFramework:mainfrom
wantaekchoi:fix/regex-delimiter-metachar
Sep 10, 2026
Merged

fix: EgovEscapableDelimitedLineTokenizer 가 정규식 메타문자 구분자를 이스케이프하지 못하는 문제 수정#367
jei007 merged 1 commit into
eGovFramework:mainfrom
wantaekchoi:fix/regex-delimiter-metachar

Conversation

@wantaekchoi

Copy link
Copy Markdown
Contributor

수정 사유 Reason for modification

  • 버그수정 Bug fixes
  • 기능개선 Enhancements
  • 기능추가 Adding features
  • 기타 Others

수정된 소스 내용 Modified source

EgovEscapableDelimitedLineTokenizer.getRegexDelimiter() 가 정규식 메타문자 구분자를 이스케이프하지 못합니다.

이 메서드는 delimiter 를 String.split() 의 정규식으로 넘기기 전에 메타문자를 처리하는 자리입니다(EgovEscapableDelimitedLineTokenizer.java:101// regex 정규식에서 특수문자로 인식하는 것에 대한 처리). 그런데 205~211행 일곱 줄은 역슬래시가 붙은 두 글자를 찾습니다.

// EgovEscapableDelimitedLineTokenizer.java:205 (origin/main)
delimiter = delimiter.replace("\\(", "\\\\(");

replace() 의 첫 인자는 리터럴이라 "\\("\( 두 글자를 뜻합니다. 구분자로 쓰는 홑문자 ( 는 어디에도 걸리지 않고 그대로 통과합니다.

그 결과 140행 line.split(this.regexDelimiter, -1) 에서 ( ) { [PatternSyntaxException 으로 배치가 멈추고, ^ 는 행 처음에만 맞는 앵커로 해석되어 예외 없이 세 컬럼이 하나로 붙습니다.

이 클래스가 delimiter 를 정규식이 아니라 리터럴로 다룬다는 근거는 doTokenize() 안에 있습니다. 따옴표로 묶인 셀을 다시 이어붙일 때 원본 delimiter 를 그대로 끼워넣으므로, 정규식으로 받는 계약이라면 재조립된 토큰에 정규식 문자열이 섞여 나옵니다.

// EgovEscapableDelimitedLineTokenizer.java:162
incompleteToken += this.delimiter + arrLine[ii];

같은 패키지의 EgovDelimitedLineTokenizer 는 정규식을 쓰지 않아 같은 구분자를 그대로 처리합니다.

// EgovDelimitedLineTokenizer.java:115
delimiterIndex = line.indexOf(delimiter, beginIndex);

getRegexDelimiter() 에서 이어지는 네 줄은 홑문자를 받습니다.

// EgovEscapableDelimitedLineTokenizer.java:213
delimiter = delimiter.replace("*", "[*]");
delimiter = delimiter.replace("+", "[+]");
delimiter = delimiter.replace("$", "[$]");
delimiter = delimiter.replace("|", "[|]");

일곱 줄이 갈린 지점은 커밋 6b5529d 입니다. 이 커밋 전에는 같은 일곱 줄이 replaceAll 이었고, replaceAll 의 첫 인자는 정규식이라 "\\(" 가 홑문자 ( 에 매치했습니다. . \ ? 는 그 이전 구현에도 없어서 이 diff 에 없습니다. 이 세 문자는 회귀가 아닙니다.

$ git show --stat --patch 6b5529d
    replaceAll to replace
---
 .../transform/EgovEscapableDelimitedLineTokenizer.java     | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

...
-		delimiter = delimiter.replaceAll("\\(", "\\\\(");
...
+		delimiter = delimiter.replace("\\(", "\\\\(");
...

AS-IS / TO-BE

6b5529d 이전으로 되돌리는 대신 replace 를 둔 채 찾는 대상만 홑문자로 바꿨습니다. 이 메서드에 남아 있던 replaceAll69e822a$ 구분자에서 나던 IllegalArgumentException 을 없애려고 걷어낸 것이라, 되돌리면 그 수정을 무르는 셈입니다. 홑문자 입력에 대한 결과 문자열은 6b5529d 이전과 같습니다.

     private String getRegexDelimiter(String delimiter) {
-        delimiter = delimiter.replace("\\(", "\\\\(");
-        delimiter = delimiter.replace("\\)", "\\\\)");
-        delimiter = delimiter.replace("\\{", "\\\\{");
-        delimiter = delimiter.replace("\\}", "\\\\}");
-        delimiter = delimiter.replace("\\^", "\\\\^");
-        delimiter = delimiter.replace("\\[", "\\\\[");
-        delimiter = delimiter.replace("\\]", "\\\\]");
+        delimiter = delimiter.replace("(", "\\(");
+        delimiter = delimiter.replace(")", "\\)");
+        delimiter = delimiter.replace("{", "\\{");
+        delimiter = delimiter.replace("}", "\\}");
+        delimiter = delimiter.replace("^", "\\^");
+        delimiter = delimiter.replace("[", "\\[");
+        delimiter = delimiter.replace("]", "\\]");
 
         delimiter = delimiter.replace("*", "[*]");

범위를 일곱 줄로 잡은 이유

일곱 문자를 문자별로 확인한 결과입니다.

구분자   결과
(        PatternSyntax Unclosed group near index 1
)        PatternSyntax Unmatched closing ')'
{        PatternSyntax Illegal repetition near index 1
}        [a, b, c]
^        [a^b^c]
[        PatternSyntax Unclosed character class near index 0
]        [a, b, c]

}] 는 Java 정규식이 짝 없는 닫는 문자를 리터럴로 받아주어 origin/main 에서도 나뉩니다. 그래도 함께 되돌린 것은 6b5529d 가 일곱 줄을 한 묶음으로 바꿨기 때문입니다. 동작하는 두 줄은 건드리지 않는 편이 낫다고 보시면 ( ) { [ ^ 다섯 줄로 줄이겠습니다.

Pattern.quote() 로 메타문자를 한꺼번에 덮는 방법이 있지만 * + $ | 가 만드는 출력 문자열까지 바뀌므로 회귀 수정과 섞지 않았습니다. 필요하시면 별건으로 올리겠습니다.

영향 범위

바뀌는 것은 구분자에 ( ) { } ^ [ ] 가 들어올 때 getRegexDelimiter() 가 돌려주는 문자열입니다.

역슬래시를 붙여 \( 를 넣던 입력은 전후가 같습니다. 수정 전에는 \(\\( 가 되고, 수정 후에는 \ 가 그대로 남고 (\( 가 되어 역시 \\( 입니다. 나머지 여섯 문자도 같은 식입니다.

저장소 안에서 이 클래스를 쓰는 자리는 테스트 잡 설정 하나와 기존 테스트 클래스 셋입니다. 거기서 주는 구분자는 콤마(기본 생성자 포함)와 $, | 이고, 셋 다 바뀐 일곱 줄에 걸리지 않습니다.

JUnit 테스트 JUnit tests

  • JUnit 테스트 JUnit tests
  • 수동 테스트 Manual testing

EgovEscapableDelimitedLineTokenizerMetaCharTest 를 더했습니다. 일곱 문자를 차례로 구분자로 주어 a<구분자>b<구분자>c 가 세 컬럼으로 나뉘는지, 그리고 같은 입력에 대한 EgovDelimitedLineTokenizer 결과와 일치하는지 확인합니다.

이 저장소는 워크플로 트리거가 contribution 브랜치라 base=main PR 에는 CI 가 붙지 않습니다. 실행 출력을 옮깁니다. 디버그 로그 줄은 앞머리(타임스탬프·레벨·로거 이름·### 클래스명)를 잘라냈습니다.

수정 전입니다. 일곱 줄만 origin/main 상태로 되돌렸고, 되돌린 파일이 origin/main 과 바이트 동일함을 git diff origin/main -- Batch/org.egovframe.rte.bat.core/src/main/java/org/egovframe/rte/bat/core/item/file/transform/EgovEscapableDelimitedLineTokenizer.java 가 빈 출력인 것으로 확인했습니다.

$ mvn -o test -pl Batch/org.egovframe.rte.bat.core -Dtest=EgovEscapableDelimitedLineTokenizerMetaCharTest
getRegexDelimiter() : (
...
[ERROR] Errors: 
[ERROR]   EgovEscapableDelimitedLineTokenizerMetaCharTest.doTokenize_metaCharDelimiter_matchesDelimitedLineTokenizer:32 » PatternSyntax Unclosed group near index 1
(
[ERROR]   EgovEscapableDelimitedLineTokenizerMetaCharTest.doTokenize_metaCharDelimiter_splitsColumns:21 » PatternSyntax Unclosed group near index 1
(
[ERROR] Tests run: 2, Failures: 0, Errors: 2, Skipped: 0
[INFO] BUILD FAILURE

로그의 getRegexDelimiter() : ( 가 이스케이프되지 않은 채 정규식으로 나간 값입니다. 첫 문자에서 던지므로 뒤의 여섯 문자는 이 실행에서 드러나지 않습니다.

^ 는 예외를 던지지 않아 따로 확인했습니다. 일곱 줄 중 ^ 한 줄만 origin/main 상태로 두면 이렇게 나옵니다.

[ERROR] Failures: 
[ERROR]   EgovEscapableDelimitedLineTokenizerMetaCharTest.doTokenize_metaCharDelimiter_matchesDelimitedLineTokenizer:33 [^] 구분자 분리 결과가 EgovDelimitedLineTokenizer 와 같아야 한다 ==> expected: <[a, b, c]> but was: <[a^b^c]>
[ERROR]   EgovEscapableDelimitedLineTokenizerMetaCharTest.doTokenize_metaCharDelimiter_splitsColumns:22 [^] 구분자로 컬럼이 분리되어야 한다 ==> expected: <[a, b, c]> but was: <[a^b^c]>
[ERROR] Tests run: 2, Failures: 2, Errors: 0, Skipped: 0

수정 후입니다. 일곱 문자가 모두 이스케이프되어 나갑니다.

$ mvn -o test -pl Batch/org.egovframe.rte.bat.core -Dtest='EgovEscapableDelimitedLineTokenizerMetaCharTest#doTokenize_metaCharDelimiter_splitsColumns'
getRegexDelimiter() : \(
getRegexDelimiter() : \)
getRegexDelimiter() : \{
getRegexDelimiter() : \}
getRegexDelimiter() : \^
getRegexDelimiter() : \[
getRegexDelimiter() : \]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

모듈 전체입니다.

$ mvn -o test -pl Batch/org.egovframe.rte.bat.core
[INFO] Tests run: 88, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

테스트 브라우저 Test Browser

  • Chrome
  • Firefox
  • Edge
  • Safari
  • Opera
  • Internet Explorer
  • 기타 Others

테스트 스크린샷 또는 캡처 영상 Test screenshots or captured video

화면이 없는 실행환경 모듈이라 첨부하지 않았습니다.

…문제 수정

getRegexDelimiter() 는 delimiter 를 String.split() 의 정규식으로 넘기기 전에
메타문자를 이스케이프하는 자리인데, 괄호류 일곱 문자만 역슬래시가 붙은 형태를
찾는다. replace() 의 첫 인자는 리터럴이라 "\\(" 는 두 글자 \( 를 뜻하고,
구분자로 흔히 쓰는 홑문자 ( 는 아무 처리 없이 통과한다.

그 결과 doTokenize() 의 line.split(regexDelimiter, -1) 에서
( ) { [ 는 PatternSyntaxException 으로 배치가 멈추고,
^ 는 행 처음에만 맞는 앵커로 해석돼 예외 없이 컬럼이 하나로 붙는다.
같은 패키지의 EgovDelimitedLineTokenizer 는 indexOf() 로 나누므로
같은 구분자를 정상 처리하고, 곧이어 오는 * + $ | 네 줄도 홑문자를 받는다.

replace() 의 대상을 홑문자로 바꿔 일곱 문자 모두 이스케이프되게 했다.
역슬래시가 붙은 입력만 찾던 기존 조건은 실제로 맞는 입력이 없었고,
맞더라도 \\( 라는 깨진 정규식이 되므로 잃는 동작은 없다.

@jei007 jei007 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

표준프레임워크에 대한 지속적인 참여에
대단히 감사드립니다.

@jei007
jei007 merged commit 956c5b6 into eGovFramework:main Sep 10, 2026
@wantaekchoi
wantaekchoi deleted the fix/regex-delimiter-metachar branch September 10, 2026 02:24
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.

2 participants