[Engineering Note] 문자열 인코딩의 이해: ASCII부터 UTF-8까지
Summary: 개발자가 반드시 알아야 할 문자열 인코딩의 기본 원리 정리. Charset과 Encoding의 차이를 명확히 하고, 한글 깨짐 현상(Mojibake)의 기술적 원인과 UTF-8이 표준이 된 이유를 비트(Bit) 단위로 분석한다.
1. 서론: 문자는 숫자의 약속이다
컴퓨터는 0과 1밖에 모른다. 우리가 화면에서 보는 'A'나 '한글'이라는 글자는 결국 특정 숫자에 매핑된 약속일뿐이다. 이 약속은 크게 두 가지 개념으로 나뉜다.
- Character Set (문자 집합): 표현할 수 있는 글자들의 목록과 각 글자에 부여된 번호(Code Point). (예: Unicode, ASCII)
- Encoding (인코딩): 그 번호를 컴퓨터가 이해할 수 있는 이진수(Binary)로 저장하는 방식. (예: UTF-8, ASCII)
2. 인코딩의 진화 과정
2.1. ASCII (American Standard Code for Information Interchange)
1960년대, 영문 알파벳과 숫자, 제어 문자를 표현하기 위해 제정되었다. 7비트(bit)를 사용하여 총 128개의 문자를 표현한다.
- 범위: 0x00 ~ 0x7F
- 특징: 1바이트(8비트) 중 맨 앞 1비트(MSB)는 Parity Bit로 남겨두고 사용하지 않았다. 영미권 외의 문자는 표현이 불가능하다는 한계가 있다.
2.2. EUC-KR vs CP949 (한글의 혼란기)
한국어 윈도우 환경에서 개발할 때 가장 많이 겪는 혼란이다. 둘 다 2바이트로 한글을 표현하지만 범위가 다르다.
| 구분 | 설명 | 특징 |
|---|---|---|
| EUC-KR | 완성형 한글 표준 | 자주 쓰는 한글 2,350자만 표현 가능. '믜', '햏' 같은 글자는 깨짐. |
| CP949 | MS 확장 완성형 | EUC-KR을 확장하여 11,172자를 모두 표현 가능. 윈도우 메모장의 기본 저장 방식(ANSI). |
3. 사실상의 표준: Unicode와 UTF-8
국가별로 제각각인 인코딩 방식(EUC-KR, Shift_JIS 등)을 통합하기 위해 유니코드(Unicode)가 등장했다. 유니코드는 '전 세계 모든 문자에 고유 번호(Code Point)를 부여'한 것이다.
중요한 점은 유니코드 그 자체는 인코딩 방식이 아니라는 것이다. 이 유니코드 번호를 어떻게 저장하느냐에 따라 UTF-8, UTF-16 등으로 나뉜다.
왜 UTF-8인가?
UTF-8은 가변 길이 인코딩(Variable-width encoding) 방식을 채택했다.
- 영어(ASCII): 1바이트 사용 (기존 ASCII와 100% 호환).
- 한글/한자: 주로 3바이트 사용.
- 이모지: 4바이트 사용.
이러한 특성 덕분에 영문이 많은 문서에서는 용량을 획기적으로 줄일 수 있으며, 엔디안(Endian) 문제에서 자유롭기 때문에 웹 표준으로 자리 잡았다.
4. 트러블슈팅: 한글은 왜 깨지는가?
한글이 깨지는 현상(Mojibake)은 인코딩 방식(저장할 때)과 디코딩 방식(읽을 때)이 일치하지 않을 때 발생한다.
Case 1: 'ëÂÃ...' (UTF-8을 ISO-8859-1로 읽음)
UTF-8로 저장된 한글(3바이트)을 1바이트씩 끊어서 읽는 라틴 계열 인코딩(ISO-8859-1 등)으로 해석할 때 발생한다. 톰캣(Tomcat) 등의 기본 설정이 ISO-8859-1인 경우가 많아 자주 목격된다.
Case 2: '????' (표현 불가)
유니코드 문자를 EUC-KR이나 ASCII 처럼 해당 문자가 없는 인코딩으로 변환하려 할 때 발생한다. 정보가 유실되었으므로 복구가 불가능하다.
5. Python을 이용한 검증
다음은 파이썬을 이용해 문자열이 바이트로 변환되는 과정과, 잘못된 디코딩이 어떤 결과를 초래하는지 보여주는 예제다.
# 1. 문자열 준비
text = "한글"
# 2. 인코딩 (String -> Bytes)
utf8_bytes = text.encode('utf-8')
euc_bytes = text.encode('euc-kr')
print(f"UTF-8 Bytes: {utf8_bytes}")
# 출력: b'\xed\x95\x9c\xea\xb8\x80' (한 글자당 3바이트: ed 95 9c / ea b8 80)
print(f"EUC-KR Bytes: {euc_bytes}")
# 출력: b'\xc7\xd1\xb1\xdb' (한 글자당 2바이트: c7 d1 / b1 db)
# 3. 잘못된 디코딩 (Bytes -> String)
try:
# UTF-8로 인코딩된 데이터를 EUC-KR로 해석 시도
broken_text = utf8_bytes.decode('euc-kr')
print(f"Broken: {broken_text}")
except UnicodeDecodeError as e:
print(f"Decoding Error: {e}")
# 데이터 비트 조합이 EUC-KR 테이블에 없으면 에러 발생,
# 억지로 매핑되면 이상한 한자나 외계어 출력
6. Conclusion
문자열 인코딩 문제는 기초적이지만, 데이터베이스 마이그레이션이나 레거시 시스템 연동 시 치명적인 버그를 유발한다.
- 가능하면 모든 시스템(DB, Server, Client)의 인코딩을 UTF-8로 통일하라.
Content-Type헤더나meta태그에 반드시charset=utf-8을 명시하라.- 한글이 깨진다면, 원본 데이터의 헥사(Hex) 값을 확인하여 몇 바이트로 구성되었는지 파악하는 것이 해결의 첫걸음이다.