이 글은 [유니코드] 유니코드 문자열을 정규화 해야하는 이유에 이어지는 글입니다.
UTF-8
UTF-8은 8비트(1바이트)로 인코딩한다는 것을 의미한다. UTF-8은 아스키코드와 완벽하게 호환되며, 표현하려는 문자에 따라 최소 1바이트에서 최대 6바이트까지 사용한다.

‘바이트 1’부터 ‘바이트 6’까지 있는 1과 0은 고정된 비트 값이며, 사용하는 바이트 수에 따라 달라진다. ‘x’ 값은 유니코드를 저장하는 데 사용할 비트 영역이다.
‘1 바이트 수’ 문자는 첫 번째 비트 값이 0이므로 0을 제외한 나머지 비트 7개로 문자를 표현한다. 0부터 127까지의 수로 문자를 표현하는 아스키 코드와 같은 규칙을 사용하므로 UTF-8은 아스키 코드와 완벽히 호환된다는 것이다.
‘2 바이트 수’ 문자는 비트 값이 110으로 시작된다. 이 경우에는 2바이트로 UTF-8 문자를 읽어야 한다. 첫 번째 바이트의 110xxxxx에서 110을 제외한 비트 5개, 두 번째 바이트의 10xxxxxx에서 10을 제외한 비트 6개를 조합하면 총 11개의 비트로 UTF-8 문자를 만들 수 있다.
예를 들어 ‘안’을 구성하는 16진 수 값은 0xec, 0x95, 0x88이다.
- 0xec: 1110 1100
- 0x95: 1001 0101
- 0x88: 1000 1000
해당 값을 차례대로 나열하면 다음과 같다.
- 11101100 100110101 10001000
위의 표를 보면 첫 번째 바이트가 1110으로 시작할 때는 3바이트가 1개의 글자가 되는 것을 알 수 있다. 두 번째 바이트와 세 번째 바이트에 있는 비트 값 10은 UTF-8 형식을 구성하는 용도로만 사용하므로 실제로 값을 읽을 때는 사용하지 않는다.
따라서 첫번째, 두 번째, 세 번째 형식 구성 비트들을 모두 제거하고 다시 한번 조합하면 다음과 같은 값을 만들 수 있다.
- 1100010101001000
해당 값을 16진수로 변환하면 0xC548이다.

보통 일반적인 문자는 3바이트 내로 처리되며, 4바이트 영역에는 이모지 같은 문자가 있으며, 고대 문자 같은 것을 사용하지 않는 한 5바이트 이상을 쓰는 경우는 거의 없다고 한다.
UTF-16
UTF-16은 16비트(2바이트)로 인코딩 하는 것을 의미한다. UTF-16은 2바이트 또는 4바이트만 사용하기 때문에 아스키 코드와 호환되지 않는다.
유니코드에는 문자의 종류에 따라 기본 다국어 평면(Basic Multilingual Plane, BMP), 보충 다국어 평면(Supplenmentray Multilingual Plane, SMP), 상형 문자 보충 평면(Supplementary Ideographic Plane, SIP), 특수 목적 보충 평면(Supplenmentray Special-purpose Plane, SSP) 등 평면 4개가 있고, 바이트 수는 표현하려는 문자가 어떤 평면에 속하는지에 따라 결정된다.
fun String.toUtf16Hex(): String {
val utf16Bytes = this.toByteArray(StandardCharsets.UTF_16)
println("Total UTF-16 Bytes: ${utf16Bytes.size}")
return utf16Bytes.joinToString(" ") { byte -> String.format("0x%02X", byte) }
}
Total UTF-16 Bytes: 12
UTF-16 Hexadecimal Bytes: 0xFE 0xFF 0xC5 0x48 0xB1 0x55 0xD5 0x58 0xC1 0x38 0xC6 0x94
- 그런데 ‘안녕하세요’ 문자열의 16진수 출력 결과 총 12바이트를 사용했고 맨 앞에 2 바이트 값인 0xFE, 0xFF 값이 추가된 것을 볼 수 있다.
- 한 글자당 2바이트로 추측이 되었으나 왜 10바이트가 아닌 12바이트 일까? 바로 BOM 때문이다.
바이트 순서 표시
UTF-16과 UTF-32는 바이트 순서 표시(byte order mark, BOM)을 사용한다. BOM은 문자열 가장 맨 앞 2바이트에 0xFEFF(U+FEFF)로 표기하여 사용한다는 것을 의미한다.
또한 0xFE와 0xFF 중 어떤 문자가 먼저 오는지에 따라 little endian, big endian으로 나뉜다. 그래서 두 방식에 따라 문자열 인코딩 시 바이트 데이터를 조합하는 순서가 바뀌게 된다.
BOM을 이용하여 바이트 표현 순서를 정하는 이유는 CPU 설계에 따라 바이트 값을 처리하는 순서가 다르기 때문이다. 같은 0xFEFF를 CPU가 읽을 때 리틀 엔디언 방식은 0xFF 다음 0xFE을 읽으며, 빅 엔디언 방식은 그 반대이다.
UTF-8에 BOM이 없는 이유는 무엇일까? BOM에 해당 하는 값이 있지만(0xEF, 0xBB, 0xBF) 1바이트 단위로 글자를 변환하기 때문에 글자를 읽는 순서가 달라도 영향을 받지 않는다. 따라서 UTF-8은 BOM을 사용할 필요가 없고 권장하지도 않는다.
하지만 MS Excel에서는 utf-8 csv 파일을 읽는 경우 BOM으로 저장되지 않는 경우 한글이 깨지게 된다. 그렇지 않으면 별도의 절차를 통해 올바르게 열어야 깨지지 않는다..
UTF-32
UTF-32는 모든 문자를 고정된 4바이트 길이로 사용한다. 이 특징을 제외하면 UTF-16과 동일한 규칙을 사용하기 때문에 더 많은 바이트만 사용하는 것 외에는 별다른 특징이 없다.
요약
UTF-8
- 오늘날 가장 많이 사용하는 문자열 인코딩이며 최소 1바이트, 최대 6바이트 사용
- 대부분 4바이트 내로 처리
- 아스키 코드와 호환 가능
- 윈도우, 자바, 임베디드를 제외한 거의 모든 환경에서의 문자열 처리 표준
- JSON은 UTF-8 인코딩만 사용하며, 다른 문자열 인코딩은 표준에서 지원하지 않음
UTF-16
- 자바와 윈도우는 유니코드를 사용하기 전부터 고정된 2바이트 길이의 문자 집합을 사용 한다.
- 그래서 멀티 바이트라고도 한다.
- 2바이트 또는 4바이트의 길이의 문자열을 사용하며, 아스키 코드와 호환되지 않는다.
- UTF-16 기반 환경에서 UTF-8을 사용할 때는 사용 영역을 명확히 구분하는게 좋다.
- 내부에서는 16을 사용하되 외부 통신시 8로 변환하여 사용하는 등
UTF-32
- 4바이트를 고정적으로 사용
- 반드시 사용해야 하는 환경이 아니라면 사용하지 않는다
참고 출처
- https://d2.naver.com/helloworld/19187
- https://brownbears.tistory.com/124
- 도서 [학교에서 알려주지 않는 17가지 실무 개발 기술]
'design & development' 카테고리의 다른 글
[유니코드] 유니코드 문자열을 정규화 해야하는 이유 (0) | 2025.01.11 |
---|---|
[만들면서 배우는 클린 아키텍처] 12. 아키텍처 스타일 결정하기 (0) | 2024.11.26 |
[만들면서 배우는 클린 아키텍처] 11. 의식적으로 지름길 사용하기 (1) | 2024.11.25 |
[만들면서 배우는 클린 아키텍처] 10. 아키텍처 경계 강제하기 (1) | 2024.11.24 |
[만들면서 배우는 클린 아키텍처] 9. 애플리케이션 조립하기 (0) | 2024.11.23 |