문자, 문자열과 유니코드
화면에 보이는 문자 하나와 Java의 char 하나는 항상 같은 단위가 아닙니다.
char는 UTF-16 코드 단위이므로 보조 평면 문자는 두 개의 char로 표현됩니다.
문자열 길이와 사용자가 인식하는 글자 수가 다를 수 있다는 뜻입니다.
문자열 리터럴과 텍스트 블록은 같은 String을 만들지만 소스에서 여러 줄을 표현하는 방식이 다릅니다.
이 절에서는 텍스트를 코드 단위, 코드 포인트, 문자열이라는 층으로 나누어 봅니다.
char는 UTF-16 코드 단위다
기본 다국어 평면의 많은 문자는 하나의 char에 들어가지만 이모지 일부는 서로게이트 쌍이 필요합니다.
전체 문자를 순회할 때는 codePoints()를 고려해야 합니다.
String은 변경할 수 없는 문자 시퀀스다
문자열을 결합할 때 원본이 바뀌는 것이 아니라 새 문자열이 만들어집니다.
불변성 덕분에 공유와 캐시가 안전하지만 반복 결합에는 비용이 생길 수 있습니다.
아래 프로그램은 같은 텍스트를 char 개수와 코드 포인트 개수로 각각 셉니다. 영문만 넣었을 때 가려지는 UTF-16의 경계를 이모지 입력으로 드러내는 예제입니다.
public class UnicodeText {
public static void main(String[] args) {
String text = "A😀한";
System.out.println("char units=" + text.length());
System.out.println("code points=" + text.codePointCount(0, text.length()));
text.codePoints()
.mapToObj(Character::toChars)
.map(String::new)
.forEach(System.out::println);
String json = """
{"language":"Java","version":25}
""";
System.out.print(json);
}
}concat, replace, toUpperCase의 반환값을 받지 않으면 원래 문자열은 그대로입니다. 같은 참조를 수정한다고 생각하지 말고, 변환 결과를 새 변수에 담는 데이터 흐름으로 읽어야 합니다.
char units=4
code points=3
A
😀
한
{"language":"Java","version":25}이모지는 char 두 개지만 codePoints 순회에서는 하나의 코드 포인트로 처리됩니다.
터미널의 출력 인코딩이나 글꼴이 유니코드를 지원하지 않으면 코드 포인트 계산은 같아도 이모지가 ?, 한글이 깨진 문자로 보일 수 있습니다. 이 경우 문자열 로직을 바꾸기 전에 터미널을 UTF-8 출력으로 맞추고 다시 확인합니다.
텍스트를 다룰 때 필요한 단위는 작업에 따라 달라집니다.
- Java API의 인덱스와
length()를 다룰 때는 UTF-16 코드 단위를 봅니다. - 유니코드 문자 자체를 보존하며 순회할 때는
codePoints()를 사용합니다. - 사용자가 화면에서 한 글자로 인식하는 범위를 자르려면 결합 문자까지 처리하는 별도 국제화 도구가 필요합니다.
즉 char, 코드 포인트, 화면의 글자는 이름만 다른 같은 단위가 아닙니다.
이스케이프는 소스 표기와 실제 값을 구분한다
\n, \t, \"는 소스 안에서 특수 문자를 표현하는 방법입니다.
출력 결과의 문자 수와 소스 코드의 글자 수를 같은 것으로 보면 안 됩니다.
소스의 \n 두 글자는 컴파일 뒤 하나의 줄바꿈 문자가 되지만 \\n은 역슬래시와 n을 그대로 남깁니다. 로그나 정규식을 작성할 때 화면에 보이는 표기와 문자열 안의 실제 문자를 구분하지 않으면 이스케이프가 한 겹씩 어긋납니다.
char unit = '한';
int point = "😀".codePointAt(0);// char emoji = '😀'; // 컴파일 오류: 이 문자는 UTF-16 코드 단위 두 개가 필요
String escaped = "line1\nline2";
String literalSlashN = "line1\\nline2";텍스트 블록은 여러 줄 구조를 보존한다
세 개의 큰따옴표로 만든 텍스트 블록은 JSON, SQL, HTML 같은 여러 줄 텍스트를 읽기 쉽게 합니다.
들여쓰기 제거와 마지막 줄바꿈 규칙을 실제 출력으로 확인해야 합니다.
코드 포인트 순회는 보조 문자를 보존한다
String.codePoints()는 서로게이트 쌍을 하나의 정수 코드 포인트로 제공합니다.
사용자 입력을 문자 단위로 검사할 때 char 인덱스보다 맞는 기준일 수 있습니다.
사용자 인식 글자 수는 더 복잡할 수 있다
코드 포인트 하나가 항상 화면의 글자 하나인 것도 아닙니다.
결합 문자와 이모지 시퀀스를 정확히 자르려면 문자 경계 알고리즘이 필요합니다.
"A😀"를 넣고 length(), codePointCount() 결과를 비교해 보세요. 둘이 다르다는 사실은 String.length()가 사람이 보는 글자 수가 아니라 UTF-16 코드 단위 수를 반환한다는 가장 짧은 증거입니다.
Java 텍스트를 안전하게 다루려면 char 개수와 사람이 읽는 문자 개수를 필요에 따라 구분해야 합니다.