Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Ruby에서 인코딩 오류를 해결하는 3단계 가이드

문자열의 인코딩은 문제가 생겼을 때에야 비로소 신경 쓰게 됩니다. 예외 트래커를 열었는데 아래와 같은 에러가 눈앞에 나타난 경험, 한 번쯤 있으시죠?

Encoding::InvalidByteSequenceError: "\xFE" on UTF-8

아니면 분명히 "they're"였던 문자열이 어느 순간 "they’re"처럼 깨져 보이기도 합니다.

그렇다면 인코딩이 잘못됐을 때, 대체 무엇이 어떻게 깨진 걸까요? 그리고 어떻게 고칠 수 있을까요?

인코딩이란 무엇인가?

인코딩이 문자열에 어떤 역할을 하는지 상상할 수만 있다면, 이런 종류의 버그는 훨씬 쉽게 고칠 수 있습니다.

문자열을 바이트(byte), 즉 작은 숫자들의 배열이라고 생각해 보세요:

irb(main):001:0> "hello!".bytes
=> [104, 101, 108, 108, 111, 33]

이 인코딩에서는 104h를, 33!를 의미합니다.

하지만 영어에서 흔히 쓰이지 않는 문자가 등장하면 이야기가 조금 복잡해집니다:

irb(main):002:0> "hellṏ!".bytes
=> [104, 101, 108, 108, 225, 185, 143, 33]

이제 어떤 숫자가 어떤 문자를 나타내는지 파악하기 어려워졌습니다. 는 하나의 바이트가 아니라 [225, 185, 143]라는 바이트 그룹으로 표현되거든요. 그래도 바이트와 문자 사이에는 여전히 관계가 존재합니다. 바로 문자열의 인코딩이 그 관계를 정의하는 것입니다.

동일한 바이트 집합을 서로 다른 인코딩으로 해석하면 어떻게 달라지는지 직접 확인해 보겠습니다:

# 특수 문자가 포함된 ISO-8859-1 문자열을 만들어 봅시다!
irb(main):003:0> str = "hellÔ!".encode("ISO-8859-1"); str.encode("UTF-8")
=> "hellÔ!"

irb(main):004:0> str.bytes
=> [104, 101, 108, 108, 212, 33]

# 같은 문자열을 ISO-8859-5로 해석하면 어떻게 될까요?
irb(main):005:0> str.force_encoding("ISO-8859-5"); str.encode("UTF-8")
=> "hellд!"

irb(main):006:0> str.bytes
=> [104, 101, 108, 108, 212, 33]

바이트는 전혀 변하지 않았습니다. 하지만 결과물은 완전히 다르게 보입니다. 인코딩을 바꾸자 바이트는 그대로인데 문자열의 출력만 달라진 것입니다.

또한 모든 문자열이 모든 인코딩으로 표현될 수 있는 것은 아닙니다:

irb(main):006:0> "hi∑".encode("Windows-1252")
Encoding::UndefinedConversionError: U+2211 to WINDOWS-1252 in conversion from UTF-8 to WINDOWS-1252
	from (irb):61:in `encode'
	from (irb):61
	from /usr/local/bin/irb:11:in `<main>'

대부분의 인코딩은 크기가 작아 모든 가능한 문자를 담을 수 없습니다. 한 인코딩의 문자가 다른 인코딩에 존재하지 않거나, Ruby가 두 인코딩 간의 문자 변환 방법을 찾지 못하면 위와 같은 에러가 발생합니다.

다행히 encode 메서드에 추가 옵션을 넘기면 이 에러를 피할 수 있습니다:

irb(main):064:0> "hi∑".encode("Windows-1252", invalid: :replace, undef: :replace)
=> "hi?"

invalidundef 옵션은 변환할 수 없는 문자를 다른 문자로 대체합니다. 기본 대체 문자는 ?이며, 유니코드로 변환할 때는 가 사용됩니다.

하지만 안타깝게도 encode로 문자를 대체하면 정보가 손실될 수 있습니다. 어떤 바이트가 ?로 대체되었는지 알 방법이 없습니다. 그래도 데이터가 반드시 새 인코딩을 사용해야 한다면, 깨진 상태로 두는 것보다 일부 손실을 감수하는 편이 나을 수 있습니다.

지금까지 인코딩을 이해하는 데 도움이 되는 세 가지 핵심 문자열 메서드를 살펴보았습니다:

  • encode: 문자열을 다른 인코딩으로 변환합니다 (각 문자를 새 인코딩의 대응 문자로 바꿉니다)

  • bytes: 문자열을 구성하는 바이트를 보여줍니다

  • force_encoding: 해당 바이트들이 다른 인코딩으로 해석되면 어떻게 보이는지 보여줍니다

encodeforce_encoding의 가장 큰 차이점은, encodebytes를 변경할 수 있지만 force_encoding은 절대 변경하지 않는다는 점입니다.

인코딩 버그를 수정하는 3단계 프로세스

대부분의 인코딩 문제는 다음 세 단계로 해결할 수 있습니다.

1단계: 문자열의 실제 인코딩이 무엇인지 파악하기

쉬워 보이지만, 문자열이 특정 인코딩이라고 주장한다고 해서 실제로 그런 것은 아닙니다:

irb(main):078:0> "hi\x99!".encoding
=> #<Encoding:UTF-8>

뭔가 이상하죠? 만약 진짜 UTF-8이라면 저렇게 이상한 백슬래시 숫자가 섞여 있을 리 없습니다. 그럼 문자열의 올바른 인코딩을 어떻게 찾을 수 있을까요?

상당수의 오래된 소프트웨어는 하나의 기본 인코딩만 고집합니다. 따라서 입력 데이터의 출처를 조사해 보세요. 누군가 Word 문서에서 붙여넣은 것이라면 Windows-1252일 가능성이 높습니다. 파일에서 가져왔거나 오래된 웹사이트에서 추출했다면 ISO-8859-1일 수 있습니다.

또한 Wikipedia 등에 공개된 인코딩 표를 검색해 활용하는 것도 큰 도움이 됩니다. 이런 표에서 알 수 없는 숫자에 해당하는 문자를 찾아보고, 맥락상 말이 되는지 확인하면 됩니다.

위 예제에서 Windows-1252 표를 보면 바이트 99는 "™" 문자를 나타냅니다. 바이트 99는 ISO-8859-1에는 존재하지 않죠. ™가 문맥상 자연스럽다면 입력이 Windows-1252였다고 가정하고 넘어갈 수 있습니다. 그렇지 않다면 더 그럴듯한 문자를 찾을 때까지 계속 조사하면 됩니다.

2단계: 문자열을 어떤 인코딩으로 만들지 결정하기

이 단계는 쉽습니다. 특별한 이유가 없다면 문자열은 UTF-8로 인코딩하는 것이 좋습니다.

Ruby에서 흔히 쓰이는 또 다른 인코딩으로 ASCII-8BIT가 있습니다. ASCII-8BIT에서는 모든 문자가 정확히 하나의 바이트로 표현됩니다. 즉, str.chars.length == str.bytes.length가 성립합니다. 따라서 문자열 내부의 개별 바이트를 세밀하게 제어하고 싶다면 ASCII-8BIT도 좋은 선택지가 됩니다.

3단계: 1단계의 인코딩에서 2단계의 인코딩으로 재변환하기

encode 메서드를 사용하면 됩니다. 이번 예제에서 문자열은 원래 Windows-1252 인코딩이었고, 우리는 이것을 UTF-8로 만들고 싶습니다. 아주 간단합니다:

irb(main):088:0> "hi\x99!".encode("UTF-8", "Windows-1252")
=> "hi™!"

훨씬 낫네요. (개인적으로는 이 호출에서 인코딩 순서가 늘 거꾸로 느껴지긴 하지만 말입니다.)

마치며: 직접 만져보며 익히세요

동일한 바이트 배열이 서로 다르게 해석될 수 있다는 것을 머리로 받아들이기란 꽤 어렵습니다. 특히 그중 하나가 깨진 상태일 때는 더욱 그렇죠. 하지만 인코딩에 익숙해지는 최고의 방법이 있습니다. 바로 직접 만져보면서 놀아보는 것입니다.

irb 콘솔을 열고 encode, bytes, force_encoding을 마음껏 실험해 보세요. encode가 문자열을 구성하는 바이트를 어떻게 바꾸는지 지켜보고, 다양한 인코딩이 어떤 모습인지 감각을 길러 보세요. 인코딩에 익숙해진 뒤 이 세 단계를 적용하면, 예전에는 몇 시간 걸렸을 문제를 몇 분 만에 해결할 수 있게 됩니다.

마지막으로, 이런 지식을 습관처럼 익히는 방법을 배우고 싶다면 제 책의 무료 샘플 챕터를 받아보세요. 콘솔에서 직접 코드를 깨뜨려 보는 것은 이런 개념을 공부하기에 정말 재미있는 방법입니다.