유니코드란, 숫자와 글자, 즉 키와 값이 1:1로 매핑된 형태의 코드로 모든 심볼을 표현하겠다는 목적을 가지고 출범한 프로젝트이다. 즉, 문자 외의 기호나 이모지 등 역시 유니코드에 매핑되어 있다.
유니코드 하나는 하나의 문자 혹은 기호와 매칭되지만 java에서 하나의 유니코드가 한글자를 의미하지는 않는다.
자바는 인코딩 방식으로 UTF-16을 채택하고 있어 Character 하나에 2byte를 할당한다. (한번에 2byte씩 읽음)
각 OS와 소프트웨어마다 채택하고 있는 인코딩 방식이 다르기 때문에 바이트코드를 유니코드로 디코딩한 후 다시 인코딩하는 과정을 거친다. 어떤 방식으로 인코딩 되어있는지는 BOM(Byte Order Mark)을 통해 전달한다.
7비트인코딩으로, 33개의 출력 불가능한 제어 문자들과 공백을 비롯한 95개의 출력 가능한 문자들로 총128개로 이루어져 있다. 제어 문자들은 역사적인 이유로 남아 있으며 대부분은 더 이상 사용되지 않는다. 출력 가능한 문자들은 52개의 영문 알파벳 대소문자와, 10개의 숫자, 32개의 특수 문자, 그리고 하나의 공백 문자로 이루어진다.
UTF-8은 byte를 하나씩 읽어서 유니코드를 표현한다. 하지만 1byte로는 유니코드의 수많은 문자들을 커버하기엔 역부족이기 때문에 가변적으로 필요에 따라 byte를 추가하면서 unicode를 표현한다.(최대 4byte까지)
이때 몇개의 byte가 하나의 유니코드를 표현하는지 알 수가 없기 때문에 첫번째 byte에서 해당 유니코드가 총 몇byte에 걸쳐 표현되는지 전달하고 나머지 비트들에선 하나의 유니코드에 포함된다는 표시를 해준다.
| 코드값의 자릿수 | 범위 | 첫 바이트 | 둘째 바이트 | 셋째 바이트 | 넷째 바이트 |
|---|---|---|---|---|---|
| 7bits | 0-127 | 0xxxxxxx | |||
| 11bits | 128-2047 | 110xxxxx | 10xxxxxx | ||
| 16bits | 2048-65,535 | 1110xxxx | 10xxxxxx | 10xxxxxx | |
| 21bits | 65,535-2,097,151 | 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx |
java에서 ‘다’의 bytecode와 유니코드 넘버를 확인해보자
public static void main(String[] args) {
// UTF-8 캐릭터셋 설정하여 bytecode로 변환
byte[] c = "다".getBytes(StandardCharsets.UTF_8);
// 1)byte 각각 출력
for (int i = 0; i < c.length; i++) {
String binaryString = String.format("%8s", Integer.toBinaryString(c[i] & 0xFF)).replace(' ', '0');
System.out.println(i + 1 + "번째 byte: " + binaryString);
}
// 2)'다'의 유니코드 넘버를 2진수로 출력
System.out.println(Integer.toBinaryString("다".codePointAt(0)));
// 3)2진수로 출력한 유니코드 넘버 확인
System.out.println(Integer.parseInt("1011001011100100", 2));
// 4)유니코드 넘버 다시 2진수로 변환하여 확인
System.out.println(Integer.toBinaryString(45796));
}
//출력 결과
//1)
11101011
10001011
10100100
//2)
1011001011100100
//3)
45796
//4)
1011001011100100
“다”의 bytecode는 11101011 10001011 10100100이다.
“다”의 유니코드 넘버의 이진수 표현은 indicator 역할을 하는 숫자들을 제외한 밑줄친 bit들의 나열과 같다는 것을 알 수 있다.
그리고 해당 이진수는 45796임을 확인할 수 있다.
한번에 2byte씩 읽어서 유니코드를 표현하며 2byte씩 읽기 때문에 byte order가 중요하다.
UTF-8과 마찬가지로 2byte로 유니코드를 다 표현하지 못하기 때문에 가변적으로 4byte로도 하나의 유니코드를 표현한다.
가변적이기 때문에 4byte일 경우 처음 나오는 2byte에서 총 몇byte가 하나의 유니코드를 표현하기 위해 사용되었는지 표시해야 한다.
UTF-16에서 4byte가 사용되었다는 것을 알리기 위해 사용하는 방법이다.
U+D800 ~ U+DFFF의 구간을 아무것도 할당하지 않고 이를 다시 high-surrogates(U+D800 ~ U+DBFF) 나머지 하나는 low-surrogates(U+DC00 ~ U+DFFF)구간으로 나눈다.
첫 byte가 high-surrogates 구간에 있는 것을 확인하면 자연적으로 다음 2byte까지 하나의 유니코드에 포함되는 것을 알리게 되며 해당 byte는 low-surrogates(U+DC00 ~ U+DFFF)구간에 있다.
각 surrogate는 2^10의 데이터를 담을 수 있으며 이에 따라 2^20의 유니코드를 추가적으로 표현할 수 있게 되었다.
UTF-16은 2byte로 65536개의 유니코드를 표현할 수 있다고 보통 되어있지만 실제로는 surrogate 구간은 비워두었기에 해당 부분의 개수만큼은 빼야한다.
한글은 영어와 달리 문자들이 분리되어 있지 않다. ‘a’는 ‘a’ 그대로 쓰이지만 ‘ㄱ’은 ‘ㅏ’와 결합하여 ‘가’로 쓰이기도 하고 ‘ㅝ’와 결합하여 ‘궈’로 쓰이기도 한다. 따라서 두문자가 결합한 ‘ab’는 UTF-16에서 4byte라는 것을 쉽게 알 수 있지만 마찬가지로 두문자가 결합한 ‘가’나 혹은 세문자가 결합한 ‘강’ 은 각각 4byte, 6byte일까? 우리에겐 모두 한글자로 보임에도? 실제로 우리나라 외의 조합형 문자를 가진 나라들은 이런식으로 할당된다. 하지만 대한민국과 일본은 전세계적인 관점에서 컴퓨터를 빠르게 받아들인 나라로서 모든 조합된 문자가 유니코드에 등록되는 특혜를 누렸기에 ‘가’와 ‘강’같은 문자도 2byte로 계산된다.
그럼 다른 조합형 문자들, 조합된 문자들이 유니코드에 등록되어 있지 않은 문자들은 조합문자를 어떻게 표현할까? ㄱㅏ 혹은 ㄱㅏㅇ 이런식으로 나오는 것일까? 이를 위해서 소프트웨어의 힘이 들어간다. ‘ㄱ’과 ‘ㅏ’를 결합하면 소프트웨어가 ‘가’로 보이게 해준다. 이것을 grapheme cluster라고 한다.
이를 통해서 조합문자가 등록되지 않은 문자들에도 폰트를 적용할 수 있게 되었다. 노션 등에서
콜론 뒤에 특정 문자를 붙이면 특정 이모티콘으로 변하는 것을 확인할 수 있는데 이런 것들이 모두 grapheme cluster이다. 폰트적용 대신에 이미지를 매핑해서 불러오는 것이다.