HOME INFO PROJECT BLOG ESSAY
Article Projects Lab
한국어 English
article |

메서드를 실행을 결정하는 과정. 디스패치에 대하여

개요

리팩터링 스터디 중 궁금한 점이 생겼는데요, 의문이 생긴 맥락으로 글을 시작합니다.

‘코드에서 나는 악취’ 챕터의 기능 편애(Feature Envy)라는 목차에서 다음과 같이 설명합니다:

프로그램을 모듈화할 때는 코드를 여러 영역으로 나눈 뒤 영역 안에서 이뤄지는 상호작용은 최대한 늘리고, 영역 사이에서 이뤄지는 상호작용은 최소로 줄이는 데 주력한다. ‘기능 편애’는 흔히 어떤 함수가 자기가 속한 모듈의 함수나 데이터보다 다른 모듈의 함수나 데이터와 상호작용 할 일이 더 많을 때 풍기는 냄새다

책에서 말하는 이 기능 편애를 해결하기 위해서 나온 방법으로, 함수가 데이터와 가까워하는 의중이 있다면 함수 옮기기를, 함수 일부에서만 기능을 편애할 경우 그 부분만 독립 함수로 빼내는 함수 추출, 그 함수를 원하는 모듈로 옮기는 함수 옮기기라는 기법을 제시하는데, 여기서 위 문단에서 말한 방법(함수 옮기기, 함수 추출..)들을 거스르는 패턴도 언급하며 전략 패턴(Strategy pattern)방문자 패턴(Visitor pattern) 을 소개합니다.

방문자 패턴이 언급되면서 스터디에서 ‘더블 디스패치’가 필요할 경우에 다형성이 양방향으로 적용이 되어야 되는 케이스가 있고, 이를 방문자 패턴같은 구조를 이용하면 멀티플 디스패치를 언어에서 지원되지 않더라도 구현할 수 있다는 말이 오갔는데요, 여기서 궁금한 점이 생겨 의문을 해결하는 과정을 글로 작성해봤습니다.

궁금증이 생긴 포인트이자 글에서 다룰 내용은 다음과 같습니다.

  • 정적 디스패치와 동적 디스패치
  • 싱글 디스패치와 가상 메서드
  • 멀티플 디스패치
  • Java에서 멀티플 디스패치를 구현하는 방법

이어서 글을 읽기 위한 사전 지식은 다음과 같습니다.

  • 언어의 다형성에 대한 이해가 있으신 분.
  • 오버로딩과 오버라이딩의 차이를 알고있으신 분.

디스패치(Dispatch)는 무엇인가?

디스패치의 사전적 의미는 ‘보내다’, ‘파견하다’인데요, ‘컴퓨터 과학’에서 디스패치는 메서드 호출 시 실행할 메서드를 결정하는 과정을 의미합니다.

디스패치는 정적 디스패치와 동적 디스패치로 나뉘는데 코드와 함께 알아보겠습니다.

정적 디스패치(Static Dispatch)

정적 디스패치는 컴파일 타임에 메서드 호출이 결정되는 메커니즘을 말합니다.

Service 객체에 이름이 같은 두 메서드가 있지만(메서드 오버로딩), 어떤 메서드를 호출할지 컴파일러가 메서드의 시그니처(매개변수 타입, 개수)를 기준으로 결정합니다.

컴파일 시점에 컴파일러도 알고 있으며, 컴파일된 바이트 코드에도 그 정보들이 그대로 남아 있어 실제 실행되는 런타임 시점이 되지 않아도 어느 메서드 호출이 일어날 것인가를 결정하게 되죠.

동적 디스패치(Dynamic Dispatch)

반면 동적 디스패치는 컴파일러가 컴파일 타임에는 호출할 메서드를 알 수 없지만, 프로그램이 실행되는 런타임 시점에 실제 객체의 타입에 따라 호출할 메서드가 결정되는 것을 말합니다.

코드를 보면 svc라는 변수의 타입은 그냥 Service이며 이 추상 클래스의 메서드를 그냥 호출하는 것처럼 되어 있지만, 컴파일 시점에서는 무엇을 선택할지 결정하지 못하고 있습니다.

하지만 실제 프로그램을 실행을 해보면(런타임) ‘MyService1’이라는 구체적인 클래스를 정의했기 때문에 클래스 안에 있는 메서드가 실행됩니다.

여기서 MyService1과 MyService2를 리시버 패러미터(receiver parameter)라 합니다.

싱글 디스패치(Multiple Dispatch)와 가상 메서드

싱글 디스패치

싱글 디스패치는 메서드 실행 시 수신 객체(호출된 객체)의 실제 타입만을 기준으로 호출할 메서드를 결정하는 방법입니다. 여기서 메서드의 인자 타입은 고려되지 않습니다.

Java는 기본적으로 싱글 디스패치만을 지원하는 언어입니다. 이는 Java의 메서드 호출 메커니즘과 관련있습니다.

일반 객체 포인터(Ordinary Object Pointer, OOP)

Java의 모든 객체는 OOP라는 객체로 표현되는데요, 객체의 클래스 정보, 해시코드, GC 관련 메타 데이터를 담고 있습니다.

이 OOP를 생성할 때 JVM이 확보한 메모리 영역에서 생성하여 새 메모리 할당을 위한 시스템 콜을 호출할 필요가 없습니다. (오버헤드가 줄겠죠?)

OOP는 instance OOPKlass OOP가 있습니다. 깊게 파고들어나면 주제에 벗어나기 때문에 내용 이해에 필요한 부분만 설명하겠습니다.

Instance OOP

Instance OOP는 Mark WordKlass Word를 포함합니다.

Mark Word는 인스턴스의 메타 데이터를 가리키는 포인터로 해시코드를 포함하며, Klass Word는 클래스의 메타데이터를 가리키는 포인터로, 클래스의 메타 정보에 대한 참조를 저장해둔 데이터 단위로 C++로 작성된 포인터입니다.

Klass OOP와 가상 메서드

가상 메서드는 상속하는 클래스 내에서 같은 메서드 시그니처(Signature)의 메서드로 오버라이딩(Override)될 수 있는 메서드입니다. (가상 함수와 메서드를 혼용되어 사용하지만, Java를 기준으로 설명하기 때문에 가상 메서드로 통일합니다.)

여기서 Java의 메서드 시그니처는 메서드 명과 패러미터 타입을 말합니다. 메서드 시그니처 만으로 메서드를 구분지을 수 있는 근거가 되는 것이죠.

klass word는 내부적으로 메서드에 대한 참조 값을 저장해둔 배열인 가상 메서드 테이블(Virtual Method Table, vtable) 을 가집니다.

가상 메서드 테이블에는 위 그림처럼 객체가 실제로 바라보는 메서드에 대한 참조 정보가 들어있습니다.

Java는 메서드 호출 시 점연산자(오프셋 연산자)를 사용하는데요, JVM은 메모리에 로드된 메서드에 대한 참조 정보를 가상 메서드 테이블의 특정 오프셋(혹은 인덱스)에 저장합니다.

예를 들어 4번 오프셋(배열의 인덱스)에 toString() 이라는 메서드에 대한 참조 정보를 저장하는 것이죠.

또한 메서드는 오버라이드될 수 있는데요, 앞서 언급한 toString() 메서드로 예시를 들어보겠습니다.

Java의 모든 클래스는 Object 클래스를 상속 받습니다.

여기서 슈퍼 클래스(부모 클래스)의 toString() 메서드를 ‘A 참조’라고 부르고 서브 클래스(자식 클래스)의 toString() 메서드를 ‘B 참조’라고 부르겠습니다.

Object 인스턴스가 4번 인덱스에 ‘A 참조’를 저장했다고 가정하면 부모 클래스의 인스턴스와, 자식 클래스의 인스턴스 모두 4번 인덱스에 참조(A,B)를 저장합니다. 이렇게 같은 인덱스에 오버라이드 된 메서드 정보를 저장하기 때문에 Java의 “상속” 구조를 구현할 수 있는 것 입니다.

이렇게 인스턴스가 바라보는 하나의 정보만 저장하기 때문에 서브 클래스의 인스턴스 입장에서는 ‘A 참조’와 ‘B 참조’ 중 어떤 것을 실제로 호출할지 고민할 필요가 없습니다.

이렇게 하나의 배열공간에 같은 오프셋에 저장되는 메커니즘 때문에 오버라이드 된 메서드는 단일 상속밖에 지원하지 않습니다. 참조인 인덱스를 가상 메서드 테이블에 덮어쓰는 방법으로 상속을 구현했기 때문이죠.

여기서 생기는 궁금증

가상 함수를 사용함으로써 얻는 메리트는 무엇일까?

Java의 모든 인스턴스가 메서드 세부사항을 저장하고 있는 것은 매우 메모리 효율상 비효율적일 것입니다.

각 객체가 메서드 구현을 저장하는 대신, 클래스당 하나의 가상 메서드 테이블만 유지하여 인스턴스가 호출할 수 있는 메서드는 모두 동일하게 동작하기 때문에 공통된 장소에 메서드를 올려둔다면(공유) 메모리를 절약할 수 있겠네요.

(JVM의 JIT 컴파일러도 가상함수 테이블을 활용해 메소드 인라이닝, 탈가상화(devirtualization) 같은 최적화를 수행한다고 합니다.)

멀티플 디스패치, 메서드(Multiple Dispatch, Multi method)

멀티플 디스패치는 패러미터가 몇 개든 상관 없이 패러미터의 동적 타입에 따라 가상 함수처럼 동작하여 메서드 호출을 결정하는 메커니즘을 말합니다.

설명은 멀티플 디스패치를 지원하는 Julia라는 언어로 특징을 알아보겠습니다.

Julia도 Java와 마찬가지로 내부적으로 “메서드 테이블”을 사용해 각 함수에 대해 정의된 모든 메서드의 목록을 저장하는 데요

  • 함수 이름으로 메서드 테이블을 찾고
  • 인자의 타입을 확인
  • 타입에 가장 잘 맞는 메서드를 선택
  • 선택된 메서드 실행

위 과정을 거쳐 메서드 호출을 결정하게 됩니다.

Integer 타입의 인자를 받는 함수 process는 Int64나 Int32 타입의 인자도 처리할 수 있습니다.

결과적으로 두 함수 모두 “Processing an integer”가 출력됩니다.

또한 필요한 경우 암시적 형변환을 수행합니다. 여기서 정수 5는 자동으로 Float64로 변환되어 연산이 이루어집니다.

코드를 보면 기존에 정의된 distance 함수를 수정하지 않고도 새로운 타입 조합(Point와 Circle)에 대한 기능을 확장했습니다.

또한 다른 객체지향 언어에서는 보통 상속이나 인터페이스를 통해 다형성을 구현해야 하지만 Julia는 단순히 같은 함수 이름에 다른 타입 시그니처의 메소드를 추가하는 것만으로 다형성을 구현할 수 있습니다.

이처럼 함수를 호출할 때 함수의 이름과 첫 번째 인자(보통 객체)의 타입만을 고려하는 ‘싱글 디스패치’와 달리, ‘멀티플 디스패치’ 방식은 모든 인자의 타입을 고려해서 적합한 함수를 선택하는 특징때문에 엄청난 유연성을 가집니다.

Java에서 멀티플 디스패치를 구현하는 방법

Java는 싱글 디스패치만을 지원하는 언어이기 때문에 ‘멀티플 디스패치’처럼 작동하게 만드려면 디스패치를 2번하게 만드는 더블 디스패치 방식으로 구현해야 합니다.

상황과 예시를 들어보겠습니다.

상황

SNS 플랫폼에 알맞은 포스팅을 만들어주는 서비스를 개발하며 다음과 같은 비즈니스 로직을 가정하고 시작합니다.

  • SNS라는 도메인과 Post라는 서비스가 있으며
  • SNS의 구현체로는 현재로써는 ‘페이스북’과 ‘트위터’가 있고
  • Post는 SNS 객체를 받아서 포스트를 만들어낸다.
  • Post의 구현체로 Text와 Picture가 있다.

코드로 나타내면 다음과 같습니다.

이제 비즈니스 로직에서 SNS 플랫폼 별로 포스팅을 해보겠습니다.

코드에서는 getClass() 메서드로 각 SNS의 클래스를 출력하는 동일한 로직이 사용되었습니다.

여기서 SNS 종류 마다 다른 기능을 추가한다고 했을 때 어떻게 변경해야 할까요?

if 분기문과 객체 타입을 확인하는 연산자인 instanceof로 타입을 분류하여 타입에 맞는 기능을 실행하도록 변경했습니다. 코드 역시 예상대로 작동합니다.

이 상황에서 다른 SNS 플랫폼인 Instagram을 추가해달라는 요구가 들어온다면 어떻게 해야될까요? SNS 인터페이스의 구현체로 Instagram을 추가하면 됩니다.

하지만 코드를 실행하면 컴파일 에러가 발생하게 됩니다. 왜냐면 Picture에는 Instagram의 기능을 추가하는 것을 잊어버렸습니다..

수동으로 Instagram의 기능을 추가하면 코드는 실행되겠지만, SNS 플랫폼이 100개가 넘어간다고 하면 일일이 그걸 다 수정할 수 있을까요?

그래서 if 분기문을 제거하고 기능을 분리하도록 해보겠습니다.

if 문과 instanceof 를 제거하고 메서드 오버로딩을 이용해 기능을 분리한 코드입니다.

하지만 여전히 새로운 SNS가 추가되는 경우에도 Post와 Text, Picture를 모두 수정해야하며 main()는 여전히 컴파일 에러를 뱉습니다.

Post의 postOn() 메서드에 Facebook과 Twitter 객체를 패러미터로 받는 메서드를 정의하고 객체도 넘겼는데, 왜 컴파일 에러가 발생할까요?

그 이유는 메서드 오버로딩은 정적 디스패치를 하기 때문입니다.

오버로딩된 메서드는 컴파일 시점에서 타입 체크를 하고 어떤 메서드를 실행할지 알아야 하는데, main() 메서드의 forEach문의 매개변수로 Facebook이나 Twitter와 같은 구현체의 타입이 아니라 SNS 객체를 넘겨주고 있어, 어떤 메서드를 실행할지 결정할 수 없는 상황입니다.

이를 해결하기 위해 더블 디스패치를 적용해보겠습니다.

더블 디스패치 기법 적용하기

위에서 살펴본 문제점은 ‘오버로딩’을 사용한 정적 디스패치 방식을 사용했기 때문에 SNS 객체를 넘길 수 없다는 것이었죠.

그래서 SNS 타입을 받아 실행할 수 있도록 동적 디스패치를 사용하도록 변경해야 합니다.

먼저 postOn() 메서드를 SNS 타입을 받도록 변경하고, Post의 구현체인 Facebook과 Twitter에 postOn을 오버라이딩 변경했습니다.

이어서 SNS 인터페이스에 post 메서드를 위치시킵니다.

그리고 구현체인 Facebook, Twitter, Instagram에 text와 picture를 패러미터로 받았을 때 해야하는 기능을 추가합니다.

그리고 Text와 Picture에는 패러미터로 전달받은 SNS 객체의 post() 메서드를 호출하고 자기 자신(this)을 패러미터로 넘깁니다.

자 이제 main() 메서드를 실행해보겠습니다.

의도대로 동작하며 기존 코드를 수정하지 않은 채, SNS의 새 구현체를 정의할 수 있습니다. 그림으로 살펴보면:

Post 클래스 구현체중 어떤 클래스의 postOn() 메서드를 사용할지 결정(Dynamic Dispatch 1회)하고

postOn에 인자로 선택된 SNS를 구현한 클래스에서 어떤 post() 메서드를 사용할지 결정(Dynamic Dispatch 2회)하여 동적 디스패치를 두 번 하게 됩니다.

여기서 SNS의 구현체인 FaceBook, Twitter, Instagram 클래스가 비즈니스 로직을 직접 구현하기 때문에 추후에 새로운 구현체가 추가되더라도 Post 클래스는 변경이 없습니다.

즉 확장에 대해 열려 있어야 하고, 변경에는 닫혀 있어야 하는 객체 지향의 개방-폐쇄 원칙을 지킬수 있게 되죠!

맺음

Java에 대해 공부를 꽤 했다고 생각했는데 아직도 모르는 부분이 이렇게 많다는 것을 보면 정말 배움의 길은 끝이 없다는 것을 느꼈습니다.

글을 작성하기 전에는 언어에서 다형성을 지원한다는 것과 상속 방식의 이점에 대해 두루뭉술하게 알고 있었다면, 이번 공부를 통해 내부를 들여다보면서 객체 지향 언어 패러다임의 철학과 Java라는 언어의 한계점을 알 수 있게된 의미있는 시간이 되었습니다.

읽어 주셔서 감사하고 틀린 내용이나 피드백이 있다면 언제든 달게 받겠습니다.

출처

article |

문자 인코딩의 역사를 알아보자

개요

Java의 파일 I/O와 Stream에 대해 공부하다가 문득 ‘UTF-8은 왜 표준이 된걸까?’ 라는 의문이 생겼습니다.

그러다가 문자(character) 인코딩의 기원까지 찾아가는 샛길로 빠져버렸는데요, 꽤나 재미있는 내용이어서 한 번 정리해봤습니다.

문자’라는 것은 인류 문명에서 빼놓을 수 없는 문화 중 하나로, 최초의 상형 문자로 알려진 수메르 문명의 ‘쐐기문자’부터 현대까지 이어진 인간의 시각적 관행인데요 아날로그에서 디지털로 넘어오는 순간에도 이 문자의 중요성은 여전했습니다.

이번 글의 메인 스트림은 문자가 디지털 시대를 거치며 생긴 변천사를 알아보는 것입니다.

글의 예상 독자는 다음과 같아요.

  • 컴퓨터의 인코딩 역사와 발전 과정에 대해 궁금하신 분
  • 그래서 UTF-8 방식이 어떻게 인코딩을 하는지 궁금하신 분

시간 순으로 알아보는 인코딩의 발전

인코딩(Encoding)

내용 이해를 위해 간단한 질문으로 시작합니다.

인코딩(Encoding)의 정의가 무엇일까요? 인코딩원본 정보약속된 형태(코드) 로 바꾸는 작업입니다.

일반적인 컴퓨터는 이진수(0과 1) 로 모든 데이터를 저장하고 처리합니다. 그러나 사람이 사용하는 문자(한글, 영어, 숫자, 특수문자 등)는 직접 이진수로 표현되지 않기 때문에 문자를 숫자로 변환하는 규칙(인코딩 방식)이 필요성이 생겼습니다.

모스 코드(부호)

저같은 경우 ‘모스 부호’라는 것을 영화에서 구조 신호로 사용하는 것을 많이 봤었는데요, 이는 미국의 새뮤얼 모스라는 화가 겸 발명가가 고안한 전신 기호(telegraphic code) 입니다.

여기서 전신 기호는 서로 떨어진 곳에서 전류나 전파를 이용하여 정보를 약속된 신호로 주고받는 통신 방법 중의 하나입니다.

화가였던 모스는 1825년에 고향을 떠나 워싱턴에서 미국 독립전쟁의 영웅 ‘라파예트 후작’의 초상화를 그리고 있을 때 말을 타고 급히 달려온 메신저가 ‘아내가 아프다’는 내용의 아버지가 쓴 편지를 전해받았습니다.

모스는 즉시 뉴헤이븐에 있는 집으로 돌아갔지만, 아내는 이미 세상을 떠났고 장례식까지 끝난 뒤였습니다.

아내의 임종을 하지 못한데 자책감을 느낀 모스는 화가의 꿈을 접고 ‘어떻게 하면 소식을 빠르게, 멀리까지 전달할 수 있을까?’ 라는 생각에 골몰한 끝에 1837년에 모스 전신기를 만들었습니다.

모스 부호를 사용한 전신이 소개되기 훨씬 전에 인류는 봉화(beacon)나 깃발을 사용해 장거리에서 메시지를 변환하여 알리곤 했지만, 모스 부호가 가지는 상징성은 영어 알파벳의 각 문자에 대한 표준 인코딩 방법을 갖는 최초의 시스템이라는 것입니다.

이는 현대 문자 인코딩 시스템에서 무시할 수 없는 기반이 되었죠.

펀치 테이프, 카드(천공 테이프)

모스 부호 기반의 전신 기술이 발전하면서, 전신 기계의 메시지를 자동 기록하기 위해 천공 테이프가 도입되었습니다.

당시 컴퓨터는 키보드와 디스플레이가 없고 하는 일은 연산 작업에 가까웠기 때문에, 위 사진과 같이 종이로 만든 천공 테이프(punched tape)를 사용해 입출력 작업을 수행해야 했습니다. (여기서 천공(穿孔) 이라는 것은 구멍이 뚫려있는 상태를 말합니다.)

천공 테이프는 구멍을 뚫거나 뚫지 않음으로 0과 1을 표시할 수 있었으며, 이는 이진법으로 비트를 구현하는 매체라는 의미입니다.

그런데 당시에 데이터를 표현하기 위해 펀치 테이프를 사용하는 방법에 대한 통일된 ‘표준’이 없었기 때문에 불필요한 펀치 테이프가 낭비되는 일이 잦았습니다.

또한 저장 밀도가 낮아서 1mb의 데이터를 저장하려면 약 10km 이상의 천공 테이프가 필요할 정도로 비효율적이었으며, 테이프가 엉키거나 꼬이면 다시 풀어야 하는 번거로움도 있었죠.

그래서 1960년대에 ANSI(American National Standard Institute, 미국 국가표준 협회) 는 데이터 처리를 위한 공통 코드를 개발하는 프로젝트를 주도했고, 이는 ASCII의 탄생으로 이어졌습니다.

ASCII(American Standard Code for Information Interchange, 미국 정보 교환 표준 부호)

사실 ASCII 이전에 프랑스 전신 기술자 에밀 보도가 전신 회선을 통해 문자를 전송할 수 있는 5비트 바이너리 코드를 발명했습니다.

‘보도 코드’로 알려진 이 코드는 전신에 널리 채택되어 수년 동안 사실상 표준이 되었고 대문자와 소문자, 숫자 및 일부 특수 문자를 전송할 수 있었습니다.

그러나 라틴 문자가 아닌 문자를 표현하는 데 제한이 있었고 결국 후술할 ASCII 같은 진보된 표준으로 대체되었습니다.

ASCII 코드는 IBM의 컴퓨터 프로그래머이자 엔지니어인 밥 베머가 1961년에 ASA(American Standards Association) 에 컴퓨터용 표준 문자 인코딩 시스템에 대한 제안을 제출한 것이 시작점입니다.

IBM 704 모델에서 사용되는 6비트 바이너리 코드를 기반으로 하는 베머의 제안에는 대문자와 소문자, 숫자, 특수 문자에 대한 조항이 포함되어 있었는데요, 이는 결국 2년 후에 발표된 ASCII 표준으로 통합됩니다.

확장 ASCII(Extended ASCII)

이렇게 탄생한 ASCII는 인간과 컴퓨터 간 상호작용 문제를 해결하고 꾸준히 개선해나갔지만, 0x00부터 0x7F까지의 총 127개 문자(제어 문자, 특수 문자, 숫자, 알파벳 등)만이 포함되어있다는 한계점이 있었습니다.

이말은 즉슨 영어로만 컴퓨터와 통신할 수 있다는 의미입니다.

이를 해결하기 위해 확장 ASCII(Extended ASCII)를 제정하여 기존의 ASCII로 정의하지 못했던 128번부터 255번까지의 새로운 문자를 정의할 수 있게 되었는데, 새로 추가된 128개의 코드(0x80 ~ 0xFF)로 프랑스어, 독일어 등의 유럽어를 표현할 수 있도록 만들었습니다.

이와 같이 다양한 유럽어를 표현할 수 있는 확장 ASCII는 ISO-8859 유럽 통일 표준안으로 제정되었고, 수년간의 개선 끝에 255개의 문자와 그에 대응하는 이진 형식으로 구성되어 대부분의 유럽 국가 언어도 지원하게 되었습니다.

아시아 문자 집합(Asian Character Sets)과 한글 인코딩

1980년대에 들어 아시아 국가들이 성장하면서 각자 자국어를 지원하는 문자 인코딩 시스템을 설계하기 시작했습니다.

한국에서는 1980년대 초부터 여러 인코딩 방법 도입을 시도했는데요, 한 번 살펴보겠습니다.

한글 조합형 인코딩

조합형 인코딩 방식은 한글을 초성, 중성, 종성으로 나누어 각각의 낱자를 조합하여 표현하는 방식으로, ‘한글’이라는 단어를 ‘ㅎㅏㄴㄱㅡㄹ’로 낱자를 조합하여 표현하는 것입니다.

완성형 자모로 결합한 모든 글자를 표현할 수 있다는 장점이 있지만, 용량 효율성 면에서 좋지 않았습니다.

예를 들어서 “가”는 2바이트지만, 받침이 있는 경우 3~5바이트가 될 수도 있다는 것이죠.

한글 완성형 인코딩과 조합형의 사멸

완성형 인코딩은 초성, 중성, 종성을 조합하지 않고 미리 만들어진 “완성된 한글 음절”을 코드로 저장하는 방식입니다.

외국어 및 특수문자를 미리 가정하고 만들었기 때문에 조합형에 비해 국제 표준과 충돌이 적다는 장점이 있었지만, 미리 조합되어 있는 글자 외의 문자는 어떻게 해도 표시할 수 없다는 단점도 있었습니다.

사용되는 모든 글자가 포함된다면 문제가 없겠지만, 1987년에 최초로 만들어진 기본 완성형 코드의 한글 글자 수는 2350자가 전부였는데, 현대에 사용되는 한글 11,172자에 비하면 크게 부족했습니다.

이는 당시에 PC통신망에서만 논쟁거리였던 것이 아니라 실제 기업들의 업무에도 꽤 민감한 문제였습니다.

1990년의 한국은 기업들이 종이 문서 기반 처리에서 전산화가 이루어지던 과도기였는데, 당장 MBC 내부 전산망에 MBC의 드라마 ‘똠방각하’의 제목을 못 써넣어 ‘돔방각하’라고 표기하는 경우도 발생했습니다.

이런 해프닝이 지속되면서 조합형과 완성형 인코딩의 팽팽하던 주도권의 균형이 무너진 시점은 윈도우95 등장 이후입니다. MS가 OS 차원에서 확장 완성형을 기본으로 지원하게 된 것이죠.

이 때 등장한 것이 MS의 CP949(코드 페이지 949) 입니다. CP949는 KS X 1001라는 완성형 인코딩 체계에 없는 한글 8822글자를 추가해서 EUCKR을 확장한 완성형 인코딩 방식으로, 128이상의 영역 중 EUCKR이 사용하지 않던 영역에 이 8822글자를 할당했습니다.

하지만 이 CP949도 문제점이 있었는데요, 기존의 완성형과 새로 추가된 글자가 사전순이 아니고, 심지어는 기존 완성형 사이에 정렬되어 있기 때문에 단순히 코드만을 가지고 사전 순서로 찾거나 정렬할 수 없었다는 것입니다.

UNICODE(유니코드)

이러한 혼동 속에서 ‘하나의 문자 집합’으로 전 세계 문자를 모두 표현하려는 움직임이 있었고, 썬마이크로시스템즈, 애플, MS, IBM 등의 회사들이 ‘유니코드 컨소시엄’을 만들어 전세계 문자를 통합한 유니코드(Unicode) 를 만들기 시작했습니다.

참여한 회사들을 보면 거의 미국 회사인 것을 알 수 있는데요, 미국 회사들이 전세계에 소프트웨어를 판매하다보니 통합의 필요성을 느꼈다는 것이겠죠?

그렇게 미국의 주도 하에 1991년 UNICODE 1.0이 탄생합니다.

유니코드의 기본 아이디어는 간단합니다. 세상의 존재하는 모든 글자를 다 모아 하나의 코드 체계로 표현하는 것입니다.

예를 들어 ‘A’는 65번, ‘가’는 0xAC00번, ‘が’는 0x304C번과 같이 번호를 부여하여 명칭 그대로 코드(Code)를 통일(Uni)한 것입니다.

유니코드가 포함하는 문자를 보면 우리가 흔히 아는 글자 외에도 도형, 이모티콘과 같은 것들도 포함이 되어있는데요, 컴퓨터로 표현 가능한 모든 문자를 코드화하는 것이 유니코드의 목표였습니다.

하지만 유니코드를 막상 쓰려고하자 문제가 발생합니다. 문자수가 너무 많기 때문에 한 바이트로 값을 저장할 수 없고 2바이트 이상을 사용해야한다는 점입니다.

최신 버전(16.0)으로 유니코드가 표현하는 글자 수는 154,998입니다. 2바이트로 표현할 수 있는 숫자가 총 65,535개이니 2바이트로도 부족하고 3바이트를 써야 모든 유니코드의 문자를 표현할 수 있습니다.

여기에는 두가지 문제점이 있습니다.

1. 바이트 순서(Byte order or Endian) 문제

한 데이터를 저장할 때 어떤 컴퓨터 시스템은 순서대로 저장을 하지만 어떤 컴퓨터 시스템은 역순으로 저장을 합니다.

예를 들어 “가”(0xAC00)를 어떤 컴퓨터 시스템은 0xAC00으로 저장하지만, 어떤 컴퓨터는 0x00AC로 저장합니다.

즉, “가”를 0xAC00으로 저장하는 컴퓨터에서 만든 문서를, “가”를 0x00AC로 저장하는 문서에서 읽으면 엉망이 됩니다.

이를 해결하기 위해 등장한 방법이 BOM(Byte Order Mark, 바이트 순서 표식) 입니다. 데이터가 시작하기 전에 문서의 맨 앞에 BOM(FE FF 또는 FF FE)을 먼저 저장하자는 방법입니다.

문서를 읽는 쪽에서는 앞에 2바이트를 먼저 읽고 나서 FE FF이면, 0xAC00을 “가”라고 해석하고, 앞에 2바이트가 FF FE였다면 0x00AC를 “가”라고 해석하게 됩니다.

2. 공간 낭비 문제

과거 ASCII 시절에는 1바이트면 “A”를 저장할 수 있었는데 이제는 “A”를 저장하기 위해서 3바이트를 써야합니다. 영어로만 된 문서는 모든 문서의 크기가 3배가 되어 손해겠지요.

한글도 마찬가지로 2바이트로 표현했었는데 3바이트를 써야합니다. 같은 정보를 저장하는데 1.5배의 공간이 필요합니다.

이 문제를 해결하기 위해서 Variable-length byte(가변 길이 바이트) 라는 아이디어가 등장합니다. 자주 쓰는 문자는 적은 바이트로 표현하고, 가끔 쓰는 문자는 많은 바이트로 표현하는 것입니다.

예를 들어 기존 ASCII 영역에 있는 영어 알파벳은 기존과 같이 1바이트(정확히는 7bit)를 써서 표현하고, 아랍 문자는 2바이트를, 베다어 글자는 3바이트로 표현하는 식입니다.

이렇게 하면 확률적으로 문서의 많은 부분은 적은 바이트를 사용하기 때문에 전체적으로 적은 공간으로 정보를 저장할 수 있습니다.

UTF(Unicode Transformation Format)

이 가변 길이 바이트 아이디어를 실체화한 인코딩 중 가장 대표적인 것이 UTF-8입니다. UTF-8의 규칙에 따르면:

0x000000 ~ 0x00007F는 0x1xxxxxx의 1바이트 (ASCII 동일) 0x000080 ~ 0x0007FF는 0x110xxxxx 10xxxxxx의 2바이트 0x008000 ~ 0x00FFFF는 0x1110xxxx 10xxxxxx 10xxxxxx의 3바이트

위와 같이 저장되며, x에 해당하는 부분이 실제 문자에 해당하는 코드가 나눠 저장되는 것입니다.

UTF-8으로 된 문서에도 BOM을 쓸 수는 있지만 실제로는 대부분 쓰지 않습니다. 몇가지 규칙을 이용해서 쉽게 문서의 Byte Order를 알 수 있기 때문입니다.

오히려 어떨 때는 BOM이 있고, 어떨 때는 BOM이 없어서 혼란을 주기 떄문에 UTF-8에 BOM을 쓰는 것을 금지해야한다는 주장도 있습니다.

UTF-8가 가지는 또다른 장점은 기존의 ASCII와 완벽히 호환된다는 점입니다.

앞의 설명처럼 0x00007F까지는 ASCII와 똑같은 코드를 사용하기 때문에 이 영역을 사용하는 문서(수많은 영어 문서, 프로그램 코드 등)는 ASCII 인코딩과 UTF-8 인코딩이 완전히 일치합니다.

즉, 과거의 수많은 ASCII 인코딩 기반 파일을 전혀 변환없이 사용할 수 있습니다.

현재 유니코드는 계속해서 새로운 문자를 추가하고 있습니다. 알파벳, 한글과 같은 전통적인 문자 개념을 넘어서 이모티콘과 같은 영역까지 포함하고 있기 때문이죠! 유니코드의 업데이트 내역은 유니코드 홈페이지에서 확인할 수 있습니다.

이렇게 유니코드는 다른 바이트를 사용해 유니코드를 UTF 인코딩 규칙이라는 해결법을 고안하여, 다른 국가의 사용자가 다른 언어로 된 문자를 동시에 영향을 받지 않고 읽을 수 있도록 하여 인터넷의 대중성을 크게 향상시키게 되었습니다.

맺음

문자 인코딩 기술의 발전 과정을 살펴보았습니다.

개인적으로 역사 공부하는 것을 좋아해서 어느 사실에 대한 기원을 쫓아가는 것을 좋아하는데, 개발 지식도 이렇게 접근하니 정리하는 데는 힘들었지만 글쓰면서 재밌었네요.

어느 사실, 정보에 대한 기원을 알아보는 Origin Series를 정기적으로 연재해보면 좋겠다는 생각이 듭니다.

읽어주셔서 감사하고, 피드백은 달게 받겠습니다.

출처