파이썬에서 한글이 깨진다고요? – 파이썬의 한글 입출력과 인코딩에 대해

파이썬의 한글 인코딩에 대해

파이썬의 대화형 인터프리터를 사용하다보면 한글 인코딩의 함정에 빠지기 쉬운데 이를테면 소스를 그대로 해석기로 실행하는 경우에는 인코딩 에러가 안나던 것이, IDLE을 통해서 실행해보면 오류가 난다거나 그 반대의 경우가 있다. 이렇게 이해할 수 없는 상황을 어떻게 해야할까?

몇년 전이라면 그것은 MS의 잘못이거나 파이썬의 잘못이었다. 하지만 윈도에서 한글로 된 데이터를 다뤄야 하는데 파이썬 2를 쓰고 있다면 그것은 매우 높은 확률로 당신의 잘못이다.

현재 실행환경(쉘)의 문자열 인코딩을 확인한다

일단 윈도의 명령줄 도구에서 몇 가지 체크를 우선해보도록 하자.

파이썬 셸(여기서는 2.7.6을 사용했다)를 실행하고 입출력 인코딩을 확인하자. 기본적으로 명령줄에서 실행되는 파이썬 REPL의 인코딩은 터미널의 로케일을 따를테기 때문에 CP949로 되어 있을 확률이 크다. 실제로 확인해보려면 sys 모듈의 stdin.encoding ,stdout.encoding 을 출력해보면 알 수 있다.

>>> import sys
>>> sys.stdin.encoding
'cp949'
>>> sys.stdout.encoding
'cp949'
>>>

실제로 찍어본 결과도 cp949로 되어 있다. cp949는 흔히 EUC-KR이라고 알려져 있는 한글 윈도 제품에서 입출력에 사용하는 인코딩이고, 이 인코딩에서 한글은 웹에서 거의 표준처럼 취급되는 UTF8과는 다른 방식으로 변환된다.

따라서 명령줄에서 실행한 상호대화형 파이썬 셸을 통해서 입력한 문자들은 모두 인코딩 CP949를 적용한 값으로 간주하여 해석되며, 출력되는 문자열 역시 CP949로 인코딩되어 나간다는 의미이다.

유니코드 문자열 테스트

한가지 예를 들어 확인해보자. 한글 ‘가’의 유니코드 값은 ac00이고 이의 파이썬식 코드 표기는 uac00이 될 것이다. 파이썬 2.x에서는 문자열과 유니코드 문자열이 구분되며, 유니코드 문자열을 생성하는 경우 u를 따옴표 앞에 붙여주는 리터럴을 사용할 수 있다.

이렇게 생성한 값은 print 명령으로 찍으면 어떻게될까?  sys.stdout.encoding에서 명시된 인코딩으로 직렬화되어 표준출력으로 나가서 콘솔 화면에는 로 출력될 것이다. 대신에 print 명령이 아니라 값 자체를 평가하면 해당 코드를 확인할 수 있다.

>>> a = u'가'      # 유니코드문자열 리터럴을 적용했다.
>>> type(a)
<type 'unicode'>  # a는 문자열이 아닌 유니코드 타입이다.
>>> a
u'\uac00'         # '가'의 코드값
>>> print a       # print 할 경우에는 문자열로 표현된다.
가
>>>

여기서 잘 구분해야 하는 점은 출력된 문자가 찍히는 콘솔은 파이썬 내부가 아닌 외부 세계라는 점이다. 데이터가 외부세계로 빠져나가는 시점에 인코딩이 적용되어야 한다.

유니코드 문자열인 u'가'print 문을 거칠 때에는 sys.stdout.encoding에 명시된 문자열 인코딩을 통해서 변환되어 송출된다. 그리고 윈도의 명령 프롬프트는 파이썬이 보내준 신호를 다시 CP949 코덱으로 디코딩한 결과(문자 ‘가’)를 화면에 그려준다.

일반 한글 입출력을 확인

이번에는 그냥 '가'에 대해 조사해보자. 참고로 다시 한 번 확인하자면 지금 테스트를 하는 환경은 명령줄 상에서 실행한 파이썬 쉘이며 이때 키보드로 직접 입력하고 있는 한글 문자열은 cp949로 인코딩되어 파이썬 해석기로 전달되는 중이다.

>>> b = '가'
>>> b         # b에는 실제로 바이트들이 저장되어 있다.
'xb0xa1'
>>> print b   # print 하면 저장된 바이트를 인코딩하여 출력한다.
가
>>> b.decode('cp949')  # 해당 값을 cp949로 디코딩하면 유니코드 코드포인트값이된다.
u'uac00'
>>> sys.stdout.write(b.decode('cp949', 'ignore'))  # print b 가 실제로 하는 일이다.
가

b의 값은 u'가' 와는 다른 값이며, 이 값을 cp949로 디코딩하였을 때 유니코드 문자 ‘가’와 같은 값이 되는 것을 확인할 수 있었다. 그리고 디코딩한 결과는 print 문을 거치면서 다시 cp949로 인코딩 되기 때문에 윈도의 기본 콘솔에서 깨짐 없이 표시될 수 있다.

UTF-8로 인코딩한 문자

그럼 UTF8 인코딩을 사용하면 무슨일이 생길까?  윈도의 명령 프롬프트 환경에서는 UTF-8 문자열을 입력할 수 없다. (사실 방법이 있는데, 텍스트 파일로 저장해서 열면된다.) 그래서 유니코드 문자 u'가'를 UTF-8로 인코딩해보자. 그리고 그 값을 print 해보자.

>>> c = a.encode('utf-8')
>>> c
'xeaxb0x80'
>>> print c
媛€

UTF8로 인코딩된 데이터를 CP949로 해석해서 화면에 뿌리기 때문에 당연히 깨진다.  정말 이 이유 때문에 그런것일까? UTF8로 인코딩된 ‘가’를 실제로 CP949로 다시 디코딩했을 때 어떤 문자열이 되는지 확인해보자.

>>> c.decode('cp949','ignore')   # cp949로 디코딩하여 유니코드문자열로 변환
u'u5a9b'
>>> print u'u5a9b'     # 그 결과를 출력...
媛

짠~ 위에서 깨진 문자랑 같은 값이 화면에 나온다.

여기서 얻을 수 있는 결론은 단순하고도 명확하다. print utf-8문자열을 제대로 표시하려면 유니코드 값으로 풀어주거나(디코딩) 아니면 표준 출력이 지원하는 인코딩으로 변환해야 한다. (물론 그 과정에 유니코드로 다시 풀어야 한다.)

그런데 소스코드에 한글을 쓸 때는?

그런데 소스코드에서 한글 문자열을 쓸 때 우리는 u"..." 같은 표기를 쓰지 않는다. (사실 이 유니코드 리터럴을 여기서 처음 본 사람도 많을 것이다.) 어떻게 파이썬은 한글이 들어간 문자열을 에러 없이 해석할 수 있을까?

그것은 비라틴 문자열이 들어간 소스코드가 있는 경우에는 소스파일의 서두에 주석으로 #-*-coding: utf-8 등과 같이 소스 코드를 저장할 때 사용한 인코딩을 명시하는 관례 때문이다. (아마 여러분도 썼을 것이다.) 그러면 파이썬 해석기가 소스 코드 파일을 읽어 들이는 과정에서 이 인코딩을 사용하여 비라틴 문자를 디코딩한 값을 갖게되고, print 문에서는 이렇게 디코딩된 값을 다시 cp949로 인코딩하여 콘솔에 출력하게되니 한글이 깨지지 않는 것이다. 즉 소스 코드 자체의인코딩을 소스에 명시했다면, 실제로는 별 문제 없이 동작할 수 있다.

만약 한글과 같은 비라틴 문자를 쓰면서 인코딩을 명시하지 않으면 예외가 발생하면서 프로그램이 실행되지 않을 것이다. 그런데 여기에는 몇 가지 함정이 더 남아있다.

idle 의 문제

이 챕터에서 설명하는 문제는 파이썬 2.7.12 이전 버전의 idle이 가지고 있던 버그에 관한 것이며, 2.7.13부터 해결된 문제입니다. 만약 idle에서는 멀쩡히 나오는 한글이 콘솔에서 실행하면 깨지는 증상을 겪었고, 이 문제에 관심이 있다면 읽어도 좋지만 그렇지 않다면 건너 뛰어도 상관없습니다.

idle의 설정에서도 소스코드인코딩을 설정하는 부분이 있는데, 그것은 편집기 모드에서 저장하는 글자의 인코딩을 지정하는 것이지, IDLE 셸의 입출력 인코딩과는 관련이 없다.  IDLE은 Tk를 사용해서 GUI를 그리는데, Tk에서는 어떤 인코딩을 사용하는지 잘 모르겠다. 아무래도 콘솔하고는 다른것 같긴한데 그래도 이해가 되지 않는 동작을 할 때가 있다.

이런 문제가 발생할 수 있다. 여기서부터의 입출력 내용은 IDLE 에서 실행한 것이다. (파이썬 2.7)

>>> a = '가'
>>> a
'xb0xa1'        # cp949로 인코딩된 '가'. 즉 입력 인코딩은 cp949이다.
>>> print a
가
>>> b = u'가'
>>> b
u'xb0xa1'      # ?????
>>> print b
°¡
>>> 

먼저 한글 문자열을 변수에 할당하면 정상적인 cp949 코드값으로 입력됨을 알 수 있다. 그런데 유니코드 문자열을 생성하면, 따옴표 안의 문자열을 디코딩해야 하는데, 디코딩하지 않고 그대로 유니코드 문자열로 인식해버린다.

>>> c  = a.decode('cp949')
>>> c
u'uac00'
>>> print c
가

실제로 디코딩한 결과는 정상적이다. 단지 변수에 ascii 외의 코드가 담긴 문자열로 바로 유니코드 문자열을 만들면 안되는 것 같다. 이는 idle의 버그로 보인다.

<update 2017-06-21> 파이썬 2.7.13의 IDLE에서는 이 문제가 해결되었다.

편집기에서 소스 코드 인코딩을 명시한 경우

만약 소스 코드의 인코딩이 명시되어 있다면 직접적으로 선언한 문자열의 인코딩은 idle 에서 제대로 표시한다. 파일의 인코딩을 UTF8로 지정해서 저장했다면, 한글로 ‘가’라고 파일에 적혀있는 문자열은 결국 xeaxb0x80로 저장될 것이며, 콘솔에서 실행했던 것과 마찬가지로 cp949로 인코딩을 변경하거나 유니코드로 디코딩하지 않으면 제대로 출력이 되지 않고 깨진 글자가 출력될 것이다.

#-*-coding:utf-8

a = '가'
print repr(a)  # xeaxb0x80
print a        # 깨질것이다.
print

b = u'나'      
print repr(b)   # \ub098
print b         # 잘 출력될 것이다.
print

c = b.encode('cp949')
print repr(c)   # xb3xaa
print c         # 잘 출력될 것이다.
print

위와 같은 코드를 실제로 utf-8 인코딩으로 저장한다. 이를 콘솔창에서 실행한 결과는 다음과 같다.

D:temp>python enc_test.py
'xeaxb0x80'
媛€

u'ub098'
나

'xb3xaa'
나

문제는 idle인데, idle에서 위 소스를 실행한 결과는 당췌 설명할 방법이 없다. UTF8 인코딩으로 넘어간 값이 제대로 출력된다 (왜?)

Python 2.7.6 (default, Nov 10 2013, 19:24:18) [MSC v.1500 32 bit (Intel)] on win32
Type "copyright", "credits" or "license()" for more information.
>>> ================================ RESTART ================================
>>> 
'xeaxb0x80'
가

u'ub098'
나

'xb3xaa'
나

>>> 

이 문제는 비단 소스코드 인코딩만의 문제가 아니다. 데이터베이스나 파일에 저장되어 있는 텍스트를 읽어왔거나, 네트워크를 통해 받아온 텍스트(디코딩하기전에는 바이트스트림이겠지)를 처리할 때에도 마찬가지이다. idle에서 테스트해보면 한글이 와장창 깨지고 해결책이 없는 경우가 너무 많다.

파이썬 3에서의 한글 처리

파이썬 3에서 내부 문자열은 모두 유니코드 문자열로 처리된다. 파이썬 2에서는 유니코드 문자열은 unicode 타입이었고, str 타입은 실질적으로 바이트 배열과 동일한 타입이었다.

뭐가 바뀌었는지 기술적으로 이해가 힘들 수 있으니 간단하게 설명하자면 다음과 같이 생각하면 된다.

  1. 문자열은 인코딩이 안된 순수한 상태의 유니코드 문자열로 파이썬 프로그램 내부에 존재하게 된다.
  2. 모든 입출력에 대해서는 필수적으로 인코딩이 필요하다. 인코딩하여 외부로 내보내고, 외부에 있는 데이터는 디코딩하여 문자열로 변환되어 들어온다.

예를 들어 텍스트 파일을 읽어들이는 경우에, 파이썬 2에서는 텍스트 파일을 그냥 읽어서 그 내용을 미리 알고 있는 인코딩 종류를 사용하여 디코드해서 사용해야 했다. 이는 바이너리 파일을 읽는 것과 1도 다를바가 없다.

## python 2.7에서 한글 파일 읽는 법 (한글 파일은 UTF-8로 인코딩되었다고 가정)
f = open('myfile.txt', 'r')
content = f.read() # content는 인코딩된 그대로의 바이트 스트림
text = content.decode('utf8') # 바이트배열을 디코딩하여 문자열데이터로 변환
print(text)

하지만 파이썬3에서는 기본적으로 인코딩을 명시해야 한다. (생략하는 경우 기본적으로 utf8이다)

## 파이썬 3에서 한글 텍스트 파일 읽기
f = open('myfile.txt', 'r', encoding='utf8')
text = f.read() # 읽어들이면서 지정된 인코딩을 이용하여 문자열 데이터로 변환한다.
print(text)

파이썬3에서의 한글 등의 문자 처리는 덕분에 매우 쉬워졌다. 다음의 규칙만 지키면 된다.

  1. 소스 코드 작성시에는 인코딩을 명시할 것 (생략하려면 utf8로 저장할 것)
  2. 외부로부터 문자열을 받아들이거나 읽어야 하는 경우에는 보통 미리 인코딩을 제시하고, 항상 ‘문자열을 읽는다’라고 생각한다.
  3. 반대로 문자열을 외부로 쏴주거나 파일에 쓸 때에도 핸들러를 생성하는 시점에 인코딩을 결정하고, 그대로 쓰면 된다.

파이썬 2.7에서는 한글이 섞인 데이터를 다루기 위해서는 디코딩했다가 처리하고 다시 인코딩해서 전송/기록하고 하는 등의 삽질을 겪어야 했고, 어디 한 군데에서 까딱 빼먹거나 실수하면 그대로 내용이 깨지는 참사로 이어졌던 것에 (그리고 IDLE에서의 버그로 IDLE에서는 되는게 실제 처리시에는 안되고…) 비하면 엄청나게 인터페이스가 깔끔해졌다. 게다가 문자열을 정말 ‘문자열’로 생각해도 된다. 문제가 생길만한 곳이 전혀 존재하지 않는 것이다.

결론 – 윈도 환경에서 파이썬으로 한글을 다룰 때

윈도의 콘솔 입출력 인코딩은  UTF-8이 아니고, 웹이나 왠만한 파일의 인코딩은 UTF-8이며, 파이썬 2.7은 내부적으로 유니코드를 사용하지 않는다. 두 가지도 아닌 이 세가지의 인코딩 미스매치는 안그래도 인코딩이 어쩌고 하는 개념이 이해가 될리 없는 초보자에게 너무나 크나큰 고통이다. 따라서 다음의 지침을 따를 것을 권장한다. 아니 꼭 따라라.

  1. 무조건 Python 3 를 쓸 것
  2. IDLE을 쓰고싶다면 가능하면 Jupyter Notebook을 쓸 것
  3. 프로그램 외부로 문자열 데이터가 드나들 때에는 반드시 인코딩이 명시되어야 한다.

파이썬 3가 나온지가 몇년짼데…. 아직도 파이썬2 써도 괜찮다는 조언해주는 전문가가 근처에 있다면, 사기꾼이다. 철저히 무시해라.