fix: EgovFileUtil.readFile의 문자셋 디코딩 오류와 개행 소실 수정 - #300
Open
z3rotig4r wants to merge 2 commits into
Open
Conversation
readFileContent(InputStream)이 바이트 하나를 char 하나로 캐스팅한다. Latin-1로
읽는 것과 같아 멀티바이트 인코딩이 전부 깨진다. 이 경로를 쓰는 public
readFile(File)도 같은 영향을 받는다.
"행정안전부 표준프레임워크"(13자)
-> "íì ìì ë¶ íì¤íë ììí¬"(37자)
스트림을 전량 읽어 Charset.defaultCharset()으로 디코딩하도록 바꿨다. UTF-8을
하드코딩하지 않은 이유는 짝이 되는 쓰기 경로가 모두 플랫폼 기본 문자셋이기
때문이다. writeFile(File, String)은 FileWriter를, readTextFile(String, boolean)은
FileReader를 쓴다. 기본 문자셋을 써야 쓰기와 읽기의 왕복이 성립하고, 기존에
기본 문자셋으로 기록해 둔 파일도 계속 읽을 수 있다.
동작 변경: 비-ASCII 파일에서 readFile(File)의 반환값이 달라진다. 종전에는 깨진
문자열이 나왔으므로 그 값에 의존하는 코드는 없다고 본다. 실행환경 안에는
readFile(File)과 readFileContent(InputStream)의 호출처가 없다.
부수 효과로 성능도 개선된다. 3.36MB 한글 텍스트를 읽을 때 5회 평균 156~189ms에서
31~33ms가 됐다. 바이트 단위 read 호출과 StringBuilder 확장이 사라진 결과이며,
반환 문자열 길이도 바이트 수(3,364,768)가 아닌 실제 문자 수(1,300,024)가 된다.
readFile(File)의 스트림 해제도 try-with-resources로 정리했다.
readFile(File, String)은 readTextLines()로 줄 목록을 얻어 구분자 없이 이어붙인다. readTextLines()가 돌려주는 각 원소에는 개행이 들어 있지 않으므로 원본의 개행이 전부 사라진다. "첫째 줄\n둘째 줄\n셋째 줄\n" -> "첫째 줄둘째 줄셋째 줄" "a\r\nb\r\n" -> "ab" 같은 클래스의 다른 읽기 경로와 견주면 의도된 동작으로 보기 어렵다. readTextFile(String, boolean)은 newline 인자로 개행을 붙일지 호출자가 고르게 하고, 붙일 때는 System.lineSeparator()를 쓴다. 파일 내용을 문자열 하나로 돌려주는 API가 개행만 말없이 버릴 이유가 없다. 파일을 전량 읽어 지정한 인코딩으로 한 번에 디코딩하도록 바꿨다. LF와 CRLF의 구분, 마지막 줄의 개행까지 원본 그대로 보존된다. new String(byte[], Charset)은 잘못된 바이트를 U+FFFD로 대체하므로 종전 readUtf8LinesLenient의 관대한 처리도 그대로 유지된다. 없는 파일에 대한 예외는 종전 UTF-8 경로와 같은 FileNotFoundException으로 맞췄다(종전 비-UTF-8 경로는 NoSuchFileException). readTextLines()는 grep(File, String)이 계속 쓰므로 남겨두었다. testReadMultiLineFile의 기대값을 갱신했다. 이 테스트는 #257에서 빈 파일의 NoSuchElementException을 고치면서 당시 동작을 고정해 둔 characterization 테스트이고, 이번에 그 고정 대상이던 동작 자체가 결함으로 드러났다. 이름과 주석도 개행 보존 취지로 바꿨다.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
기존 테스트의 기대값을 바꿉니다
이 점을 먼저 확인해 주시기 바랍니다.
testReadMultiLineFile은 PR #257에서 당시 동작을 그대로 고정해 둔 characterization 테스트입니다. 이번에 그 고정 대상이던 동작 자체가 결함으로 드러나 기대값을 바꾸고 이름도testReadMultiLineFilePreservesLineSeparator로 정리했습니다. 종전 기대값이 규정된 계약이 아니라 구현 부산물이라고 판단한 근거는 다음 세 가지입니다.readFile(File, String)의 javadoc은 "String 형으로 파일의 내용을 읽는다" 한 줄뿐입니다.readTextFile(String, boolean)은 개행을 붙일지 여부를 호출자가newline인자로 고르게 하고 붙일 때는System.lineSeparator()를 씁니다. 이 클래스는 개행을 말없이 버리는 설계가 아닙니다.c3d1fc7("Apache common readLines(File) DEPRECATED 대응")에서 들어온 구현 세부입니다.이 판단에 동의하지 않으시면 개행 부분만 따로 되돌릴 수 있도록 커밋을 나눠 두었습니다(아래 "변경 내용" 참고).
변경 이유
EgovFileUtil의 파일 읽기 경로에 결함이 두 가지 있습니다.멀티바이트 문자가 깨집니다.
readFileContent(InputStream)이buf.append((char) i)로 바이트 하나를 char 하나로 캐스팅합니다. UTF-8로 저장한행정안전부 표준프레임워크(13자)를 읽으면 37자짜리 깨진 문자열이 됩니다. public API인readFile(File)이 이 경로를 쓰므로 한글이 든 파일은 이 메서드로 읽어서는 원문을 복원할 수 없습니다.개행이 전부 사라집니다.
readFile(File, String encoding)이readTextLines()가 돌려준 줄들을 구분자 없이 이어 붙입니다.line1\nline2\nline3을 읽으면line1line2line3이 됩니다. 읽은 내용을 다시 파일로 쓰거나 줄 단위로 파싱하면 원본과 달라집니다.변경 내용
주제별로 커밋을 둘로 나눴습니다.
2145063인코딩 —readFileContent가new String(in.readAllBytes(), Charset.defaultCharset())으로 바이트가 아니라 문자 경계로 디코딩합니다.readFile(File)의 스트림 닫기도 try-with-resources로 정리했습니다.25cb547개행 —readFile(File, String)이 줄 단위로 읽어 붙이는 대신 파일 전체를 읽어 지정 인코딩으로 디코딩합니다. 원본 개행이 LF든 CRLF든 그대로 남습니다.encoding이 null이면 종전처럼 플랫폼 기본 문자셋을 씁니다.두 커밋은 서로 독립적입니다. HEAD에서
git revert 25cb547이 충돌 없이 적용되고 그 상태에서도 테스트 27건이 모두 통과하므로 개행 변경은 보류하고 인코딩 수정만 받으셔도 됩니다.인코딩 미지정 오버로드는 JVM 기본 문자셋에 의존합니다
이 선택도 확인 부탁드립니다.
readFile(File)은 이제Charset.defaultCharset()으로 디코딩하므로 결과가 배포 JVM의 기본 문자셋에 좌우됩니다. UTF-8을 하드코딩하지 않은 이유는 짝이 되는writeFile(File, String)(FileWriter)과readTextFile(FileReader)이 모두 플랫폼 기본 문자셋을 쓰기 때문입니다. UTF-8로 고정하면 기본 문자셋이 EUC-KR인 JVM에서 이 클래스 자신의 쓰기·읽기 왕복이 오히려 깨집니다.writeFile(File, String)으로 쓴 한글을readFile(File)로 다시 읽는 왕복을 JDK 21에서-Dfile.encoding을 바꿔가며 측정한 결과입니다(소스는javac -encoding UTF-8로 선컴파일).ISO-8859-1과 windows-31j(일본어 MS932 계열)는 한글 자체를 표현하지 못해 쓰는 시점에 이미 손실됩니다. 읽기 쪽 수정으로 되돌릴 수 있는 범위 밖입니다. 어떤 환경에서도 이 PR이 수정 전보다 나쁘지 않습니다. 인코딩을 고정해야 하는 호출자는
readFile(File, String)을 쓰라는 안내를 javadoc에 적어 두었습니다.예외 타입이 바뀌는 경우가 있습니다
존재하지 않는 파일에 UTF-8이 아닌 인코딩을 지정해
readFile(File, String)을 호출하면 예외가NoSuchFileException에서FileNotFoundException으로 바뀝니다. 둘 다IOException입니다. 종전에도 UTF-8 경로는FileNotFoundException이었으므로 두 경로가 같아지는 방향입니다. 레포 안에서 이 차이에 영향받는 호출처는 없습니다.테스트 방법
mvn -B -o -pl Foundation/org.egovframe.rte.fdl.filehandling testmain기준 25건 통과 → 이 브랜치에서 31건 통과입니다. 신규 6건을 추가했습니다. 갱신한 1건은 위에 적은testReadMultiLineFile의 기대값 변경 및 이름 변경입니다.영향 범위
EgovFileUtil.java와FilehandlingServiceTest.java둘뿐이며 시그니처 변경은 없습니다.readFileContent와readFile(File)을 호출하는 곳은EgovFileUtil자신 말고는 레포 전역에 없습니다.readFile(File)이 150.6ms에서 14.6ms로,readFile(File, "UTF-8")이 30.2ms에서 14.3ms로 줄었습니다. 반환 문자열 길이도 바이트 수가 아니라 실제 문자 수로 바로잡힙니다.