문자 인코딩과 텍스트 파일
파일의 바이트를 문자로 해석하려면 문자 인코딩이 필요합니다.
양쪽이 다른 인코딩을 사용하면 파일은 열려도 글자가 깨지거나 변환 예외가 날 수 있습니다.
Java의 편의 메서드는 UTF-8을 기본으로 쓰는 경우가 많지만 외부 포맷 계약에서는 Charset을 명시하는 편이 분명합니다.
작은 파일을 한 번에 읽는 방식과 큰 파일을 스트림으로 처리하는 방식도 메모리 비용이 다릅니다.
이 절에서는 바이트, 문자, 줄이라는 단위를 구분합니다.
Java의 String 인덱스와 length는 UTF-16 코드 단위를 셉니다. 사용자가 보는 글자 수, 유니코드 코드 포인트 수, 파일의 UTF-8 바이트 수는 서로 다를 수 있습니다. 이 단위를 섞으면 길이 제한과 파일 크기 계산이 특히 비 ASCII 문자에서 어긋납니다.
Charset이 바이트와 문자의 변환 규칙이다
StandardCharsets.UTF_8처럼 이름 붙은 상수를 사용해 쓰기와 읽기에 같은 규칙을 전달합니다.
플랫폼 기본 인코딩에 암묵적으로 의존하지 않습니다.
작은 파일은 편의 메서드로 단순하게 처리한다
writeString, readString, readAllLines는 전체 내용을 메모리에 올립니다.
설정이나 작은 문서에는 읽기 쉽습니다.
TextEncoding.java는 지금 설명한 규칙을 실제 값의 변화로 보여 줍니다.
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class TextEncoding {
public static void main(String[] args) throws Exception {
Path file = Files.createTempFile("utf8-", ".txt");
List<String> lines = List.of("Java", "한글", "😀");
Files.write(file, lines, StandardCharsets.UTF_8);
try (var stream = Files.lines(file, StandardCharsets.UTF_8)) {
System.out.println(stream.map(String::length).toList());
}
byte[] bytes = Files.readAllBytes(file);
System.out.println(bytes.length > lines.stream().mapToInt(String::length).sum());
Files.delete(file);
}
}Java와 한글은 각각 UTF-16 단위 4와 2이고, 보조 평면 문자인 이모지는 surrogate pair라 길이가 2입니다. 두 번째 true는 저장 바이트 수가 String 길이 합과 같지 않음을 보여 줍니다. 줄바꿈 바이트도 파일 크기에 포함됩니다.
[4, 2, 2]
trueString.length는 UTF-16 단위라 이모지가 2이고 UTF-8 바이트 수는 문자 단위 합보다 큽니다.
StandardCharsets.UTF_8처럼 이름 붙은 상수를 사용해 쓰기와 읽기에 같은 규칙을 전달합니다.
writeString, readString, readAllLines는 전체 내용을 메모리에 올립니다.
newBufferedReader와 Files.lines는 전체 파일을 한 번에 보관하지 않습니다.
큰 파일은 Reader나 lines로 순차 처리한다
newBufferedReader와 Files.lines는 전체 파일을 한 번에 보관하지 않습니다.
열린 스트림을 try-with-resources로 닫습니다.
아래 코드는 텍스트 계약, 임의 바이트, 순차 읽기를 각각 다른 API로 다뤄야 하는 이유를 비교합니다.
Path file = Path.of("message.txt");
Files.writeString(file, "한글", StandardCharsets.UTF_8);
String text = Files.readString(file, StandardCharsets.UTF_8);// 외부 계약이 UTF-16이면 양쪽 모두 같은 Charset을 명시한다.
byte[] binary = {0, (byte) 0xFF, 10};
// String unsafe = new String(binary); // 기본 Charset 의존 + 임의 바이트 손실 가능try (var reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
System.out.println(reader.readLine());
}바이너리 파일을 문자 API로 읽지 않는다
이미지나 압축 파일은 InputStream과 byte 배열로 처리합니다.
임의 바이트를 String으로 바꾸면 정보가 손실될 수 있습니다.
줄바꿈은 플랫폼과 파일 형식을 구분한다
System.lineSeparator()는 현재 플랫폼 출력에 적합하지만 고정 프로토콜이나 저장 형식은 요구한 줄바꿈을 명시해야 합니다.
목록에 "é"와 "e\u0301"를 추가해 각 length와 UTF-8 바이트를 비교해 보세요. 화면에서는 비슷해 보여도 코드 포인트 조합이 달라질 수 있습니다. 문자열 동등성이나 검색에 사용자 관점의 동일성이 필요하다면 Charset 변환과 별도로 Unicode 정규화 정책을 정해야 합니다.
Files.lines는 지연 읽기라 최종 연산이 끝날 때까지 파일이 열려 있습니다. Stream을 메서드 밖으로 반환한다면 호출자에게 close 책임도 함께 넘기는 셈입니다. 단순 집계 결과만 필요하다면 메서드 안에서 try-with-resources를 끝내고 값만 반환하는 편이 안전합니다.
텍스트 I/O에서는 파일 크기와 Charset을 호출 양쪽에서 명시하면 환경에 따른 결과 차이를 줄일 수 있습니다.