From b31915b00c78153303f1bd9a266313a985ceceb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B2=94=EC=88=98?= Date: Mon, 22 Jun 2026 03:39:23 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=9D=B4=EC=8A=88=20#4=20-=20Java/Kotli?= =?UTF-8?q?n=20=EC=8B=A4=ED=96=89=20=EA=B2=B0=EA=B3=BC=20UTF-8=20=EC=9D=B8?= =?UTF-8?q?=EC=BD=94=EB=94=A9=20=EA=B0=95=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows 기본 코드페이지(MS949 등)에서 한글을 반환/출력하는 코드의 실행 결과가 깨지는 문제를 해결한다. - javaCommand 헬퍼로 java 실행에 -Dfile/stdout/stderr.encoding=UTF-8 전달 - executeProcess의 프로세스 입출력 스트림 읽기/쓰기를 UTF-8로 명시 - javaCommand가 인코딩 플래그를 항상 포함하는지 회귀 테스트 추가 실행 출력 깨짐은 native.encoding이 MS949인 실제 Windows에서만 재현되며, macOS/Linux에서는 -Dstdout.encoding 오버라이드가 먹지 않아 커맨드 구성 검증으로 회귀를 방지한다. --- .../com/codingtestkit/service/CodeRunner.kt | 28 ++++++++++++++----- .../codingtestkit/service/CodeRunnerTest.kt | 22 +++++++++++++++ 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/com/codingtestkit/service/CodeRunner.kt b/src/main/kotlin/com/codingtestkit/service/CodeRunner.kt index 97ed0f4..ad00448 100644 --- a/src/main/kotlin/com/codingtestkit/service/CodeRunner.kt +++ b/src/main/kotlin/com/codingtestkit/service/CodeRunner.kt @@ -619,7 +619,7 @@ else console.log(_result); if (compile.exitCode != 0) { return RunResult(output = "", error = I18n.t("컴파일 에러", "Compile error") + ":\n${compile.error}", exitCode = compile.exitCode) } - return executeProcess(listOf(javaPath, "-cp", dir.absolutePath, "Main"), dir, input, timeout) + return executeProcess(javaCommand("-cp", dir.absolutePath, "Main"), dir, input, timeout) } val className = detectJavaClassName(code) @@ -641,7 +641,7 @@ else console.log(_result); if (compile.exitCode != 0) { return RunResult(output = "", error = I18n.t("컴파일 에러", "Compile error") + ":\n${compile.error}", exitCode = compile.exitCode) } - return executeProcess(listOf(javaPath, "-cp", dir.absolutePath, "Main"), dir, input, timeout) + return executeProcess(javaCommand("-cp", dir.absolutePath, "Main"), dir, input, timeout) } val sourceFile = File(dir, "$className.java") @@ -652,13 +652,27 @@ else console.log(_result); return RunResult(output = "", error = I18n.t("컴파일 에러", "Compile error") + ":\n${compile.error}", exitCode = compile.exitCode) } - return executeProcess(listOf(javaPath, "-cp", dir.absolutePath, className), dir, input, timeout) + return executeProcess(javaCommand("-cp", dir.absolutePath, className), dir, input, timeout) } private fun javacCommand(vararg sourceFiles: File): List { return listOf(javacPath, "-encoding", "UTF-8") + sourceFiles.map { it.absolutePath } } + /** + * java 실행 커맨드. 자식 JVM의 표준 입출력 인코딩을 UTF-8로 강제한다. + * Windows 기본 코드페이지(MS949 등)로 인해 한글 결과가 깨지는 것을 방지 (이슈 #4). + * stdout/stderr.encoding은 JDK 18+에서 동작하고, 하위 버전에서는 무시되어 무해하다. + */ + private fun javaCommand(vararg args: String): List { + return listOf( + javaPath, + "-Dfile.encoding=UTF-8", + "-Dstdout.encoding=UTF-8", + "-Dstderr.encoding=UTF-8" + ) + args + } + private fun extractJavaClass(code: String, className: String): String { val pattern = Regex("(class\\s+$className\\s*\\{)", RegexOption.DOT_MATCHES_ALL) val match = pattern.find(code) ?: return code @@ -741,7 +755,7 @@ else console.log(_result); return RunResult(output = "", error = I18n.t("컴파일 에러", "Compile error") + ":\n${compile.error}", exitCode = compile.exitCode) } - return executeProcess(listOf(javaPath, "-jar", jarFile.absolutePath), dir, input, timeout) + return executeProcess(javaCommand("-jar", jarFile.absolutePath), dir, input, timeout) } private fun runJavaScript(code: String, input: String, dir: File, timeout: Long): RunResult { @@ -768,7 +782,7 @@ else console.log(_result); .start() if (input.isNotBlank()) { - process.outputStream.bufferedWriter().use { it.write(input) } + process.outputStream.bufferedWriter(Charsets.UTF_8).use { it.write(input) } } // 메모리 폴링 스레드 시작 @@ -815,8 +829,8 @@ else console.log(_result); return RunResult(output = "", error = I18n.t("시간 초과 (${timeout}초)", "Time Limit Exceeded (${timeout}s)"), exitCode = -1, timedOut = true, executionTimeMs = elapsedMs, peakMemoryKB = peakMemory.get()) } - val output = process.inputStream.bufferedReader().readText().trimEnd() - val error = process.errorStream.bufferedReader().readText().trimEnd() + val output = process.inputStream.bufferedReader(Charsets.UTF_8).readText().trimEnd() + val error = process.errorStream.bufferedReader(Charsets.UTF_8).readText().trimEnd() return RunResult(output = output, error = error, exitCode = process.exitValue(), executionTimeMs = elapsedMs, peakMemoryKB = peakMemory.get()) } diff --git a/src/test/kotlin/com/codingtestkit/service/CodeRunnerTest.kt b/src/test/kotlin/com/codingtestkit/service/CodeRunnerTest.kt index 13d0868..28b9742 100644 --- a/src/test/kotlin/com/codingtestkit/service/CodeRunnerTest.kt +++ b/src/test/kotlin/com/codingtestkit/service/CodeRunnerTest.kt @@ -404,4 +404,26 @@ class CodeRunnerTest { dir.deleteRecursively() } } + + /** + * Regression test for issue #4: the java run command must force UTF-8 standard I/O encoding. + * + * The runtime garbling only reproduces on a real Windows host (native.encoding = MS949), where + * `-Dstdout.encoding` cannot be overridden from the command line on macOS/Linux. So instead of + * an unreproducible end-to-end test, we assert the command `javaCommand` builds always carries + * the UTF-8 encoding flags — this fails OS-independently if the flags regress. + */ + @Test + fun `javaCommand forces UTF-8 standard IO encoding`() { + val method = CodeRunner::class.java.getDeclaredMethod("javaCommand", Array::class.java) + method.isAccessible = true + @Suppress("UNCHECKED_CAST") + val cmd = method.invoke(CodeRunner, arrayOf("-cp", "X", "Main")) as List + + assertTrue(cmd.contains("-Dfile.encoding=UTF-8"), "missing file.encoding flag: $cmd") + assertTrue(cmd.contains("-Dstdout.encoding=UTF-8"), "missing stdout.encoding flag: $cmd") + assertTrue(cmd.contains("-Dstderr.encoding=UTF-8"), "missing stderr.encoding flag: $cmd") + // The caller's arguments must be preserved at the end of the command. + assertEquals(listOf("-cp", "X", "Main"), cmd.takeLast(3)) + } }