파이썬 문자열이 유니코드를 어떻게 처리하는지, 인코딩이 왜 필요한지 깊이 이해한다.
🎯 이 강의에서 배우는 것
파이썬 3에서 문자열은 단순히 글자들의 나열이 아닙니다. 전 세계 모든 언어를 지원하는 유니코드 코드 포인트의 불변 시퀀스입니다. 이 강의에서는 유니코드와 UTF-8의 원리, 문자열이 왜 불변이어야 하는지, 그리고 인코딩 오류가 왜 발생하고 어떻게 해결하는지를 완전히 이해합니다.
🌐 ASCII에서 유니코드로 — 왜 유니코드가 필요한가
초창기 컴퓨터는 영어권에서 발명됐습니다. ASCII(American Standard Code for Information Interchange)는 128개의 문자만 표현합니다: 영문자(대·소문자), 숫자, 특수문자. 한글, 한자, 아랍어, 이모지는 전혀 표현할 수 없었습니다.
각 나라들이 자국 언어를 위한 인코딩을 따로 만들기 시작했습니다. 한국의 EUC-KR, 일본의 Shift-JIS, 중국의 GB2312 등. 서로 다른 인코딩의 파일을 잘못된 인코딩으로 열면 글자가 깨지는 이른바 "모지바케(문자 깨짐)"가 발생했습니다.
유니코드(Unicode)는 이 문제를 해결하기 위해 전 세계 모든 문자에 고유한 번호(코드 포인트)를 부여하는 표준입니다:
# 유니코드 코드 포인트 확인
print(ord("A")) # 65 (U+0041)
print(ord("가")) # 44032 (U+AC00)
print(ord("😀")) # 128512 (U+1F600)
print(ord("漢")) # 28450 (U+6F22)
# 코드 포인트에서 문자로
print(chr(65)) # A
print(chr(44032)) # 가
print(chr(128512)) # 😀
# 유니코드 이름 확인
import unicodedata
print(unicodedata.name("가")) # HANGUL SYLLABLE GA
print(unicodedata.name("😀")) # GRINNING FACE
현재 유니코드는 140,000개 이상의 코드 포인트를 정의합니다. U+0000부터 U+10FFFF까지 약 110만 개의 슬롯이 있으며, 계속 새로운 문자(이모지 포함)가 추가됩니다.
🔤 UTF-8: 유니코드 코드 포인트를 바이트로 변환하기
유니코드는 코드 포인트의 목록이지, 저장 방식이 아닙니다. 실제로 파일이나 네트워크에 저장·전송하려면 코드 포인트를 바이트로 변환해야 합니다. 이 변환 방식을 인코딩(encoding)이라고 합니다.
UTF-8은 현재 가장 많이 사용되는 유니코드 인코딩입니다:
| 코드 포인트 범위 | 바이트 수 | 예시 |
|---|---|---|
| U+0000 ~ U+007F | 1바이트 | ASCII 문자 (A, 0, @) |
| U+0080 ~ U+07FF | 2바이트 | 라틴어 확장, 아랍어 |
| U+0800 ~ U+FFFF | 3바이트 | 한글, 한자, 일본어 |
| U+10000 ~ U+10FFFF | 4바이트 | 이모지, 고대 문자 |
# str → bytes 인코딩 (encode)
text = "안녕, Python!"
encoded_utf8 = text.encode("utf-8")
print(encoded_utf8) # b'\xec\x95\x88\xeb\x85\x95, Python!'
print(len(text)) # 12 (문자 수)
print(len(encoded_utf8)) # 18 (바이트 수 — 한글 각 3바이트)
# bytes → str 디코딩 (decode)
decoded = encoded_utf8.decode("utf-8")
print(decoded) # 안녕, Python!
# 다른 인코딩
encoded_cp949 = text.encode("cp949") # 한국 윈도우 기본 인코딩
print(len(encoded_cp949)) # 14 (한글 각 2바이트)
# 인코딩 불일치 예시
try:
# UTF-8로 인코딩된 데이터를 CP949로 디코딩 시도
encoded_utf8.decode("cp949")
except UnicodeDecodeError as e:
print(f"오류: {e}") # 인코딩 불일치로 오류 발생
UTF-8이 널리 사용되는 이유:
- ASCII 하위 호환: ASCII 문자(코드 포인트 0-127)는 UTF-8에서 동일한 1바이트로 표현됩니다. 영문 텍스트를 처리하는 기존 시스템과 호환됩니다.
- 자기 동기화: 멀티바이트 시퀀스의 어느 바이트에서든 문자 경계를 찾을 수 있습니다.
- 공간 효율: 주로 ASCII를 사용하는 텍스트는 UTF-32(모든 문자를 4바이트)보다 훨씬 적은 공간을 사용합니다.
🔒 문자열의 불변성 — 왜 str은 바꿀 수 없는가
s = "hello"
# 인덱싱으로 읽기는 가능
print(s[0]) # h
# 인덱싱으로 쓰기는 불가 — TypeError 발생
try:
s[0] = "H"
except TypeError as e:
print(f"오류: {e}") # 'str' object does not support item assignment
# 수정처럼 보이는 것은 사실 새 객체 생성
s2 = s.replace("hello", "Hello")
print(s) # hello — 원본 변경 없음
print(s2) # Hello — 새 객체
print(s is s2) # False
문자열이 불변(immutable)인 이유:
- 해시 가능성: 불변 객체는 해시 가능합니다. 이 덕분에 문자열을 딕셔너리 키나 셋 원소로 사용할 수 있습니다.
- 인터닝 최적화: 같은 내용의 문자열 리터럴을 재사용(인터닝)할 수 있습니다.
- 스레드 안전: 불변 객체는 여러 스레드에서 동시에 읽어도 안전합니다.
문자열 연결(+)의 함정: 루프에서 + 연산자로 문자열을 계속 이어붙이면 매번 새 객체가 생성됩니다:
import time
# 느린 방법: + 연산자 반복 (O(n^2))
start = time.perf_counter()
result = ""
for i in range(100_000):
result += str(i)
print(f"+ 연산자: {time.perf_counter() - start:.3f}초")
# 빠른 방법: join() (O(n))
start = time.perf_counter()
result = "".join(str(i) for i in range(100_000))
print(f"join(): {time.perf_counter() - start:.3f}초")
# join()이 5~10배 빠름
💡 CPython의 문자열 내부 표현
CPython 3는 문자열의 메모리 효율을 위해 내용에 따라 세 가지 내부 인코딩 중 하나를 선택합니다:
import sys
# Latin-1 (ISO-8859-1): 모든 코드 포인트가 U+00FF 이하면 1바이트/문자
s_ascii = "hello"
print(sys.getsizeof(s_ascii)) # 54 bytes (문자당 1바이트)
# UCS-2: 가장 큰 코드 포인트가 U+FFFF 이하면 2바이트/문자
s_korean = "안녕"
print(sys.getsizeof(s_korean)) # 76 bytes (문자당 2바이트)
# UCS-4: U+10000 이상 코드 포인트 포함 시 4바이트/문자 (이모지 등)
s_emoji = "😀"
print(sys.getsizeof(s_emoji)) # 80 bytes (문자당 4바이트)
# 같은 문자열에 이모지가 하나만 들어가도 전체가 UCS-4가 됨
s_mixed = "안녕😀"
print(sys.getsizeof(s_mixed)) # 문자당 4바이트 × 3문자
🔗 문자열 인터닝
import sys
# CPython은 짧은 식별자 형태 문자열을 자동 인터닝
a = "hello"
b = "hello"
print(a is b) # True (인터닝으로 같은 객체)
# 공백 포함 문자열은 인터닝 안 됨
c = "hello world"
d = "hello world"
print(c is d) # False (실행 환경마다 다를 수 있음)
# sys.intern()으로 명시적 인터닝
e = sys.intern("hello world")
f = sys.intern("hello world")
print(e is f) # True (강제 인터닝)
# 인터닝의 실용적 용도: 대량의 반복 문자열 키 비교 성능 향상
⚠️ 자주 하는 실수
- 파일 인코딩 명시 안 함:
open("file.txt")는 운영체제 기본 인코딩을 사용합니다. Windows는 CP949, Linux/macOS는 UTF-8. 항상encoding="utf-8"을 명시하세요. - 루프에서 + 연산자로 문자열 연결: O(n²)의 시간 복잡도.
"".join(list)를 사용하세요. len()이 바이트 수가 아닌 문자 수:len("안녕")은 2 (문자 수). 바이트 수는len("안녕".encode("utf-8"))로 확인합니다.
📝 정리 및 다음 강의 예고
- 유니코드는 전 세계 모든 문자에 코드 포인트를 부여하는 표준이고, UTF-8은 이를 바이트로 변환하는 가장 널리 쓰이는 인코딩입니다.
- 파이썬 str은 유니코드 코드 포인트의 불변 시퀀스입니다.
- 문자열 연결 루프에서는
+대신"".join()을 사용하세요. - 파일 I/O에서 항상
encoding="utf-8"을 명시하세요.
다음 강의: 불리언(bool)과 None의 깊은 의미를 배웁니다. 트루시니스(truthiness)가 무엇인지, 단락 평가(short-circuit evaluation)가 어떻게 작동하는지, 그리고 None을 올바르게 사용하는 방법을 배웁니다.
관련 주제
- 유니코드와 UTF-8 인코딩
- 문자열 불변성
- 문자열 인터닝
- bytes vs str 변환
- UnicodeDecodeError
- join vs 문자열 연결 성능
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 파이썬 기초 40강 — 처음 배우는 프로그래밍
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
댓글
불러오는 중...
