1. 오류의 증상
이상적인 세계에서는 프로그래머가 항상 파일의 인코딩을 알고, 읽을 때 올바르게 지정합니다. 하지만 현실에서는 파일이 Windows, Linux, 서버, 각종 편집기 사이를 오가며 각자 다른 방식으로 바이트를 해석합니다. 그 결과 다음과 같은 증상을 마주하게 됩니다.
- “깨진 문자” — 기대한 텍스트 대신 낯선 기호, 물음표, 네모 박스, 혹은 어떤 언어로도 보이지 않는 글자 조합이 나타납니다.
- 문자 손실 — 텍스트의 일부가 사라지거나 ?로 대체됩니다.
- 예외 발생 — 예를 들어 MalformedInputException처럼 Java가 선택한 인코딩으로 바이트를 “소화”하지 못할 때 발생합니다.
- 파싱 오류 — 텍스트가 훼손되어 키워드나 구조가 깨지면 프로그램이 파일을 제대로 처리하지 못합니다.
다음은 키릴 문자(Cyrillic)가 들어 있는 파일을 잘못된 인코딩으로 읽을 때의 전형적인 “깨진 문자” 예시입니다.
Ozhidali: Privet, mir!
Poluchili: Привет, мир
이것은 새로운 언어가 아니라, 필요한 “사전”(인코딩)과 다르게 바이트가 해석된 결과입니다.
2. 왜 오류가 발생하는가: 근본 원인
파일은 한 인코딩으로 저장되고, 다른 인코딩으로 읽힐 때
누군가가 파일을 Windows-1251로 저장했는데, 당신이 이를 UTF-8로 연다고 가정해 봅시다. Java는 UTF-8 규칙에 따라 바이트를 해독하려고 애쓰지만, 바이트 값이 기대와 맞지 않기 때문에 결국 의미 없는 결과가 됩니다.
시스템 “기본값” 인코딩 사용
인코딩을 명시하지 않으면, Java는 시스템 인코딩 — 즉 컴퓨터에 설정된 기본 인코딩을 사용합니다. Windows의 러시아 로캘에서는 Windows-1251, Linux에서는 UTF-8, Mac에서도 UTF-8일 수 있습니다. 내 컴퓨터에서 잘 열리던 파일이 다른 OS를 쓰는 동료의 컴퓨터에서는 읽히지 않을 수 있습니다.
구식 생성자 사용
오래된 Java 버전(그리고 일부 교재)에서는 FileReader/FileWriter 같은 구문을 자주 볼 수 있는데, 이들은 시스템 인코딩을 사용해 제어할 수 없게 만듭니다 — 이는 함정이며 “깨진 문자”의 원인이 됩니다.
FileReader reader = new FileReader("file.txt");
FileWriter writer = new FileWriter("file.txt");
BOM(Byte Order Mark)의 유무
일부 인코딩(예: UTF-8의 BOM 또는 UTF-16)은 파일 시작에 특수 바이트를 추가해 자신의 특성을 알립니다. 프로그램이 BOM을 기대하지 않는데 존재하거나, 반대로 기대하는데 없으면 문제가 생길 수 있습니다. 파일의 처음 몇 글자가 깨지거나, 아예 파일을 인식하지 못하는 상황이 벌어집니다.
3. 어떻게 나타나는가: 실전 분석
예제 1: Windows-1251로 저장된 키릴 문자 파일을 UTF-8로 읽기
import java.nio.file.*;
import java.nio.charset.*;
public class EncodingDemo {
public static void main(String[] args) throws Exception {
Path path = Paths.get("russian.txt");
// 파일은 Windows-1251로 저장되었는데 UTF-8로 읽음 — 글자가 깨집니다!
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
System.out.println(reader.readLine());
}
}
}
결과적으로 “Privet, mir!” 대신 이상한 기호들이 보이게 됩니다.
예제 2: 파일은 UTF-8로 저장되고 ISO-8859-1로 읽을 때
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.ISO_8859_1)) {
System.out.println(reader.readLine());
}
결과: ASCII가 아닌 모든 문자는 쓰레기로 바뀌거나 ?로 대체됩니다.
예제 3: 파일 읽기 중 예외
바이트가 선택한 인코딩의 규칙에 맞지 않으면, Java는 다음과 같이 예외를 던질 수 있습니다.
Exception in thread "main" java.nio.charset.MalformedInputException: Input length = 1
at java.base/sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
...
이는 Java가 선택한 인코딩에서 정상적으로 해석할 수 없는 바이트를 만났다는 뜻입니다.
4. 진단: 인코딩 문제를 파악하는 방법
파일 인코딩을 확인하세요
- Notepad++, VS Code, Sublime Text 같은 편집기에서는 보통 파일 인코딩을 확인하거나 변경할 수 있습니다(대개 하단 상태표시줄).
- Linux에서는 다음 명령으로 인코딩에 대한 단서를 얻을 수 있습니다(항상 100% 정확하진 않음):
file imya_fayla.txt
Java의 시스템 인코딩을 확인하세요
콘솔에 file.encoding 시스템 속성 값을 출력합니다:
System.out.println(System.getProperty("file.encoding"));
테스트 데이터를 사용하세요
키릴 문자, 라틴 문자, 특수기호, 이모지 등 다양한 문자를 넣은 작은 파일을 만들고, 여러 인코딩으로 읽어 보면서 기대한 결과와 언제 일치하는지 확인하세요.
항상 인코딩을 명시하세요
코드에서 인코딩을 지정하지 않은 채 파일을 읽거나 쓴다면 — 의심해야 합니다. 예를 들어 “기본값” 대신 Files.newBufferedReader(..., StandardCharsets.UTF_8)을 사용하세요.
5. Best practices: 함정 피하기
규칙 №1:
항상 파일 작업 시 인코딩을 명시하세요. 특히 파일이 서로 다른 컴퓨터, OS에서 사용되거나 네트워크로 전송될 가능성이 있다면 더욱 그렇습니다.
규칙 №2:
가능하면 현대적이고 “표현력 있는” 인코딩 — 우선적으로 UTF-8(StandardCharsets.UTF_8)을 사용하세요. 특별한 요구사항(예: 레거시 시스템과의 연동)이 있는 경우에만 다른 인코딩을 사용하세요.
규칙 №3:
FileReader와 FileWriter 클래스는 피하세요(인코딩을 지정할 수 없음). 대신 InputStreamReader, OutputStreamWriter 또는 Files의 메서드와 명시적 Charset을 사용하세요.
규칙 №4:
결과를 확인하세요! 다양한 인코딩을 지원하는 편집기에서 기록한 파일을 열어 텍스트가 올바르게 보이는지 검증하세요.
6. 특이 사항과 뉘앙스: BOM, XML, JSON 및 기타 “재밌는” 사례
BOM(Byte Order Mark): UTF-8 파일은 때때로 “보이지 않는” 바이트(EF BB BF)로 시작합니다. 대부분의 현대 프로그램은 이를 무시하지만, 일부는 줄 시작에 깨진 문자를 보여 주거나 파일을 거부할 수 있습니다(예: 구형 XML/JSON 파서).
XML/HTML: 파일 시작에 <?xml version="1.0" encoding="UTF-8"?> 같은 라인이 있을 수 있습니다. 이는 프로그램에 어떤 인코딩을 기대해야 하는지 알려 줍니다. 그러나 실제 인코딩이 선언과 다르면 — 다시 “깨진 문자”가 나타납니다.
JSON: 표준상 UTF-8이어야 합니다. 하지만 파일이 Windows-1251로 만들어졌다면 파서는 오류를 내거나 데이터를 왜곡시킬 것입니다.
GO TO FULL VERSION