From 21450635a3b889852b52d177a55947a07737dcf7 Mon Sep 17 00:00:00 2001 From: z3rotig4r Date: Fri, 31 Jul 2026 10:54:55 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20EgovFileUtil.readFile=EC=9D=B4=20?= =?UTF-8?q?=EB=A9=80=ED=8B=B0=EB=B0=94=EC=9D=B4=ED=8A=B8=20=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=EB=A5=BC=20=EA=B9=A8=EB=9C=A8=EB=A6=AC=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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로 정리했다. --- .../rte/fdl/filehandling/EgovFileUtil.java | 17 +++----- .../filehandling/FilehandlingServiceTest.java | 40 +++++++++++++++++++ 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/Foundation/org.egovframe.rte.fdl.filehandling/src/main/java/org/egovframe/rte/fdl/filehandling/EgovFileUtil.java b/Foundation/org.egovframe.rte.fdl.filehandling/src/main/java/org/egovframe/rte/fdl/filehandling/EgovFileUtil.java index 41aef2aa..f321f22a 100755 --- a/Foundation/org.egovframe.rte.fdl.filehandling/src/main/java/org/egovframe/rte/fdl/filehandling/EgovFileUtil.java +++ b/Foundation/org.egovframe.rte.fdl.filehandling/src/main/java/org/egovframe/rte/fdl/filehandling/EgovFileUtil.java @@ -25,6 +25,7 @@ import java.io.*; import java.io.FileNotFoundException; +import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; @@ -146,32 +147,26 @@ public static void cd(final String changDirectory) throws FileSystemException { } /** - * 파일을 읽는다. + * 파일을 플랫폼 기본 문자셋({@link Charset#defaultCharset()})으로 읽는다. + * 인코딩을 지정하려면 {@link #readFile(File, String)}을 사용한다. */ public static String readFile(File file) throws IOException { - BufferedInputStream in = new BufferedInputStream(new FileInputStream(file)); String sResult = ""; - try { + try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(file))) { sResult = readFileContent(in); } catch (IllegalArgumentException e) { LOGGER.debug("[{}] EogvFileUtil : {}", e.getClass().getName(), e.getMessage()); - } finally { - in.close(); } return sResult; } /** - * String 형으로 파일의 내용을 읽는다. + * String 형으로 스트림 전체를 읽어 플랫폼 기본 문자셋으로 디코딩한다. */ public static String readFileContent(InputStream in) throws IOException { - StringBuilder buf = new StringBuilder(); - for (int i = in.read(); i != -1; i = in.read()) { - buf.append((char) i); - } - return buf.toString(); + return new String(in.readAllBytes(), Charset.defaultCharset()); } /** diff --git a/Foundation/org.egovframe.rte.fdl.filehandling/src/test/java/org/egovframe/rte/fdl/filehandling/FilehandlingServiceTest.java b/Foundation/org.egovframe.rte.fdl.filehandling/src/test/java/org/egovframe/rte/fdl/filehandling/FilehandlingServiceTest.java index 6ce03a01..b7dad7bb 100755 --- a/Foundation/org.egovframe.rte.fdl.filehandling/src/test/java/org/egovframe/rte/fdl/filehandling/FilehandlingServiceTest.java +++ b/Foundation/org.egovframe.rte.fdl.filehandling/src/test/java/org/egovframe/rte/fdl/filehandling/FilehandlingServiceTest.java @@ -21,7 +21,9 @@ import java.io.*; import java.net.URL; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.Iterator; import java.util.List; @@ -244,6 +246,44 @@ public void testReadMultiLineFile() throws IOException { assertEquals("line1line2line3", EgovFileUtil.readFile(new File(multiPath), "UTF-8")); } + /** + * 파일 읽기 테스트. 플랫폼 기본 문자셋으로 쓴 한글은 그대로 읽혀야 한다. + */ + @Test + public void testReadFileKoreanWithDefaultCharset() throws IOException { + File file = new File(EgovFileUtil.getTmpDirectory() + "/read-default-korean.txt"); + String content = "행정안전부 표준프레임워크"; + + try { + Files.write(file.toPath(), content.getBytes(Charset.defaultCharset())); + + assertEquals(content, EgovFileUtil.readFile(file)); + } finally { + if (file.exists()) { + EgovFileUtil.delete(file); + } + } + } + + /** + * 빈 파일 읽기 테스트. 내용이 없는 파일은 두 오버로드 모두 빈 문자열을 반환해야 한다. + */ + @Test + public void testReadEmptyFileWithDefaultCharsetAndEncoding() throws IOException { + File file = new File(EgovFileUtil.getTmpDirectory() + "/read-empty.txt"); + + try { + Files.write(file.toPath(), new byte[0]); + + assertEquals("", EgovFileUtil.readFile(file)); + assertEquals("", EgovFileUtil.readFile(file, "UTF-8")); + } finally { + if (file.exists()) { + EgovFileUtil.delete(file); + } + } + } + /** * 파일 복사 테스트. */ From 25cb547b657b53e2edc1e1d55e0e027a05d2c431 Mon Sep 17 00:00:00 2001 From: z3rotig4r Date: Fri, 31 Jul 2026 10:56:18 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20EgovFileUtil.readFile(File,=20String?= =?UTF-8?q?)=EC=9D=B4=20=EA=B0=9C=ED=96=89=EC=9D=84=20=EC=9E=83=EB=8A=94?= =?UTF-8?q?=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 테스트이고, 이번에 그 고정 대상이던 동작 자체가 결함으로 드러났다. 이름과 주석도 개행 보존 취지로 바꿨다. --- .../rte/fdl/filehandling/EgovFileUtil.java | 13 ++- .../filehandling/FilehandlingServiceTest.java | 81 ++++++++++++++++++- 2 files changed, 83 insertions(+), 11 deletions(-) diff --git a/Foundation/org.egovframe.rte.fdl.filehandling/src/main/java/org/egovframe/rte/fdl/filehandling/EgovFileUtil.java b/Foundation/org.egovframe.rte.fdl.filehandling/src/main/java/org/egovframe/rte/fdl/filehandling/EgovFileUtil.java index f321f22a..6eab34ca 100755 --- a/Foundation/org.egovframe.rte.fdl.filehandling/src/main/java/org/egovframe/rte/fdl/filehandling/EgovFileUtil.java +++ b/Foundation/org.egovframe.rte.fdl.filehandling/src/main/java/org/egovframe/rte/fdl/filehandling/EgovFileUtil.java @@ -170,17 +170,14 @@ public static String readFileContent(InputStream in) throws IOException { } /** - * String 영으로 파일의 내용을 읽는다. + * String 형으로 파일 전체를 읽어 지정한 인코딩으로 디코딩한다. 원본의 개행은 그대로 보존된다. + * encoding이 null이면 플랫폼 기본 문자셋을 사용한다. */ public static String readFile(File file, String encoding) throws IOException { - StringBuilder sb = new StringBuilder(); - List lines = readTextLines(file, encoding); - - for (Iterator it = lines.iterator(); it.hasNext(); ) { - sb.append(it.next()); + Charset charset = encoding == null ? Charset.defaultCharset() : Charset.forName(encoding); + try (InputStream in = new FileInputStream(file)) { + return new String(in.readAllBytes(), charset); } - - return sb.toString(); } /** diff --git a/Foundation/org.egovframe.rte.fdl.filehandling/src/test/java/org/egovframe/rte/fdl/filehandling/FilehandlingServiceTest.java b/Foundation/org.egovframe.rte.fdl.filehandling/src/test/java/org/egovframe/rte/fdl/filehandling/FilehandlingServiceTest.java index b7dad7bb..4048c1f4 100755 --- a/Foundation/org.egovframe.rte.fdl.filehandling/src/test/java/org/egovframe/rte/fdl/filehandling/FilehandlingServiceTest.java +++ b/Foundation/org.egovframe.rte.fdl.filehandling/src/test/java/org/egovframe/rte/fdl/filehandling/FilehandlingServiceTest.java @@ -237,13 +237,13 @@ public void testReadEmptyFile() throws IOException { } /** - * 여러 줄 파일 읽기 테스트. 줄들이 종전과 동일하게 연결되어야 한다. + * 여러 줄 파일 읽기 테스트. 원본의 개행이 보존되어야 한다. */ @Test - public void testReadMultiLineFile() throws IOException { + public void testReadMultiLineFilePreservesLineSeparator() throws IOException { String multiPath = tmppath + "/multiline.txt"; EgovFileUtil.writeFile(multiPath, "line1\nline2\nline3", "UTF-8"); - assertEquals("line1line2line3", EgovFileUtil.readFile(new File(multiPath), "UTF-8")); + assertEquals("line1\nline2\nline3", EgovFileUtil.readFile(new File(multiPath), "UTF-8")); } /** @@ -284,6 +284,81 @@ public void testReadEmptyFileWithDefaultCharsetAndEncoding() throws IOException } } + /** + * 파일 읽기 테스트. 지정 인코딩으로 읽을 때 LF 개행은 보존되어야 한다. + */ + @Test + public void testReadFileWithEncodingPreservesLf() throws IOException { + File file = new File(EgovFileUtil.getTmpDirectory() + "/read-utf8-lf.txt"); + String content = "첫째 줄\n둘째 줄\n셋째 줄\n"; + + try { + Files.write(file.toPath(), content.getBytes(StandardCharsets.UTF_8)); + + assertEquals(content, EgovFileUtil.readFile(file, "UTF-8")); + } finally { + if (file.exists()) { + EgovFileUtil.delete(file); + } + } + } + + /** + * 파일 읽기 테스트. 지정 인코딩으로 읽을 때 CRLF 개행은 보존되어야 한다. + */ + @Test + public void testReadFileWithEncodingPreservesCrLf() throws IOException { + File file = new File(EgovFileUtil.getTmpDirectory() + "/read-utf8-crlf.txt"); + String content = "a\r\nb\r\n"; + + try { + Files.write(file.toPath(), content.getBytes(StandardCharsets.UTF_8)); + + assertEquals(content, EgovFileUtil.readFile(file, "UTF-8")); + } finally { + if (file.exists()) { + EgovFileUtil.delete(file); + } + } + } + + /** + * 파일 읽기 테스트. EUC-KR로 쓴 파일은 지정 인코딩으로 그대로 읽혀야 한다. + */ + @Test + public void testReadFileWithEucKrEncoding() throws IOException { + File file = new File(EgovFileUtil.getTmpDirectory() + "/read-euc-kr.txt"); + String content = "한글\nEUC-KR\n"; + + try { + Files.write(file.toPath(), content.getBytes(Charset.forName("EUC-KR"))); + + assertEquals(content, EgovFileUtil.readFile(file, "EUC-KR")); + } finally { + if (file.exists()) { + EgovFileUtil.delete(file); + } + } + } + + /** + * 파일 읽기 테스트. 잘못된 UTF-8 바이트 시퀀스는 종전과 같이 대체 문자로 처리되어야 한다. + */ + @Test + public void testReadFileWithInvalidUtf8BytesReplacesMalformedInput() throws IOException { + File file = new File(EgovFileUtil.getTmpDirectory() + "/read-invalid-utf8.txt"); + + try { + Files.write(file.toPath(), new byte[]{(byte) 0xC3, (byte) 0x28}); + + assertEquals("�(", EgovFileUtil.readFile(file, "UTF-8")); + } finally { + if (file.exists()) { + EgovFileUtil.delete(file); + } + } + } + /** * 파일 복사 테스트. */