유니코드의 배경

유니코드는 전 세계의 모든 문자를 통일된 방식으로 표현하기 위해 고안된 표준이다. 과거 ASCII는 128개의 문자만을 표현할 수 있었으며, 이는 영어를 제외한 다른 언어를 지원하지 못하는 문제가 있었다. 유니코드는 각 문자를 고유한 코드 포인트(Code Point)로 표현하여 이러한 문제를 해결했다.

코드 포인트와 인코딩

  • 코드 포인트: 유니코드에서 각 문자를 U+XXXX 형식의 고유 번호로 표현된다. 예를 들어 ‘A’는 U+0041로, ‘가’는 U+AC00으로 표현된다.
  • 인코딩: 코드 포인트를 실제로 저장하거나 전송하려면 바이트로 변환해야 한다. 주요 인코딩 방식은 다음과 같다.
    • UTF-8: 가변 길이 인코딩 방식으로, 효울적이고 가장 널리 사용된다.
    • UTF-16: 16비트로 고정 길이 인코딩 방식이다.
    • UTF-32: 32비트로 고정 길이 인코딩 방식이다.

2. UTF-8 인코딩 방식

UTF-8은 유니코드 문자를 1~4 바이트의 가변 길이로 인코딩하는 방식이다. 유니코드는 전 세계의 모든 문자를 표현하기 위해 수천 개의 코드 포인트를 사용한다. 모든 문자를 동일한 바이트 수로 인코딩하면 비효율적일 수 있다.

예를 들어, 영어 알파벳은 코드 포인트가 작아 1바이트로 충분하지만 한글, 중국어는 더 큰 코드 포인트를 가지므로 더 많은 바이트가 필요하다.

UTF-8에서 코드 포인트 범위에 따라 문자에 할당되는 바이트 수가 다르다.

코드 포인트 범위 (16진수) 바이트 수 첫 바이트 추가 바이트 구조
U+0000 ~ U+007F 1바이트 0xxxxxxx -
U+0080 ~ U+07FF 2바이트 110xxxxx 10xxxxxx
U+0800 ~ U+FFFF 3바이트 1110xxxx 10xxxxxx 10xxxxxx
U+10000 ~ U+10FFFF 4바이트 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
  1. 가'의 유니코드 코드 포인트는 U+AC00이다. 코드 포인트 범위는 U+0800 ~ U+FFFF`이므로 3바이트로 인코딩된다.
  2. 2진수로 변환하면 U+AC00은 1010 1100 0000 0000이다.
  3. 이를 3바이트로 분할하면, UTF-8은 규칙에 따라 첫 바이트는 4비트, 나머지 바이트는 6비트씩 나눈다.
10101100 00000000

아래 구조에 따라 위의 2진수를 3바이트로 인코딩한다.
1110xxxx 10xxxxxx 10xxxxxx

첫 4비트, 6비트, 6비트를 각각 대입하면 다음과 같다.

-> 1110 `1010` 10 `110000` 10 `000000`
-> 11101010 10110000 10000000
-> 0xEA 0xB0 0x80

흥미로운 패턴이다.

효율성: UTF-8은 가변 길이 인코딩을 통해 공간을 효율적으로 사용한다. 예를 들어, ASCII 문자는 1바이트, 유니코드의 큰 범위 문자는 2~4바이트로 표현되므로 전체 파일 크기를 줄일 수 있다.

명확한 구분: 첫 바이트의 비트 패턴 (0, 110, 1110, 11110)을 통해 각 문자의 바이트 수를 쉽게 식별할 수 있다. 추가 바이트는 항상 10으로 시작하므로 바이트 경계가 깨졌을 때도 복구 가능하다.

1바이트의 인코딩 시 비트 패턴을 10으로 시작하지 않은 이유는 추가 바이트 구조와 구분하기 위함인 것 같다. 그리고 ASCII 문자는 1바이트로 인코딩되므로 첫 바이트가 0으로 시작한다. 아마도 초기 ASCII 문자를 유지하기 위함인 것 같다.

3. python과 Unicode

python의 str은 유니코드 문자열을 나타낸다. str은 유니코드 코드 포인트를 저장하며, 인코딩 방식에 따라 바이트로 변환된다. str은 불변(immutable)이며, 인덱싱을 통해 개별 문자에 접근할 수 있다. 문자열이 생성되면 수정할 수 없다. 흔히 오해하는 것이 문자열 변수에 선언한 값을 변경할 수 있다는 것이다.

>>> a = 'hello'
>>> id(a)
4343245488
>>>
>>> a = 'world'
>>> id(a)
4343245552

하지만 이것은 새로운 문자열을 생성하고 변수에 할당하는 것이다.

출력 결과에서 id(a)의 값이 변경되었음을 확인할 수 있다. 이는 변수 a가 더 이상 ‘hello’ 객체를 참조하지 않고, 새로운 문자열 객체 ‘world’를 참조한다는 것을 의미한다.

4. interning

파이썬에서 문자열이 불변 객체이기 때문에 같은 문자열을 여러 번 생성하면 메모리를 낭비할 수 있다. 파이썬은 이를 해결하기 위해 문자열 interning 기능을 제공한다.

자주 사용되거나 불변 속성을 가진 객체를 재사용하기 위해 동일한 객체를 공유하는 최적화 기술이다.

interning 사례

  • CPython은 -5부터 256까지의 정수를 인터닝한다. 이 범위의 정수는 실행 중 재사용되며, 새로 생성되지 않는다.
a = 100
b = 100
print(a is b)  # True

c = 1000
d = 1000
print(c is d)  # False (인터닝되지 않음)
  • 불변 문자열 중 특정 조건을 만족하는 경우 인터닝된다.
    • 알파벳과 숫자로만 이루어진 문자열
    • 작은 길이의 문자열
    • 문자열 리터럴(코드에 직접 작성된 문자열)
>>> a = 'hello'
>>> b = 'hello'
>>> a is b
True
>>> a = 'hello world'
>>> b = 'hello world'
>>> a is b
False
  • 성능 이점 동일한 값을 가진 객체가 여러번 생성되는 대신, 하나의 객체를 공유하므로 메모리를 절약한다. is 연산자는 객체의 동일성을 비교하므로, 값 비교보다 빠르게 결과를 반환한다.

참고