컴퓨터 일반

텍스트 인코딩 더 이상 헷갈리지 말자 (euc-kr, utf-8, ucs-2, 유니코드?)

Folivora 2020. 1. 10. 10:24

너무 길어져서 아예 글을 따로 쓰게 되었습니다.

 

사실 인코딩 문제는 근본적으로 우리가 "글자"를 어떻게 컴퓨터가 이해하는 "숫자"로 나타내는가에 관한 것입니다.

 

기본적으로 "안녕하세요!!"라는 문자열은 "안" + "녕" + "하" + "세" + "요" + "!" + "!" 각 글자들을 일렬로 배치한 것이고요. 인코딩(encoding)이란 각 글자들을 어떤 방식으로 컴퓨터가 이해하는 바이트로 대응(mapping)시켰는지 규칙을 의미합니다. 만약 규칙을 잘못 적용하면 엉뚱한 글자들을 보게 되겠죠?

 

과거 영어권 나라에서는 7 bit (0 ~ 127)만 가지고도 알파벳을 "대강" 나타낼 수 있었습니다. ASCII 코드표를 보시면 a~z, A~Z, 0~9 등등을 모두 넣어도 충분했습니다. 나머지 128 ~ 255 부분은 특수문자나 유럽권 국가들에서 기존 알파벳에 뭔가 더 붙은 à, á, â, ä, æ, ã, å, ā 와 같은 문자를 나타내는 데 사용하였습니다. 한글이나 한자들의 글자들은 조합의 수가 많아서 2바이트를 사용하였고, 이러한 문자들을 multibyte character라고 불렀습니다. 대개 확장 영역 128~255 부분이 사용되면 바로 다음 따라오는 바이트도 같이 사용하고 (2바이트), 확장 영역이 아닌 기본 영역 (0~127)이 사용되면 ASCII 코드를 적용하는 식입니다. 과거 한글 윈도우에서는 EUC-KR이라는 인코딩 방식을 주로 사용하였습니다.

 

2바이트로 나타낼 수 있는 글자 수는 최대 256 * 256 = 65,536 가지니까 문제가 없는 줄 알았는데요. (참고로 현대 한글의 글자 수 조합은 11,172가지입니다.) 그런데 다른 multibyte 인코딩 방식들과 겹치는 영역이 있으므로 동시에 여러 다국어를 표현할 수 없습니다. 그래서 아예 세계 표준을 제정해서 모든 글자들을 하나의 숫자(코드 포인트)로 "일대일 대응"시키자고 약속한 것이 유니코드입니다. 다시 코드 포인트 숫자를 나타내는 방식들이 여러 가지가 있는데, UTF-8, UTF-16 (LE/BE), UTF-32 (LE/BE), UCS-2 등이 있습니다. 저는 처음에 유니코드를 접했을 때, 다시 왜 이렇게 불편하게 규칙들을 정했는지 불만이었습니다. 그러나 컴퓨터 입장에서 0x12, 0x34, 0x56의 바이트 순열을 보았을 때 0x123456으로 해석할지 0x563412로 해석해야 할지 결정하는 문제는 유니코드 협회에서 할 수 있는 일이 아니죠. 하아... 세상은 왜 이리 복잡할까요.

 

여하튼 유니코드에서는 "안"이라는 글자를 50504라는 숫자와 대응을 시켜놓았고, 이를 바이트로 나타낼 때 주로 UTF-8을 사용합니다.

글자 숫자 바이트
U+c548  = 50504 ec 95 88
  • 0xc548 = 0xc5 * 256 + 0x48 = 50504 (10진수)
  • 0xec9588 을 2진수로 변환하면  11101100 10010101 10001000 가 되고요. 처음 밑줄 친 1110은 UTF-8의 규칙에 따르면 3바이트를 사용할 것임을 알려줍니다. 뒤에 따라오는 10들은 종속된 2 바이트를 의미하고요. 하이라이트로 표시한 1100 010101 001000을 합쳐놓으면 1100010101001000 = 50504가 됩니다. UTF-8은 유니코드를 바이트로 나타내는 여러 방식 중 하나입니다.

UTF-8의 장점은 가변적인 길이의 바이트를 사용하면서 동시에 0~127 영역에서는 ASCII 코드와 완전히 동일하기 때문에 하위 호환성이 있습니다. 따라서 UTF-8 인코딩을 지원하는 프로그램은 ASCII 코드로 작성된 문서를 변환 과정 없이 읽을 수 있습니다. 매우 영리한 발상이지요! 그렇지만 하위 호환성에 대한 비용이 없는 것은 아닌데요. 한글 글자 하나 당 2바이트가 아닌 3바이트가 사용되게 됩니다. 그러나 한글 사용자 입장에서는 큰 문제가 아닌 것이 애초에 텍스트 파일 크기가 그렇게 부담되지 않기 때문입니다. 오히려 다국어 지원이 제대로 안 되는 기존 인코딩 방식이 더 골치 아프죠.

 

UTF-8 방식으로 나타낼 수 있는 글자 수는 최대 1,112,064 가지이고, 현재 유니코드 12.1.0 에 따르면 137,929 가지의 글자가 대응되어있습니다. UCS-2는 가변적인 길이의 바이트를 사용하는 것이 아니라 항상 2바이트만 사용하는데 65,536 가지밖에 표현이 안됩니다. UCS-2 방식을 사용하면 현재 137,929 가지의 글자 수가 정의된 유니코드를 완전히 표현할 수 없겠죠? UTF-16 등은 더 많은 글자 수를 표현할 수 있지만 굳이 하위 호환성을 포기하면서 2바이트로 넘어갈 이유가 현재로선 없습니다. 또한 바이트 순서가 Little Endian이냐 Big Endian인지 여부도 중요해지죠. 아직 UTF-8에서도 사용되지 않은 영역이 많기 때문에 현재로선 UTF-8을 사용하는 것이 최선이라 할 수 있겠습니다.

 

인코딩 방식을 잘못 적용했을 때 일어나는 일

EUC-KR 인코딩 방식으로 "안녕하세요!!"를 나타내면 다음과 같습니다.

한글 문장 EUC-KR
안녕하세요!! bec8 b3e7 c7cf bcbc bfe4 21 21

be c8은 "안"을 나타내고 b3 e7은 "녕", c7 cf는 "하", bc bc는 "세", bf e4는 "요"를 나타냅니다. 21은 1바이트로 "!"에 대응이 되고요.

UTF-8 인코딩 방식으로 "안녕하세요!!"를 나타내면 다음과 같습니다.

한글 문장 UTF-8
안녕하세요!! ec9588 eb8595 ed9598 ec84b8 ec9a94 21 21

컴퓨터 입장에서는 "안녕하세요!!"가 아닌 바이트 순열로 이해하므로 인코딩 방식(규칙)을 지정해줘야 합니다.

바이트 적용한 인코딩 방식 보게되는 글자
bec8b3e7c7cfbcbcbfe42121 latin1 ¾È³çÇϼ¼¿ä!!
bec8b3e7c7cfbcbcbfe42121 euc-kr 안녕하세요!!
bec8b3e7c7cfbcbcbfe42121 ibm437 ╛╚│τ╟╧╝╝┐Σ!!
bec8b3e7c7cfbcbcbfe42121 utf-8
ec9588eb8595ed9598ec84b8ec9a942121 latin1 안녕하세요!!
ec9588eb8595ed9598ec84b8ec9a942121 euc-kr <에러> 혹은 "몄!!"
ec9588eb8595ed9598ec84b8ec9a942 ibm437 안녕하세요!!
ec9588eb8595ed9598ec84b8ec9a942121 utf-8 안녕하세요!!
  • �: 해석할 수 없는 글자를 의미함. 
  • � 기호는 EUC-KR에서 정의되지 않았으므로 EUC-KR이 해석할 수 없는 글자는 무시됩니다.

우리가 가끔 보는 "¾È³çÇϼ¼¿ä!!" 같은 모양은 euc-kr로 나타내진 바이트 순열에 latin1 인코딩을 적용했기 때문이죠. 보통 인코딩 방식이 지정되지 않으면 여러 가지 방법으로 추측을 해야 하는데, 정말 모르겠을 때는 latin1이라고 가정하고 해석하게 됩니다. 그런데 만약 모든 텍스트 파일에 대해서 어떤 인코딩 방식을 사용했는지 매번 추측해야 한다면 너무 불편하겠죠? 그래서 해결책은 유니코드를 사용하고, 하위 호환성이 있는 UTF-8을 사용하자가 됩니다. :)