Tumgik
arachneng · 7 years
Text
카일루아, 둘: 파서 (1)
저번 글에서 카일루아가 왜 만들어졌는지, 그리고 초기 설계가 어떠했는지에 대해서 간략히 설명했다. 많은 언어가 그렇듯, 카일루아 또한 실제로 작성된 첫 코드는 파서였다. 이번 글에서는 카일루아의 파서가 어떻게 발전했는지를 이야기해 보기로 한다.
왜 러스트를?
모두가 알고 있듯 카일루아는 러스트로 작성되어 있다. 꽤 많은 루아 타입 시스템이나 루아 확장 언어가 루아로 작성된 것과는 대조적인데, 여러 이유가 있다.
내가 작성하기 편했다. 어차피 이걸 무슨 언어로 짜도 건들 수 있는 사람은 나 밖에 없을 것이다(…).
타입 시스템이 강하고 대수적 자료형(algebraic data type)이 있는 언어는 보통 언어 구현 짜기 편리하다.
적절한 크기의 네이티브 바이너리가 하나 툭 튀어나오면 배포가 편리하다.
솔직히 말해서 이걸 루아로 짜라고 하면 그냥 혀 깨물고 죽을 것이다.
내가 한 러스트 빠 하긴 하지만(…) 카일루아 작업을 하면서 한 가지 더 느끼게 된 것은 바로 이것이다.
러스트 코드는 매우 리팩토링하기 좋다. 리팩토링 과정에서 컴파일 오류를 잡는 건 물론 해야 하지만, 리팩토링된 결과가 이전과 다르지 않다는 걸 타입 시스템을 사용해서 증명하기가 매우 수월하다.
현재의 카일루아 코드는 상당히 유기적으로 만들어진 코드라서, 크레이트가 10개로 쪼개져 있긴 하지만 여기 저기 이상한 구석이 많이 남아 있다. 유기적으로 만들 수 밖에 없던 것은 설계를 여러 차례 변경하면서 아키텍처도 함께 바꿔야 하는 경우가 많았기 때문인데, 그럼에도 불구하고 힘을 덜 들이고 리팩토링을 할 수 있었던 덕분에 그 정도로 힘든 작업은 아니었다. 좀 더 힘을 쓰면 타입 시스템 수준에서 이상한 구석들을 많이 잡을 수 있겠지만 여러 가지 이유로 (시간 문제도 있고, 그렇게 해서 얻는 이득이 크지 않다고 판단하여) 하지 않았다.
LALRPOP
코드를 오픈소스로 공개하면 좋은 것이 이렇게 초창기 코드를 그대로 링크해서 보여 줄 수 있다는 것이다. 이 코드는 날짜를 보면 알 수 있지만 설계에 앞서 빠르게 테스트로 구현한 토큰화 코드이고, 전형적인 추상 문법 트리(AST) 코드와 함께 LALRPOP으로 작성된 간단한 테스트 파서가 들어 있다.
토큰화 과정은 그다지 별로 특이한 점은 없다. 현재 코드와 비교해도 큰 차이가 없는데, 추가된 점은 오류 보고와, 카일루아 전용 토큰을 해석할 수 있도록 했다는 것, 그리고 이 글의 뒤에서 설명하겠지만 토큰 이전에 한 단계가 더 추가된 점이 있다. 토큰화 자체는 u8 바이트 반복자로부터 Tok 값 반복자를 만들어 내는 어댑터로 구현되었는데 이렇게 하다 보니 오류 복구가 미묘해진 부분이 있긴 하다. (최종적으로 모르는 문자가 나오면 아는 문자가 나올 때까지 스킵하고 하나의 오류를 보고하도록 했기 때문에 토큰화 과정에서 복구 불가능한 오류는 나올 수 없게 되었다.)
크게 달라진 것은 파서이다. LALRPOP은 여전히 꽤 실험적인 라이브러리 같지만 2015년 11월 당시에는 정말 실험적인 라이브러리였다. 내가 LALRPOP을 써 보면서 느낀 가장 큰 문제는 두 가지인데, 하나는 생성된 파일이 너무 크다는 것이었다. 몇 줄 안 되는 parser.lalrpop으로부터 생성된 parser.rs 파일이 이미 2천줄이 넘어간다! 실제로는 do와 break만 파싱하는 아주 간단한 물건인데도 말이다. 그래도 뭐 코딩이 편하니까 큰 문제 없겠지 하고 작업을 하긴 했었는데… 루아 문법을 거진 다 구현하고 나니 문법은 200여줄 남짓했는데 생성된 파일은 20만줄을 넘는 상황이 벌어졌다. 이 정도 되면 컴파일하는 데 몇 분 걸리고 난리가 난다. (당시 러스트 컴파일러는 더 느렸다는 점을 생각해 보자.) 이런 상황이 벌어진 것은 당시 LALRPOP이 LR/LALR 파서를 구현할 때 컴팩트한 테이블을 만든 게 아니라 모조리 match로 때려 박아서였고, 지금은 상황이 개선된 것으로 알고 있다(짐작컨대 당시 파서를 지금 LALRPOP로 컴파일하면 1/10 정도 나오지 않을까 싶다). 어쨌든 당시에는 이터레이션이 너무 느리다는 것 자체만으로 상당한 스트레스였다.
다른 문제는 좀 더 본질적인데, LALRPOP은 shift-reduce 충돌을 제어할 방법을 지원하지 않는다. 형식 문법이 뭔지 안다는 전제 하에 잠깐 파싱 이론을 가볍게(?) 들어 가면, LR 계열 파서는 현재까지 읽은 토큰들(42, foo, = 따위)이 어떤 비단말(non-terminal) 기호(루아의 경우 exp, chunk 따위)들에 포함되는지를 스택에 집어 넣고, 가장 안쪽에 있는 비말단 기호가 끝나서 스택에서 뽑혀야 하는지(“reduce”), 또는 다음 토큰을 읽어야 결정할 수 있는지(“shift”)를 상태 기계로 만들어서 파싱을 수행한다. 이를테면 다음과 같이 local x = 42 + 54 * f() return x라는 코드가 있는데 +까지 읽었다면…
| 열려 있는 비단말 기호들 | | <----| - - - -> exp <--------------| - - - -> stmt <--------------| - - - - - - - - > chunk local x = 42 + | 54 * f() return x | 현재까지 읽은 위치
…그럼 현재 위치에서 열려 있는 비단말 기호는 안쪽부터 순서대로 수식을 나타내는 exp, 문장을 나타내는 stmt, 그리고 실행 가능한 코드 뭉치를 나타내는 chunk 세 개가 있다. 42 또한 수식이고, 맨 앞의 x는 문법과 대조해 보면 대입문 좌변을 나타내는 var에 대응하겠지만 현재 위치에서는 더 이상 영향을 줄 수 없으므로 스택에 없는 것을 유념하자. 직관적으로 생각해 보면 이 상태에서는 어떤 비단말 기호도 끝마칠 수 없고, 따라서 다음 기호를 읽어야만 뭘 할지를 결정할 수 있다. 바로 이 과정을 상태 기계로 표현한 것이 LR 파서인 것이다. LR 파서 안에서는 SLR, GLR, LALR 등의 다양한 바리에이션이 있긴 하지만 기본적인 구조는 다 똑같다.
근데 뭐가 문제냐고? 카일루아 파싱 테스트에 들어 있는 다음 코드를 생각해 보자.
a = b + c (print or io.write)('done')
이건 문장 하나일까 두 개일까? 두 줄로 나뉘어 있는 건 아무 의미도 없다. 루아에서 개행문자는 공백과 완전히 똑같다(비슷하게 세미콜론을 생략 가능한 자바스크립트와는 이 점에서 다르다). 루아 문서에 따르면 이는 한 문장으로, c(...)는 함수 호출로 해석된다. 보이는 것과 비슷하게 두 문장으로 바꾸려면 중간에 세미콜론을 넣어야만 한다.
a = b + c; (print or io.write)('done')
하지만 이런 식으로 문법이 구성되어 있으면 c를 읽은 시점에서 현재 열려 있는 수식(exp)이나 문장(stmt) 따위를 reduce, 즉 마무리짓고 다음 문장으로 진행해야 할지, 아니면 다음 토큰을 shift, 즉 더 읽어 봐야 할지를 결정할 수 없다. 이를 shift-reduce 충돌이라고 한다. 이런 류의 충돌은 은근 흔해서 C의 조건문에서도 비슷한 사례가 있다. 이걸 해결하는 방법은 크게 두 가지가 있다.
해당 상황에서 shift를 할지 reduce를 할지를 명시적으로 지정한다. 꽤 많은 파서 생성기는 이 상황에서 옵션을 주면 강제로 shift를 하게 하는 기능이 있어서, 이 경우에도 shift를 하게 하면 문제가 해결된다. 근데 LALRPOP에는 이게 없다.
문법을 뜯어 고쳐서 충돌이 불가능하게 만든다. 루아 문법 같은 경우 수식으로 끝날 수 있는 모든 문장 뒤에는 (로 시작할 수 있는 문장(실질적으로 다른 대입문이나 함수 호출로 제한될 것이다)이 올 수 없도록 문법을 갈아 치우면 된다.
LALRPOP의 저자가 쓴 소개글에 따르면 s-r 충돌은 보통 문법 작성시의 실수로 생기는 경우가 많고, 따라서 실수를 잘 잡을 수 있게 하려면 s-r 충돌을 내버려 두는 게 아니라 문법을 고쳐서 충돌을 잡기가 더 쉽게 만들어야 한다고 생각하여 이런 결정을 내렸다고 한다. 나는 이 생각에 어느 정도 일리가 있다고는 보지만, 문제는 이 경우에는 충돌을 잡으려면 거의 모든 비단말 기호가 중복되어야 한다! 이는 충돌이 exp와 var 사이에서 일어나고, exp가 끄트머리에 등장하지 않는 문법 요소는 거의 없었기 때문이다. (하다못해 stmt조차도 repeat … until … 문법 때문에 중복이 필요하다.) 물론 어떻게든 고치면 돌아가게 만들 수 있었지만, 이런 상황에서 컴파일도 오래 걸리고 있다 보니 이틀 정도 잡고 고쳐 보다가 때려치고 손 파서로 선회했다.
그리하여, 현재의 카일루아 파서는 거대한 재귀 강하(recursive descent) 파서가 되었다. 중간에 수식 처리를 더 빠르게 하고 스택을 덜 쓰기 위해서 연산자 우선순위 파서를 추가적으로 구현하긴 했는데, 이건 성능 외에도 루아가 이미 똑같은 코드를 쓰고 있어서 거기에 맞추려고 한 것도 있다. 그리고 위에서 언급되었듯 손 파서가 실수할 가능성이 없진 않지만, 이미 파서를 몇 번 짜 봤고, 대상 문법이 극히 복잡한 게 아니며(LALR(1)로 파싱이 안 되��� 수준이라거나…), 적절히 유지보수 가능하게 짜여졌고 테스트가 되는 상황이라면 손 파서를 굳이 기피할 이유는 없었다. 혹시 LALRPOP이 더 많이 발전해서 PEG 같은 걸 지원하기 시작하면 모르겠다.
입력 소스와 위치 추적
손 파서로 모든 걸 갈아 치운 뒤, 그 다음으로 파서 구조가 크게 바뀐 때는 오류 보고를 처음 설계할 때였다. 오류 보고에 대해서는 나중에 더 자세히 설명하겠지만, 일단은 오류 보고에는 기본적으로 열 단위까지 위치 정보가 들어갈 수 있다고만 알아 두자. 물론 대응되는 코드도 함께 표시되어야 한다(IDE에서는 그럴 필요가 없지만, 카일루아는 원래 명령줄 체커였다).
열 단위까지 위치 정보가 들어간다는 얘기는 파서가 뱉어 내는 AST에도 그 수준의 위치 정보가 모두 들어 있어야 한다는 소리와 동일하다. 이런 류의 위치 정보는 정말 아무 데나 들어갈 수 있기 때문에 일반화(generic) 타입이 있는 게 적절한데, 러스트 컴파일러에서 이 역할을 하는 자료구조의 이름을 Spanned라고 붙였기 때문에 카일루아에서도 마찬가지로 Spanned<T>라는 이름을 썼다. 이 타입이 가지고 있는 위치 정보(Span)는 크게 세 가지로 구성된다.
어느 파일에서 유래한 위치 정보인가? 물론 꼭 파일일 필요는 없다. 파서나 체커가 필요해서 생성한 경우도 있고, 내장 선언에서 유래한 경우도 있을 수 있다. 카일루아에서는 이들 정보를 모두 퉁쳐서 Unit이라는 타입으로 만들어 놓았다.
시작점이 파일의 어느 위치인가?
끝점이 파일의 어느 위치인가? 실제 범위에 끝점이 포함되냐 안 되냐는 대부분 편의 문제인데 카일루아에서는 포함되지 않는다.
그리고 각 필드 별로 32비트씩 12바이트가 할당되었다. Unit도 32비트라는 점에서 알 수 있듯이 이 데이터를 정말로 해석하려면 Source라 하는, 입력 소스를 위한 별도의 타입이 필요하다. 좀 더 정확히는 Unit을 만드는 주체가 Source라고 해야 할 것이다. 위치 값은 바이트 단위거나, (후술하듯) 워드 단위로 어느 쪽이 쓰이느냐는 파일에 따라 결정된다.
과연 이게 유일한 설계인가 하면, 당연히 그럴 리가. Source를 없애는 방법(Unit을 참조 카운팅을 하는 스마트 포인터로 갈아 치우면 된다)이라거나, 오프셋에 파일 정보를 합쳐 버리거나, 심지어 더 많은 정보를 Source에 던져 주고 아무 정보 없는 인덱스만 남기거나 하는 대안이 존재한다. 내가 위의 설계를 채택한 것은 기본적으로 i) 구현하기 쉽고 ii) 먼 훗날 Source 같이 위치 정보가 필요할 때 무조건 필요한 또 다른 맥락이 들어갈 때 기존에 존재하는 맥락이 있는 게 편해서였는데, 나중에 보니 내 생각이 아주 틀리진 않았지만 완벽한 것도 아니었다. 다른 설계의 장단점에 대해 잠시 살펴 보기로 하자.
Source가 없는 설계는 더 이상 Unit이 어느 AST에서도 사용되지 않을 때 메모리를 자동으로 해제해 준다는 잇점이 있고, 멀티스레드 환경에서 좀 더 사용하기 쉽다(기존 설계에서는 Source에 적절히 락을 걸어야 한다). 하지만 이런 상황이 생각보다 흔하진 않았고, 어차피 후술할 IDE와의 연동이 들어가면 이 설계의 장점이 크지 않다. 메모리를 더 많이 쓸 수 있고 참조 카운트의 부하가 있을 수도 있다는 것도 걸림돌이었다.
오프셋에 파일 정보를 포함하는 설계는 다르게 말하면 파일 정보가 없이 거대한 버퍼에다가 모든 파일의 내용을 집어 넣고 그 버퍼에서의 인덱스를 위치 정보로 쓰겠다는 뜻이다. (물론 나중에 참조를 위해서 파일 정보를 따로 둘 순 있겠지만.) 이 설계를 잠시 고민하다가 접은 가장 큰 이유는 파일이 변경될 가능성을 배제할 수 없었기 때문이다. IDE에서 일부 파일만 수정된 걸 새로 체크하는 경우는 흔한 일이다.
모든 위치 정보를 Source에 의존하는 핸들로 만드는 설계는 생각보다 할만할 수도 있다. 본래 이걸 채택하지 않은 이유는 위치 정보가 아무리 불투명해도 두 Span을 포함하는 하나의 Span을 계산한다거나 하는 정도의 연산은 할 수 있고, 그럼 그걸 할 때마다 Source를 요구하는 건 좋지 않다고 생각했기 때문인데, 다행히 그다지 흔한 일은 아니었다. 그러나 디버깅 과정에서 위치 정보를 볼 일이 꽤 많은데, Source가 없는 맥락에서 핸들 번호만 보이는 건 별로 유쾌한 경험은 아니라1 여전히 그다지 내키지 않는다.
한편으로 IDE와 연동을 해 보다 보니까 Source 자체에도 설계 변경이 필요했다. 하나는 UTF-16(처럼 생긴) 문자열의 존재로, 비주얼 스튜디오(C#)나 비쥬얼 스튜디오 코드(JavaScript)나 2바이트 워드를 기반으로 하는 문자열을 인터페이스에서 강제하고 있기 때문에 Source에서도 이를 따라야 했다. 결과적으로 Span의 인덱스는 파일이 어디서 왔느냐에 따라서 바이트 인덱스거나 워드 인덱스냐가 결정되고, 개별 문자는 8비트 바이트일 수도 16비트 워드일 수도 있게 되었다(다행히 루아의 모든 문법 요소는 7비트 그래픽 문자에 포함된다). 물론 인코딩 정보가 어디 있는 건 또 아니기 때문에, 8비트 바이트 파일이라면 UTF-8이거나 MBCS2로 가정하고, 16비트 워드 파일이라면 UTF-16으로 가정하는 식으로 처리한 뒤, 파싱 후에는 UTF-16을 UTF-8로 변환하여 바이트 문자열로 정규화하는… 뭐 그런 노가다를 해야 했다.
또 하나는 Source가 단순히 변경 가능한 수준 뿐만 아니라 버저닝이 가능해야 한다는 점이었다. 이 설계의 존재를 알게 된 것은 VS 확장을 만들 시점이었는데, 파싱을 하거나 체킹을 할 때 사용한 소스 코드와 현재의 소스 코드와 다를 수 있기 때문에, 소스 코드가 변경될 때마다 다른 버전을 붙이고 Span(에 대응하는 타입)을 버전들 사이에서 서로 변환 가능하게 하는 것이었다. 다행히 현재 카일루아 설계는 파일이 추가 삭제되는 것에는 대응되어 있으니까 기술적으로는 새 파일이 추가된 것처럼 구현하면 되지만, 어떤 “파일”들이 실제로는 같은 파일의 다른 버전을 가리키는지, 그리고 이들 사이에서 Span이 어떻게 변환되는지는 아직 구현한 게 없다. 따라서 현재는… 변경이 일어나게 되면 그냥 락을 건다. 장기적으로는 버저닝을 구현해야 할 거라 본다.
차회 예고
본래는 파서의 오류 복구를 이번 글에 쓰려고 했는데… 파서 얘기를 쓰다 보니까 너무 길어져서 해당 부분은 다음 글로 넘기기로 한다.
카일루아에는 실제로 이런 식으로 내부적으로는 핸들로만 보이는 값이 하나 존재한다. 바로 ScopedId라고 부르는 지역 변수 인덱스인데, 이름이 안 나오는 건 좀 귀찮지만 해 보다 보니까 결국 지역 변수 인덱스를 할당하는 로그를 찍으면 검색으로 알아낼 수 있어서 디버깅할 때 문제가 크진 않았다. 하지만 위치 정보를 이런 식으로 로그에 매번 찍어 줄 수는 없는 노릇이다. ↩︎
대부분의 레거시 문자 인코딩이 포함된다. 컬럼 계산할 때 바이트 인덱스가 컬럼 인덱스와 사실상 같다는 매우 중요한 장점이 있다. 물론 GB 18030 같은 변종이 있긴 하지만… ↩︎
4 notes · View notes
arachneng · 7 years
Text
카일루아, 하나: 태동
이 글을 쓰다가 지우다 한지는 꽤 오래 되었다. 첫 문단만 쓰여 있는 초안은 2015년 12월 15일이라는 날짜가 박혀 있고, IRC 로그를 잠시 뒤져 보니 처음으로 이 일을 하고 있다는 얘기를 은연중에 언급한 건 2015년 12월 3일이었다. 이렇게.
<lifthrasiir> 아 오랜만에 PL 관련된 걸 하려니까 아무것도 기억나지 않음…
그로부터, 지난 4월 25일에 카일루아라는 물건을 발표하기까지는 16개월이 걸렸다. 그동안 나를 아는 여러 사람들은 이제 나루를 다시 만드시죠라는 무시무시한 개드립을 치고 있었는데, 한 언어가 설계되고 구현되고 다시 설계된 뒤에 재구현되는 과정을 거쳐 보니 나루는 참 꿈도 컸구나 하는 생각이 든다. 이 정도 언어 만드는 데 이 정도 노력이 필요한데 나루 같은 걸 어떻게 만들 생각을 했단 말인가…
나에게 카일루아는 두 가지 의미를 가지고 있는데, 하나는 앞에서 말했듯 언어를 만드는 모든 과정을 처음으로 내 손으로 다 거친 사례라는 점이고, 다른 하나는 한국에서 드물게 언어 관련해서 밥 벌어 먹고 산 사례였다는 점이었다. (나는 이런 류의 일을 한 번이라도 해 본 사람이 학계 제외하면 한국에서 두 자릿수 정도 되지 않을까 싶다.) 여기에는 마침 상대적으로 필요한 인력이 적어도 되는 시기에, 되면 훌륭하고 안 되어도 큰 부담은 없는 프로젝트가 있었고, 그걸 수행하기에 적합한 사람이 마침 손이 비어 있어서 맡겨도 되는 몇 가지 행운이 함께 따라 주었던 것 같다. 물론 반대로 보면 혼자 하는 프로젝트는 언제나 번아웃의 위험이 있고 실제로 몇 차례 겪기도 했지만, 그럼에도 불구하고 이런 드문 기회를 얻은 것은 긍정적으로 생각한다. 어쨌든, 뭔가 나오지 않았는가? :)
이 글은 카일루아 연작의 첫번째 글이 된다. 어떤 부분은 당연한 부분도 있을 수 있고 어떤 부분은 나만 그렇게 생각했을 수도 있지만, 이번 연작에서는 카일루아의 처음부터 끝까지를 모두 하나 하나 다뤄 보려고 한다. 읽는 사람한테나 그걸 쓰는 나한테나 쉽지 않은 연작일 수 있는데 아무쪼록 모종의 도움이 되었으면 하는 바람이다.
도대체 어쩌다 루아를...
작년에도 올해에도 발표 후 공통으로 나온 질문으로 "도대체 왜 루아를 이렇게까지 써야 하나요?"라는 질문이 있다. 당시에 짧게 답변한 게 있긴 하지만, 애초에 이걸 16인월(person-month)을 들여 만들 필요가 있었는가를 정당화하려면 한 번 짚고 넘어가야 할 문제일 것이다.
루아는 잘 알려진 프로그래밍 언어로, 언어가 상당히 단순한데다 이식성 있는 ISO C로 작성되어 아무 데나 갖다 붙이기 좋은 구조라 스크립팅용으로 아주 많이 쓰는 언어이다.1 게임 업계에서는 《월드 오브 워크래프트》가 애드온에 갖다 쓴 것 때문에 유명해진 것 같고, 그 밖에도 구현이 작다 보니 전혀 예상치 못 한 프로그램, 이를테면 Pandoc 같은 곳에서 루아가 들어 있는 경우도 볼 수 있다. 요즘은 LuaJIT 같은 좋은 구현과 Torch 같은 녀석도 있어서 사용처가 늘고 있다.
데브캣에서 루아는 다양한 용도로 사용되고 있지만, 카일루아의 직접적인 개발 동기가 된 것은 《마비노기 듀얼》의 클라이언트와 서버 모두에 루아가 사용되었기 때문이다. 이 프로젝트는 데브캣에서 실질적으로 모바일로 출시한 제대로 된 첫 게임이었고, 스튜디오 전체가 새로운 개발 환경에 적응을 하는 전환기에 있던 시기에 만든 것이다. 클라이언트의 경우 초기 게임 디자이너가 기데로스를 사용하여 만든 프로토타입이 프리프로덕션을 거쳐서 프로덕션까지 대규모 수정 없이 유지되었기 때문에 루아를 자연스럽게 쓰게 되었다. 다른 많은 이유도 있지만 가장 큰 이유는 바로 이것, 즉 새로 만드는 비용이 비쌌기 때문이다. 그럼 서버는 어쨌냐 하면… 프로토타입 시절부터 오랫동안 클라이언트는 서버 없이 클라이언트들끼리 소켓으로 대강 통신하는 구조였다. 그러다 제대로 된 서버를 붙이기 위해서 C#로 개발이 들어갔는데, 상당한 난항을 겪었다(고 들었다. 나는 이 코드를 직접 본 적이 없다). 결국 일정에 맞출 수 없음이 명백하게 되자, 클라이언트 단에서 루아를 이미 사용하고 있으니 해당 바인딩을 그대로 가져다가 asio에 붙여서 빠르게 구현하자는 제안이 나왔다. 그 뒤에 무슨 일이 일어났는지는 모두가 아시리라 믿고 생략하기로 한다.
내가 모든 의사결정에 참여한 것은 아니지만 나는 이 의사결정이 매우 시의적절했다고 생각한다. 결과적으로, 게임이 제대로 나오고 서버에서는 별다른 큰 문제가 발생하지 않았다. 오히려 캐시 레이어로 레디스를 사용한 것이 문제에 더 많이 기여했다면 기여했지… 루아라는 언어 자체의 이런 저런 결점과는 별개로 기본적으로 한 워커 스레드가 한 루아 VM을 가질 수 있는 구조는 이런 저런 귀찮은 데이터 공유 문제 등을 피하는 데 도움이 되었다(사실 많은 점에서 우리는 루아를 node.js처럼, 하지만 그 설계를 더 극대화한 형태로 사용하고 있었다). 그리고 일반적인 웹 서버와는 완전히 다른 환경이기 때문에 라이브러리의 부재(후술하겠지만 아주 심각한 문제이다)가 그 정도로 크게 다가오진 않았다.
하지만 의사결정이 훌륭했던 거랑은 별개로 여러 의미에서 루아는 끔찍한 언어였다. 여기에는 일반적으로 말하는 동적으로 타이핑된 언어(동적 언어)들이 끔찍한 부분이 하나 있고, 루아 자체가 끔찍한 부분이 하나 있다. 루아는 그렇게 욕을 먹는 자바스크립트 이상으로 동적인 언어이기 때문에 어지간한 문제가 문제가 터지기 직전까지 전혀 보이지 않는 경우가 많았고, 엄청난 양의 유닛 테스트, 통합 테스트, 단언문, 피어 리뷰, 그리고 심지어 인터프리터 수정2에 이르기까지 다양한 대응책을 마련했음에도 빠져 나가는 건 항상 존재했었다. 스튜디오 내 대부분의 프로그래머는 C#을 이미 알고 있었지만 동적 언어를 써 본 사람이 많지는 않았고, 때문에 동적 언어의 문제를 어떻게 효과적으로 대응할지에 대해 모르는 사람도 여럿 있었다.
루아 자체도 그다지 좋은 언어라고 할 수는 없었다. 루아는 지금까지도 개발 과정이 그다지 공개적이지 못한데, 그 결과 언어 사용자나 다른 구현자들이 원하는 기능이 제대로 구현되지 않고 구현자들의 입맛에 따라서 많은 부분이 결정되는 문제가 있다. 굳이 내가 “입맛”이라는 낱말을 쓴 건… 아무리 봐도 이들이 내리는 모든 결정이 정확성이나 편의성 등등 보다는 구현체 크기를 작게 하는데 집중되어 있다는 느낌 밖에 들지 않기 때문이다. SQLite 같은 사례를 보면 루아는 지금보다 대략 3배 정도는 더 커져도 되는데도 말이다! 그로 인한 문제들로는 마당 바깥에 갖다 버린 버전 호환성(버그 수정 백포팅도 안 하면서…), 어중간한 라이브러리 생태계와 패키지 시스템… 음 그냥 까고 말하면 PHP만도 못한 수준인데 그런 다양한 것들이 있다. 아예 LuaJIT 전 개발자인 Mike Pall이 대놓고 깔 정도로.
그럼에도 불구하고 루아를 버리긴 어려웠다. 물론 《듀얼》처럼 클라이언트 서버 합쳐서 30만줄이 넘는 정신 나간 코드 베이스는 앞으로 자주 보기 어렵긴 하겠지만, 여전히 스크립팅이 여기 저기서 필요하다는 점은 부정할 수 없었고, 임베딩 가능하고 작은 크기에 외부 환경과 독립적으로 쉽게 붙일 수 있는 스크립팅 언어는 루아 말고는 여전히 큰 대안이 없었다. 그럼 루아로 몇천~몇만줄 정도의 코드를 짤 가능성은 여전히 상존하는데(실제로 개발 기간동안 몇 개의 사례가 등장했다), 그런 상황에서 쓸 수 있는 정적인 타입 체커를 만드는 게 루아를 갈아 치우는 것보다 싸지 않을까? 마침 당분간 손이 빌 사람이 전공자인데?
그렇게 카일루아는 태어났다.3
초기 설계
카일루아의 초기 설계 과정은 두 달 정도가 걸렸다. 여기에는 실제로 언어를 만들지 않고도 원하는 바를 이룰 수 있는지 조사하는 기간도 포함되었다. 설계 및 다른 구현에 대해서 다루기 전에 우리가 가지고 있던 제약사항을 좀 살펴 보자.
기존에 있는 코드에 점진적으로 적용 가능해야 한다. 많은 의미에서 점진적 타이핑(gradual typing)을 시사하는 제약이었다.
기존 개발팀이 큰 문제 없이 적응할 수 있어야 한다.
별도의 컴파일 과정이 있어서는 안 된다. 타입이 붙어 있는 코드가 그대로 실행이 가능해야 한다. 이건 코드를 loadstring해서 사용하는 경우가 많았기 때문에(이를테면 레디스 루아 스크립팅) 생긴 제약이다.
전역 변수를 잘 지원해야 한다. 당시 코드도 그렇고, 기데로스도 그렇고 전역 변수를 선언하고 사용하는 일은 흔했다.
구조적 타이핑을 잘 지원해야 하고, 테이블을 점진적으로 생성할 수 있어야 한다. 우리가 작성했던 많은 루아 코드는 암묵적 레코드 타입이 흔하게 날아다녔고, 어쩌다 보니 레코드가 한 번에 뙇하고 생성되지 않는 경우도 많았다. 이들을 모두 고치는 건 상당히 어려운 일이었다.
물론 클래스 비스무리한 것도 지원되어야 한다. 루아에 클래스 같은 건 없지만 비스무리한 걸 만들어 쓸 수는 있는데, 모두가 서로 다른 클래스 시스템을 만들어 쓰기 때문에(…) 어느 정도 설정 가능한 게 좋았다.
잘 동작한다는 전제 하에, IDE 지원도 있었으면 좋겠다. (나중에 보니 이게 매우 큰 변수였지만…)
당시 검토되었던 타입 체커는 다음 세 개였다. 나중에 한 개(Sol)가 더 추가되었는데 결정에 큰 변화를 줄 정도는 아니었다.
Typed Lua
Tidal Lock
Lua Analyzer
Sol
이 중 주석을 써서 컴파일 과정 없이 사용할 수 있는 체커는 Lua Analyzer 하나 뿐이었다. (Sol은 설명에 “문법이 안 예쁘면 사용되지 않을 것이다”라는 주장을 하고 있는데, 개인적인 경험으로는 명백히 거짓이다…) 근데 Lua Analyzer는 전역 변수 지원이 사실상 부재하다! 모든 사용될 전역 변수를 globals.lua 같은 파일에 집어 넣어야만 체크가 가능한데, 이미 알려진 인터페이스를 넣는 게 아니라 새로 만들어진 코드까지 거기게 다 집어 넣는다는 것은 사실상 불가능한 일이었다.4 Typed Lua는 자체 문법으로 컴파일을 하는 것도 모자라서 자체 클래스 시스템이 따로 있고, 점진적인 테이블 생성이 어려워 보였다. Tidal Lock은 점진적인 테이블 생성이 확실히 되는 타입 시스템을 가지고 있었지만 역시 자체 문법을 가지고 있었다. 나중에서야 확인해 본 Sol은 상대적으로 Typed Lua의 좀 더 멀쩡한(?) 버전처럼 보였지만 마찬가지로 자체 문법이 걸렸다. (살펴본 모든 시스템 중에서는 메타테이블을 가장 잘 지원했기 때문에 메타테이블을 좀만 더 많이 썼어도 이걸로 선회했을지도 모르겠다. 물론 우리는 메타테이블은 별로 안 썼다.) 결국 자체 개발을 하되, 가장 어려워 보이는 부분을 해결한 듯한 Tidal Lock의 타입 시스템을 기반으로 개발하기로 결정을 했다.
지금 다시 생각해 보면, Tidal Lock이 아니라 Lua Analyzer를 기반으로 하는 것이 더 작업이 쉽지 않았을까 하는 생각이 있다. 두 가지 이유가 있는데, 하나는 결국 Tidal Lock에서 유래한 타입 시스템은 뒤에서 설명하겠지만 교체되었기 때문이고, 다른 하나는 이들 시스템 중에서 IDE까지 지원되는 게 이것 하나 뿐이었기 때문이다. (Lua Analyzer는 LÖVE 게임 엔진용 IDE에 쓰려고 만들어진 시스템이다.) 변경하는 비용에 있어서도 다른 것보다 더 나았을 듯 싶은데, Lua Analyzer는 F#이지만 다른 모든 시스템은 루아(를 기반으로 한 자체 확장)로 이루어져 있고, 이미 한참 루아에 데여 본 입장에서는 루아로 또 몇천줄이 넘는 코드를 건드는 건 하고 싶지 않았다. 이 또한 후술하겠지만 사실 러스트를 쓰게 된 것도 이 이유 때문이다.
여하튼, Tidal Lock 타입 시스템은 기본적으로는 이런 특징을 가지고 있다. “값” 타입과는 별개로 변수나 레코드 값 등에 위치하는 “필드” 타입이라는 게 따로 있다(먼 훗날 카일루아의 최종 버전에서는 이게 “슬롯” 타입이라고 불리게 된다). 필드 타입은 값 타입에 변경 가능성이 붙은 형태로, 보통 생각하는 var과 const 말고도 just와 currently라는 이상한 것이 있다. currently가 특히 중요한 부분인데 다음과 같은 특성을 가지고 있다.
currently T는 현재 스코프로부터 알려진 값으로 유한번 인덱싱할 수 있는 위치에만 존재한다. 즉 일반 배열의 값으로 들어 갈 수는 없다.
currently T에 아무 타입 U나 대입하면 해당 타입은 제자리에서 currently U로 바뀐다(!). 다만, 다음 조건이 있다.
currently T는 항상 유일한 방법으로 접근 가능해야 한다. 그렇지 않은 상황이 발생할 경우, 이를테면 currently [x: currently number]가 다른 변수에 대입되어 두 개의 참조가 생길 경우, 두번째 참조는 currently [x: field]로 변경된다(field는 존재하지만 접근할 수 없다는 뜻이다).
이런 특성을 선형성(linearity)이라고 부르며 Tidal Lock의 가장 특이한 특징이자 local x = {} x.a = 42 x.b = 'string' 따위를 가능하게 하는 기능이다. 나는 이 타입 시스템이 어떻게 돌아가는지 파악하자마자 이런 질문이 머릿속에 떠올랐다.
왜 두번째 참조는 아예 접근할 수 없게 되는 것일까? 임시로 참조가 두 개가 생기는 일은 매우 흔한데 이렇게 해서야 사용하기 힘들다. currently 두 개가 만나면 함께 const나 var로 변경되게 하면 되지 않을까?
Tidal Lock에서 변경 가능성은 생략할 수 없다. 추론을 하긴 하는데 추론 과정에서 변경 가능성은 추론되는 게 아니라 말 그대로 경험적인 방법으로 때려 맞추기를 하고 있다. 이걸 추론할 수 있��� 하면 변경 가능성도 대부분 생략할 수 있게 되지 않을까?
살펴 보니 생각보다 할만한 변경인 것 같았다. 물론 안전성(soundness)을 증명하는 게 문제겠지만(사실 나는 첫번째 질문을 원 저자가 생각하지 않았을 리가 없고 증명이 귀찮을 것 같아서 안 선택한 것 같다) 나는 돌아가는 타입 시스템을 만들고 싶지 타입 시스템의 증명을 작성하려는 건 아니었으니까 말이다. 그래서 첫번째 질문은 그렇게 하기로 하고, 두번째 질문에 대해서는 이런 계획을 세웠다.
필드 타입의 변경 가능성이 여러 개가 될 수 있도록 한다. 이렇게 변경 가능성의 추이적 닫힘(transitive closure)을 구하면 var|const, var|currently, var|const|field 세 개가 추가되는 걸로 확인되었다.
변경 가능성이 여러 개 있을 경우 어느 쪽이 진실인가는 별도의 플래그로 추적하게 한다. 이 플래그는 세 가지 상태가 있는데, 둘 중 하나만 참이거나, 둘 다 참인 경우이다. (둘 다 거짓일 경우 바로 오류가 날 것이므로 추적할 필요가 없다.) var|const|field의 경우 플래그가 두 개 필요하다.
currently 두 개가 만났을 때 const인지 var인지는 바로 판단할 수 없으므로(var T와 var U는 서브타입 관계가 없어서 쓰기 어렵다) var|const로 변경하고 사용되는 걸 보고 결정하기로 한다.
이 계획을 세울 당시에는 몰랐는데, 나중에 보니까 이 기법은 태그되지 않은 합 타입(untagged union type)5을 구현하는 데도 종종 쓰이는 방법이었다. 만약 이 사실을 당시에 미리 알고 있었다면 난 바로 이 계획을 때려치고 다른 설계를 고민해 봤을 것이다. 이 뒤로 초기 타입 시스템의 구현 전반에서 합 타입이 항상 문제가 되었기 때문이다.
차회 예고
다음 글에서는 카일루아의 가장 밑부분, 즉 파서를 살펴 보기로 한다.
언어 외적으로는 비영어��에서 만들어져 널리 쓰이는 얼마 안 되는 프로그래밍 언어로도 알려져 있다. 브라질 PUC-Rio 대학에서, 국내 산업을 보호하기 위해 소프트웨어 제품에 금수 조치가 있던 시절에 자체적으로 만들었기 때문이다. 그 얼마 안 되는 다른 예제로는 일본에서 만들어진 루비가 있겠다. ↩︎
한 번도 대입되지 않은 전역 변수가 읽힐 때 오류를 내게 변경했다. 이미 선언된 변수에 오타를 냈을 때 오류가 나는 게 아니라 nil이 그냥 튀어나오는 상황을 방지하기 위함이다. 본래는 strict.lua라고 부르는, 전역 테이블의 메타테이블을 갈아 치워서 오류를 내 주는 방법을 쓰고 있었지만(이마저도 한 두 개가 아니다…), 실행 시간보다 더 먼저 잡을 수 있게 하기 위해 파싱 시점에서 오류가 나도록 한 것이다. ↩︎
몇 번 언급했지만 카일루아라는 이름은 하와이의 지명에서 따 온 것이다. 잘 알려지지 않은 점은 내가 이 지명을 알게 된 것은 리듬게임에서였다는 거고… 나중에 사람들이 KAIST-Lua 아니냐, 改lua 아니냐 하는 얘기를 하게 되었는데 전부 다 나중에 붙은 의미이다. 改lua는 그럴듯해서 나중에는 이중적인 의미라고 얘기하긴 했었다. 여튼, 나는 NDC 발표가 끝난 날마다 오락실에 가서 해당 곡을 플레이하고 항상 죽을 쑤었다. ↩︎
여기에는 루아 특유의 쓸데 없는 자유분방함도 한 몫 한다. 루아는 모듈 시스템 비스무리한 걸 가지고는 있지만 보장해 주는 게 별로 없어서, 심지어 모듈을 재귀적으로 require하는 걸 어떤 버전은 잡기도 하고 어떤 버전은 안 잡기도 한다. 일단 “표준적인” 컨벤션은 모든 걸 지역적으로 만들어서 외부 인터페이스만 return하는 거지만, 전역 변수에 전혀 제한이 없기 때문에 우리처럼 require하면 전역 변수를 쓰는 코드도 상당히 많다. 심지어 C로 구현된 모듈에서는 두 컨벤션이 섞여 있는 경우도 많다. ↩︎
T나 U가 될 수 있는데 해당 타입에 T나 U 타입의 값이 아무 제약 없이 자유롭게 대입될 수 있는 타입. 대입하기 전에 타입 생성자를 거쳐야 하는 경우는 태그된 합 타입(tagged union type)이라 하여 구분한다. 보통 태그되지 않은 경우가 훠어어얼씬 어렵다. ↩︎
2 notes · View notes
arachneng · 7 years
Text
어제 이런 무서운 트윗을 보았다. 사람들이 자바스크립트에서 소숫점 두 자리로 반올림을 하려고 하는데, 웬 문자열 연산이 들어가고 이상한 코드들이 난무하는 그런 상황이었다. 그래서 쓰게 된 트윗이 바로 이것인데...
일반적인 조언: 소숫점 n자리에서 반올림 같은 것은 출력의 문제입니다. 값을 미리 계산하면 99%의 확률로 실수합니다. printf 따위로 출력할 때 인자를 넘기는 것으로 대응하는 것이 쉽고 옳은 방법입니다.
이 얘기를 좀 더 길게 써 보려고 한다.
반올림이 출력의 문제라고 하는 것은 반올림의 거의 모든 사용이 출력 직전에 일어난다는 데서 나오는 관찰이다. 사실 착각하기 쉬울 수도 있는데, 우리는 십진법에 워낙 익숙하기 때문에 유효자릿수 같은 개념을 아무 생각 없이 사용하긴 하지만 사실 이건 숫자 범위(interval)를 나타내는 덜 정확한 방법에 불과하다. 이를테면 3.1415±0.0073 같은 오차 범위를 사람이 읽기 귀찮으니 3.142(7)이라고 표시하는 것인데, 이걸 두고 3.142 같은 숫자 자체에 의미가 있다고 주장하면 좀 곤란한 것이다. 반올림이 계산 과정의 일부로 들어가는 경우가 아주 없진 않은데1 일반적으로는 다분히 출력에 한정된 임의적인 것이므로 출력에 맡기는 게 맞다는 것이다.
반올림된 수치를 저장하는 게 문제가 되는 이유는 앞의 트윗에서도 링크되어 있지만, 의도치 않은 계산 실수를 쉽게 할 수 있다는 데 있다. 옛날에 쓴 글에서 사용한 표기법을 재활용하면, round(x)와 round([x])는 서로 다른 값이 나올 수 있는 데다가 [round([x])]의 참값이 round([x])보다 작을 수 있다. 구체적인 예시를 두 개 들어 보기로 하자. 아래에서 round(x, y)는 소숫점 y자리까지 남기고 반올림한다.
x = 1.005 [x] = 1.00499 99999 99999 89341 85896 35984 97211 93313 59863 28125 round(x, 2) = 1.01 round([x], 2) = 1.00 (???) x = 1.00005 [x] = 1.00005 00000 00000 10551 55962 60374 87760 18619 53735 35156 25 round([x], 4) = 1.0001 [round([x], 4)] = 1.00009 99999 99999 98898 65875 95718 44711 89975 73852 53906 25 (?????)
첫번째 예제에서는 [x] 시점에서 이미 원래 정확한 값보다 작은 숫자가 되어 버렸기 때문에 반올림에 의미가 없다. 두번째 예제에서는 반올림 자체는 잘 되었지만, 그걸 저장하니 마찬가지로 더 작은 숫자가 되어 버렸다. 따라서 (int) Math.round(x * 10000) 같은 걸 한다면 전혀 예상하지 못한 결과가 나와 버릴 것이다. 상황이 이러하니 정확한 값이 필요하다면 부동소숫점을 쓰지 말고, 부동소숫점을 이미 쓰고 있다면 반올림과 같은 출력에 관련된 것을 모두 출력에 맡기는 게 옳은 것이다.
한편 이 트윗을 쓰다 보니 루비 2.4부터는 1.005.round(2)가 1.01이 나온다는 충격적인 제보를 듣게 되었다. 이게 뭔 소리야?? 나도 모르는 사이에 스리슬쩍 루비가 BigDecimal을 기본값으로 만들었나?? 싶어서 코드를 찾아 봤는데 코드를 보고 더 충격을 먹었다. 문제의 커밋을 보면...
* numeric.c (flo_round, int_round): support round-to-nearest-even semantics of IEEE 754 to match sprintf behavior, and add half: optional keyword argument for the old behavior. (강조는 본인)
보아하니 이런 류의 버그에 대응하기 위해 sprintf를 고쳤고, 거기에 맞춰서 round 메소드도 따라 고친 것이다. 구현을 깊이 보진 않았지만 뭐 1 ulp2 사이에서 반올림이 가능하면 올려 버리는 그런 류의 구현이면 될 것이다.
이런 류의 반올림을 흔히 "겉치레 반올림"(cosmetic rounding)이라고 한다. 즉, 진짜 반올림 결과랑은 무관하게 사람이 보기 좋으라고 반올림을 한 것이다. 이런 류의 반올림이 전례가 없는 것은 아니어서, William Kahan의 고전에서는 매틀랩이 log_10(10^x)가 정수 x에 대해서 항상 x가 나오도록 내부적으로 조정이 되어 있다는 예제가 나온다. (정작 해당 함수는 다른 값에 대해서는 ~3 ulp까지의 오차를 보였다나.) 나는 이것이 문제의 본질을 흐려버리기 때문에 나쁜 선례라고 생각하고, 더 나아가서는 Numeric#round에 추가 인자를 넣은 것 자체가 문제가 있지 않나 싶은데 뭐 루비는 국제 표준(...)이 되었으니 어쩔 수 없을 지도 모르겠다.
내가 들었던 반론 중 하나는 사람이 계산 과정을 따라가도록 로직을 설계해야 하는 경우 (예: 게임에서 사용되는 숫자) 반올림이 중간 중간에 강제로 들어가야 할 수 있다는 것이었다. 틀린 말은 아닌데 나라면 부동소숫점 안 쓰고 소숫점 n자리까지 나타낸 고정소숫점 형식을 쓸 것이다. ↩︎
Unit in the Last Place, 해당 숫자에서 가장 가까운 다른 부동소숫점 숫자 사이의 거리를 나타내는 (상대) 단위. (두 거리가 다를 수 있는데 일단 같은 경우만 생각하자.) "올바르게" 반올림되었을 때 계산의 최대 오차는 ±0.5 ulp, 오차 범위의 크기는 1 ulp가 된다. ↩︎
1 note · View note
arachneng · 7 years
Text
요전에 한겨레에서 기사를 하나 본 뒤 어이가 ��어져서 간만에 긴 글을 써서 기자한테 보냈더니 사람들의 반응이 꽤 좋았다. 왜 기사가 어이가 없는지에 대해서는 저 글을 참고하기로 하고, 여기에서는 언론 그 자체에 대해서 쓴 소리를 해 보기로 한다.
글을 읽어 본 사람들은 눈치챘을 지도 모르지만 사실 내가 짜증난 건 네이버 때문이 아니라 (네이버는 내가 안 쓰기도 하고 욕 좀 먹으면 고칠 가능성이 높으니까…) 한겨레 때문이었다. 좀 더 구체적으로는, 내 글은 “너네 글 쓰고 검수하고 하는 사람은 몇 명 안 되지만 그걸로 영향받는 사람들은 수천 수만명인데 좀 책임감이라는 게 있어야 하지 않겠니 그래도?”를 아주 정중하게 쓴 것에 가깝다. 여기에 대한 기자의 반응은 “우리가 틀려도 공론화가 되면 우리의 책임을 다하는 것이라 생각한다”1에 가까웠는데, 개인적으로는 이 반응이 더 어이가 없었다. 아 물론 JTBC가 태블릿 입수해서 공개하는 것 같이 공론화 그 자체로 언론의 역할이 되는 경우도 있는데… 그런 건 보통 특종이라고 부른다. 사실 자체만으로 사회를 움직일 수 있다는 확신이 생기는 그런 보도는 많지 않다.
좀 더 구체적으로 말하면, 내 사견으로 언론이 제공하는 컨텐츠는 크게 세 종류가 있다. 하나는 단순 정보의 제공이다. 이를테면 리빙 포인트 같은 것. 물론 이 역할은 언론의 근본적인 역할이라기보다는 언론이 가장 쉽게 접할 수 있는 활자 매체였던 시절에 하던 역할이 지금까지 내려 오는 것에 가까우며, 지금 와서 단순 정보만 제공하는 언론은 살아남기 힘들 것이다. 두번째는 공적으로 알려지는 것이 마땅하다고 여겨질 사건에 대해서 자료를 수집하거나 취재원을 찾아 다니는 것, 즉 사건의 탐사이다. 마지막으로 앞의 두 컨텐츠를 가공하여 독자에게 제공하는 과정에서 그것이 무엇을 의미하는지 관점을 제시하는 것이다. 이상의 세 종류의 컨텐츠는 분리되어 제공되기도 하고 적절히 섞여서 제공되기도 하는데, 해당 기사에서 한겨레는 네이버의 입장을 복붙해 버리면서 사건의 탐사에도 관점의 제시에도 실패했다. 그나마 어디처럼 HTTPS가 국제 표준이 아니라는 미친 소리는 하지 않아서 다행이지.
한겨레를 살짝 변호해 주면, 뭐 이게 다른 나라라고 상태가 좋은 건 아니지만, 특히 한국 언론은 광고주에 지나치게 휘둘릴 정도로 재정 상태가 좋지 않으며, 재정 상태가 안 좋을수록 (기자 수와 능력에 크게 의존하는) 사건의 탐사가 취약한 경우가 많다. 그러니 무슨 관점을 제시하고 싶어도 잘 될 턱이 없는 것. 이게 변호인진 잘 모르겠다… 하지만 이건 한겨레 사정이고 최근의 퀄리티 저하는 한겨레가 한걸레(…)라는 멸칭으로 불릴 정도로 심각한 상황인데, 여기에 위기의식을 가질 망정 우리가 공론화를 하면 오피니언을 이끌 수 있다는 안일한 자세가 드러나는 반응을 보니 뒷골이 좀 많이 땡겼다.
내가 이 건에서는 한겨레가 관련되어 있어서 한겨레를 깠지만, 사실 이런 시각이 한겨레에만 국한되어 있지는 않을 거라고 생각한다. 기실 한국에서 정말로 탐사보도를 제대로 할 수 있을 정도로 자본도 있고 거기에 호응하는 대중도 많은 언론은 JTBC 밖에 남지 않은 것 같고2, 여기에서 그런 시각을 가진다면 뭐 그럴 수 있겠다 싶지만 다른 데라면 아니올시다. 꽤 잘 나가는 신규 인터넷 언론들을 보면 이런 점을 인식하여 컨텐츠 특화를 많이 하는데, 이를테면 뉴스타파 같이 사건 탐사에 올인을 하거나, 슬로우뉴스 같이 자원이 많이 드는 사건 탐사를 피하고 오피니언에 집중하는 등의 접근을 볼 수 있다. 더 이상 오피니언 리더가 될 수 없다는 걸 인식했으면 이런 시도라도 해야 하는 거 아닐까? 아니면 옛날에 내가 제안했듯 매체를 최대한 활용해 보시거나.
주의. 나는 답장을 받긴 했으나 이 답장을 공개해도 된다는 허락을 미리 구하지도 않았고 필요성도 느끼지 않아서 답장을 공개하지 않는다. 즉 앞의 따옴표에 쓰여진 내용은 이 답장에 대한 나의 해석으로, 내가 제대로 답장을 해석한 것인지, 또는 내가 의도적으로 답장의 내용을 왜곡하는 것인지에 대해서 나를 믿어서는 안 된다! 뭐 그렇다고. ↩︎
물론 JTBC가 나쁘다는 건 아니지만 이건 매우 안 좋은 상황이다. 왜냐하면 어느 언론도 실수를 피할 수는 없기 때문이다. JTBC가 둘 중 하나를 깎아 먹을 정도의 실수를 해 버리면 백업 솔루션이 없다. 모두가 대안 언론을 보고 있진 않을 거 아닌가? ↩︎
3 notes · View notes
arachneng · 7 years
Text
안티바이러스
한국에서는 모 제품의 영향으로 백신이라고도 많이 부르는 안티바이러스(AV) 소프트웨어는 옛날부터 필수적인 것으로 인식되어 왔다. 컴퓨터는 일반 목적의 컴퓨팅1 기기이니만큼, 자칫 실수하면 내가 원하지 않는 계산을 할 수도 있는데 AV는 이 상황을 해결하기 위한 방법 중 하나이다. 그런데 그것을 아시는가? 대부분의 AV 소프트웨어가 생각보다 보안적으로 취약하다는 것을? 농담같아 보인다면 해커뉴스에서 작년에 나온 AV 뉴스만 모아서 보자. 노턴 안티바이러스가 사정 없이 뚫린 것이 아마도 가장 대표적인 사건일텐데, 기사화만 덜 되었지 거의 모든 주요 AV 소프트웨어에서 강력한 취약점이 하나 쯤은 튀어 나왔다. 도대체 무슨 일이 일어난 걸까?
컴퓨터는 기본적으로는 결정론적으로 돌아가고, (외부의 사건 등을 모두 포함해서) 입력이 고정되면 출력이 똑같이 나온다는 것이 가장 큰 특징이다. 그래서 컴퓨터에서 문제가 생길 경우 그건 컴퓨터(하드웨어)가 잘못 해서 그런 게 아니고 거기에 있던 소프트웨어가 잘못 했거나 모니터와 의자 사이에 있는 사람이 잘못해서 그런 것이다. 그리고 문제를 미리 막냐(방어)와 문제가 터졌을 때 확산을 막냐(감지)로 다시 세분화하면, 여섯 가지 상황이 가능하다.
시스템이 문제여서 방어
시스템이 문제여서 감지
소프트웨어가 문제여서 방어
소프트웨어가 문제여서 감지
���람이 문제여서 방어
사람이 문제여서 감지
그래서 뭔 말을 하고 싶냐면, 원래의 AV는 4번과 6번 밖에 감지할 수 없다. 그것도 한동안은 “알려진” 악성코드만을 패턴으로 감지할 수 있었는데(signature-based detection), 당연히 패턴을 계속 바꾸는 악성코드가 등장하자 AV의 접근은 i) 일단 알려진 악성코드는 빠르게 갱신해서 감지해 내고(뒤에서 못 걸러냈다고 손을 놓고 있을 수만은 없으니) ii) 그게 안 되면 차선으로 이상한 징후를 감지해 보자는 것으로 바뀌게 된다. 이른바 AV의 보안 솔루션화를 통해 2번을 추가적으로 감지하겠다는 전략인데, 문제는 보안 솔루션은 시스템에 속한다는 것이다. 즉 AV에 보안 취약점이 있으면 일반 소프트웨어처럼 그 소프트웨어로 문제가 국한되는 것이 아니라 전체 시스템에 영향을 미친다.
더 큰 문제는 일반적으로 시스템으로 받아들여지는 운영체제 벤더들이나 웹 브라우저 벤더들은 보안적으로 매우 높은 기준선을 가지고 있으나(물론 하루 아침에 이렇게 된 건 아니고 오랫동안 뚫리면서 기준이 올라갔다), AV 벤더들은 이런 기준선을 적용받지 않는다는 점이다. 이를테면 노턴 안티바이러스 사건의 경우 실행 파일 압축을 풀기 위해서 시스템 수준에서 압축을 푸는 코드를 가지고 있었는데, 이 코드가 잘못되었을 때 어떤 방어 장치도 없었기 때문에 취약점이 발견되자 바로 시스템이 뚫려 버렸다. 비교를 해 보면, 요즘 웹 브라우저 벤더들은 똑같은 종류의 코드(이를테면 글꼴 포맷이나 이미지 포맷 등)를 샌드박스에 집어 넣고 돌리는데, 이런 류의 코드에 버그가 없다는 걸 보장하기에는 복잡도가 크기 때문에 차라리 이상한 일이 일어나도 피해를 최소화하는 게 낫다는 걸 깨달았기 때문이다. 이런 샌드박스 접근이나 요즘 모질라가 전폭적으로 밀고 있는 “언어 기반” 보안 같은 접근은 비용은 차치하고라도 상당한 효과가 있다고 알려져 있으나 AV 벤더들은 이런 걸 시도해 본다는 낌새조차 없었다. 혹자는 AV가 보안 문제들을 원천적으로 막는 데 인센티브2 자체가 없어서 이런 사태가 벌어지지 않냐 하는 추측을 하기도 한다.
하나 더 언급해야 하는 것은, AV는 원치 않는 계산을 막는 유일한 방법이 아니라는 것이다(애초에 AV는 방어를 하기도 어렵다). 3번 시나리오를 막는 가장 확실한 방법은 버그가 없거나 적은 소프트웨어를 짜는 것이고, 4번 시나리오는 이른바 방어적 프로그래밍이라고 하는 전략이나, 컴파일 시점에서 방어 코드를 삽입하는 전략으로 흔히 구현된다. 1번 시나리오는 가장 높은 권한으로 실행되는 코드(“커널”)를 최대한 줄이는 전략을 생각해 볼 수 있고, 2번 시나리오는 용도별로 계정을 나누는 등의 정책이 해당된다. 가장 어려운 것은 사람이 관여되는 5와 6번 시나리오로, 교육을 통해 애초에 문제가 될만한 짓을 하지 않는 것이 5번, 그리고 문제가 되는 상황에서 지속적으로 상기시켜 주는 것이 6번에 해당할 것이다(아시다시피 완벽하진 않다). 이를 다시 정리하면,
시스템과 소프트웨어가 충분히 안전하고, 그걸 쓰는 사람이 충분한 보안 의식을 가지고 있다면, AV는 불필요하다.
물론 이는 전건 두 개가 완벽하게 만족될 때만 성립하는 명제이다. 현실에서는 시스템도 소프트웨어도 그 정도로 안전하지는 않으며(특히 제로 데이 공격의 존재는 위협 감지의 필요성을 배가시켜 준다), 사람은… 말을 말자. 그렇기 때문에 현실적으로는 가벼운 AV가 있는 것 자체는 감수해야 할 비용이다(윈도의 경우 Windows Defender가 이 역할을 하고 있다). 하지만 AV가 이 모든 상황을 해결시켜 줄 거라는 믿음은, 어, 말 그대로, 믿음—즉 미신이다. 개인 단위에서 AV보다 더 중요한 것은 소프트웨어 업데이트(1번 및 3번 시나리오를 강화시키기 위하여)와 보안 의식(5번 및 6번 시나리오를 차단하기 위하여)이다. 그리고 운영체제들이 점차 방어 우선 전략으로 가면 ��수록—우리는 iOS가 이걸 얼마나 성공적으로 수행하는지(와 그에 수반되는 개발자들의 비용)를 봐 왔다—더욱 더 중요해지는 것은 후자가 될 것이다. 보안은 공짜가 아니다.
우리가 그다지 인식하지 않을 뿐 “계산”(computation)은 매우 본질적인 개념으로, 우리가 머릿속에서 하는 모든 생각들도 계산이요, 우주의 입자들이 상호작용하는 것도 계산의 일종으로 볼 수 있다. 단지 우리가 계산이라고 하면 숫자로 하는 극히 단순한 계산을 가리키는 경우가 많아서 컴퓨팅이라는 미번역어를 잠깐 썼을 뿐. ↩︎
소비자 대상 AV 시장이 거진 무료화되면서, AV 벤더들의 주 수입원은 기업을 대상으로 한 보안 솔루션으로 바뀌었다. (소비자 시장은 여기에 써 먹기 위한 악성코드 데이터베이스 수집 등에 대신 쓰인다.) 그리고 아시다시피 소비자들은 뉴스에 민감하지만 기업은 뉴스가 있어도 계약 때문에 다른 벤더로 갈아 타기가 어렵다… 어차피 다 고만고만한 것도 한 이유 ↩︎
3 notes · View notes
arachneng · 7 years
Link
그러니까, 옛날에 이런 정신나간 크로스워드 퍼즐1을 보고 와 미쳤네ㅋㅋㅋ 하고 실제로는 풀지 않고 넘어간 적이 있었다. 이걸 푸는 데 얼마나 시간이 걸릴지 조금만 해 보고 짐작할 수 있었기 때문이다. 근데 어제 누군가가 풀려고 하고 있길래... 낚여 버렸다.
처음에는 PDF 파일 위에다 텍스트를 올려 놓아서 편집을 하다가, 실수로 잘못 건드는 경우를 도저히 참을 수 없어서 (그리고 한 번 백트래킹을 할 뻔 해서...) 아예 각을 잡고 풀이 과정을 텍스트로 써 놓고 보니 왠지 공개하고 싶어졌다. 이 접근은 옛날에 스도쿠 퍼즐을 인간이 푸는 방법대로 푸는 프로그램을 본 적이 있어서 떠올린 것인데, 물론 적절한 알고리즘2을 쓰면 풀리기야 하겠지만... 원래 퍼즐이라는 건 자동으로 풀면 재미가 없는 것이니까. 그런 고로 뭐 이런 그지 깽깽이 같은 퍼즐을 어떻게 풀어 라고 생각한다면 스포일러를 당하고 더 풀지 않도록 하기로 하자.
MIT Mystery Hunt 2013 문제 중 하나였다고 한다. ↩︎
대강 예상하기로는 정규식의 상태 기계를 역으로 추적할 수 있는 게 있으면 그냥 백트래킹으로 풀릴 듯 하다. 결국 손으로 풀 때도 현재 시점에서 올 수 있는 문자의 집합을 구하는 게 병목이었기 때문에. 이렇게 보면 어려운 스도쿠보다는 쉬운 문제일지도? (하지만 단서가 6방향으로 쓰여 있어서 목을 돌리는 게 힘들었다...) ↩︎
1 note · View note
arachneng · 7 years
Text
압축 알고리즘 르네상스 (2)
이전 글에서 예고했듯, 앞에서 코딩의 발전에 대해서 얘기했다면 여기에서는 모델링과 기타 알고리즘의 근황을 다루기로 한다. 아마 이전 글보다는 덜 기술적…일까?
모델링의 발전(?)
위에 물음표가 붙어 있는 이유는 사실 코딩에서의 발전과는 달리 모델링에서는 엄밀한 의미에서 발전이 계속 일어나고 있었기 때문이다. 당연하게도 사람들은 다들 코딩은 풀린 (또는 건들기 어려운) 문제라고 생각하고 있었으니 더 쉬워 보이는 모델링을 계속 건들어 보는 수 밖에… 그러므로 여기에서는 코딩 부분을 제외하고 요즘 뜨고 있는 포맷들이 무슨 접근을 취했는지를 다뤄 본다.
Zstandard: LZ77을 최대한 우려 먹어 보자
Zstandard의 파일 포맷은 생각 이상으로 간단하다. 얼마나 간단하냐 하면 앞에서 꽤 길게 설명했던 FSE의 구현 디테일이 뒤에 포함되어 있다(이해가 어려운 거지 tANS는 구현 자체는 복잡하지 않다). FSE라는 중대한 개선을 이루어 낸 탓인지(?) 모델링 부분에서는 좀 미묘한 모양새이다.
LZ77은 DEFLATE가 써 먹은 걸로도 유명한 모델링 방법인데, 기본적으로는 데이터에서 중복이 있으면 중복되는 부분을 (복사할 위치, 복사할 길이)로 별도로 구분1하는 방법이다. 복사가 끝나는 위치가 꼭 현재 위치보다 앞이어야 한다는 법은 없어서, ABC까지 쓰고 복사할 위치를 B 앞으로, 복사할 길이를 3이라고 주면 BCB가 붙는다. 따라서 반복 길이 부호화의 완벽한 상위 호환이기도 하다. LZ77은 반복을 추출하는 매우 좋은 방법인 대신에, 완전하게 반복으로 나타나지 않는 패턴(1씩 올라간다거나…)을 잡는 데는 상당히 취약하기 때문에 현대 압축 알고리즘들은 보완하는 방법을 여럿 고안해 내었는데, Zstandard에서 LZ77만 쓴 건 좀 희한하다 싶다(성능 문제가 아닐려나 싶음). 다행히도(?) 코딩 방법은 확연히 달라서, (앞에서 복사할 위치, 앞에서 복사할 길이, 그대로 복사할 바이트열 크기) 세 개가 항상 붙어 다니고 셋이 서로 다른 확률 분포를 사용한다. 복사할 길이에 최근 사용한 3개까지의 길이를 되풀이하는 게 있다거나(“repcode” 모델링이라고 한다) 하는 건 요즘은 흔한 얘기고.
FSE는 허프만 코딩과 마찬가지로 확률 분포를 미리 어딘가에 써 넣어야 한다는 단점 아닌 단점이 있는데, DEFLATE에서 이중 허프만 코드(…)로 테이블을 코딩했다면 Zstandard는 그런 거 없이 그냥 비교적 간단한 비트 코드를 사용한다. 최근에 쓴 값을 반복한다거나 하는 기본적인 RLE는 구현되어 있다만… 아마도 테이블 크기가 DEFLATE보다는 훨씬 작고(DEFLATE는 두 종류의 부호가 섞여 있는 구조라 부호가 280여개까지 들어갈 수 있다), 그래서 큰 영향을 주지 않을 거라고 판단한 모양이다. 반대로 Zstandard가 FSE의 장점을 유감 없이 써 먹는 분야도 있는데, 앞에서 ANS는 여러 코드가 한 비트스트림에 함께 달려들어도 정상 동작한다고 했었다. 그래서 코더를 4개로 나눈 뒤에 매 단계마다 4바이트씩 읽어서 각 코더한테 1바이트씩 넘기면 서로 독립적이므로 수퍼스칼라를 100% 활용할 수 있다. 이 접근은 앞에서 링크한 ryg 씨의 정리에도 적혀 있는데 앞으로 ANS를 쓰는 아주 일반적인 방법이 될 듯 하다.
LZ77를 끝까지 우려 먹는 대신 Zstandard는 최종 사용자한테 상당히 중요한 인터페이스를 하나 노출하고 있는데, 바로 사용자 정의 사전이다. 즉 출력 없이 미리 지정된 데이터로 LZ77 등을 위한 준비를 해 준 뒤 실제 데이터를 압축하면 작은 데이터에서도 큰 압축률을 보여 줄 수 있다는 얘기. 사실은 이건 DEFLATE는 아니지만 zlib 압축 컨테이너에도 포함되어 있는 기능인데, 거짓말같이 아무도 쓰지 않는 기능이기도 했다. Zstandard의 진정한 공로는 이걸 사용하기 적절한 명령줄 인터페이스로 만들어 낸 게 아닐까 싶다.
Brotli: 코딩이 안 되면 모델링으로 승부
Zstandard 얘기를 많이 하긴 했는데 이건 코딩에서 큰 진전을 이루었기 때문이고 사실 모델링 면에서는 구글이 미묘하게 먼저 발표한 Brotli가 더 앞서 있다. 이미 WOFF2 등에서 그 성능을 입증했고, HTTP의 압축 코덱으로도 써 먹을 요량으로 RFC까지 등록되어 있다. Zstandard가 zlib을 어느 면에서도 바를 수 있어서 무조건 고려할 수 있는 대체제로 만들어졌다면, Brotli는 훨씬 넓은 범위에서 튜닝할 수 있는 괜찮은 압축 알고리즘으로 만들어졌다는 인상이다.
Brotli의 기본적인 모델링 방법은 물론 LZ77이다. 앞에서 말했지만 싼 값으로 완전 중복된 데이터를 뽑아 내는 데는 이만한 알고리즘도 없다. 일단 LZ77을 거치고 나서 실제로 코딩되는 단위는 (“블록 종류”, 앞에서 복사할 길이 + 그대로 복사할 길이, 그대로 복사할 데이터, 앞에서 복사할 위치)이다(“블록 종류”는 후술). 그런데 이에 그치지 않고 Brotli는 상당한 수준의 모델링 복잡도를 가지고 있는데, 이를테면…
LZ77와는 별개로 2차 맥락(order-2 context), 즉 그대로 복사되는 바이트를 코딩할 때 그 전에 코딩되었던(복사되었던 것도 포함) 두 개의 바이트에 따라 사용하는 확률 분포가 다르다. 좀 더 정확하게는 두 개의 바이트를 가지고 64개 중 하나의 확률 분포를 선택하는 구조. 이론적으로야 차수가 올라가고 데이터가 무한히 많으면 압축률이 올라가지만 실제로는 오버헤드가 커서 그대로 쓰기는 거시기한데, 이걸 맥락을 1/1024로 압축하여 쓸만하게 만들었다. Brotli가 WOFF2 같은 바이너리 포맷 압축에서 강점을 보이는 이유가 이 부분 때문이다(글쎄 Unison 같은 글꼴이 1/10로 준다니까?).
맥락을 압축한다고 했다. 근데 이 압축하는 함수라는 것이 어떤 건 UTF-8 텍스트에 특화되어 있고 어떤 건 바이너리 데이터에 특화되어 있고 그래서 하나만 줄창 쓸 수는 없다. 이 때문에 서로 다른 종류의 데이터를 한 스트림 안에서 다른 확률 분포로 코딩할 수 있도록 블록 전환 명령이 따로 있다. 블록 전환은 나머지 세 종류의 확률 분포마다 하나씩 존재하고 다른 것들과 별개로 전환할 수 있다. 압축하는 쪽이 투자를 무진장 한다면 엔트로피를 끝까지 뽑아 먹을 수도 있을 것이다(Brotli에는 11이라는 최대 압축 모드가 있는데 구현을 직접 보진 않았지만 전체 탐색을 하는 거 아닐까 싶다).
거대한 기본 사전과 매우 속셈이 뻔히 보이는 전처리 규칙들이 존재한다. 이 거대한 기본 사전은 전체 데이터와 사용자 정의 사전 길이보다도 더 앞에 있는 데이터를 복사해 달라고 요청할 경우 사용되며(이 경우 한정으로 LZ77 코드의 해석이 다르다), 극히 짧은 텍스트에 대해서 상당한 효과를 발휘한다. 심심하면 기본 사전을 직접 디코딩해 보자… 한자, 한글, 키릴 문자, 아랍어, HTML 태그, 자바스크립트 코드 조각(…) 등 생각할 수 있는 온갖 것이 들어 있다.
개인적으로 Brotli 명세를 보면서 복잡도가 미디어 파일 포맷 보는 것 같다는 느낌을 받았는데(물론 진짜 미디어 파일 포맷은 훠어얼씨이인 복잡하다), 굳이 변호하자면 원래 이런 류의 포맷은 자연어로 써 놓으면 더 복잡해 보이는 경향이 있다(…). (반대로 코드로 써 놓으면 필요 이상으로 쉬워 보인다.) 여하튼 Zstandard가 코딩에 집중했다면 Brotli는 모델링에 크게 집중했다는 인상으로, 서로 다른 트레이드오프를 가지고 있어서 당분간은 공존할 것으로 보인다.
앞으로는?
파레토 프론티어라는 개념이 있다. 요컨대 가능한 해법들의 성능과 비용을 2차원 평면에 그려 놓고, 각 해법 중 그 해법보다 성능이 좋은데 비용도 더 낮은 해법이 존재하지 않는 것들만 놓아 놓은 집합인 것. 파레토 프론티어는 트레이드오프가 존재할 수 있는 상황에서 어떤 해법들이 고려할만한 가치가 있는지 우리가 아는 “최전선”(frontier)을 그려 놓았다고 생각하면 된다.
압축 알고리즘을 실제로 제대로 평가하는 건 꽤 어렵고, 방법에 따라서는 파레토 프론티어도 달라질 수 있지만(당장 성능을 압축 속도로 놓느냐 압축 해제 속도로 놓느냐 압축률로 놓느냐로 세 종류가 나온다), 일단 Squash라는 꽤 잘 알려진 벤치마크 결과가 있으므로 이걸 사용하자. 압축 해제 속도에 집중해서 분석한 결과에 따르면, 현재 실질적인 파레토 프론티어는 네 부류로 구성되어 있다고 여겨진다.
개빠른 압축 알고리즘(압축 및 압축 해제 속도가 디스크 I/O를 상회): 전통적으로 LZ4는 여기에서 킹왕짱이었다. LZO도 강력한 경쟁자이긴 한데 파레토 프론티어를 완전히 밀어 버리진 못 했다.
시간을 더 투자할 경우 효과적인 알고리즘(압축 해제 속도가 네트워크 I/O를 상회): 전통적으로 LZMA/xz가 널리 쓰이는 접근이었는데, Brotli가 사실상 이 위치를 먹어 버린 것으로 보인다. 압축 해제 속도 그래프 윗쪽 가운데에 혼자서 놀고 있다.
존내 느리고 ���내 효과적인 알고리즘(그냥 다 느림): 이 글에서 전혀 다루진 않았지만, 사실 다른 거 다 필요 없고 압축률이 짱이라면 이 자리는 PAQ 계열 알고리즘들이 자리를 잡은지 오래이다. PAQ 계열 알고리즘들의 주된 특징은 맥락 혼합(CM)이라 하여 여러 개의 괜찮은 확률 모델로부터 더 좋은 확률 모델을 만들어 내는 기법에 있는데, 당연히 확률 모델을 여러 개 돌려야 하니 오지게 느려진다(빨라야 MB당 초 단위). 요즘은 신경망도 쓴다고 그러더라. 어쨌든 이런 알고리즘을 필요로 하는 경우는 별로 없을테지만 있다는 것만 알아 두자.
그냥 적절한 알고리즘: 사실 이게 가장 애매한 부분인데… 일단 Zstandard가 이 부류를 개척하려고 만들어진 것은 틀림이 없으나, 묘하게도 압축 해제 속도에서는 파레토 프론티어를 밀어 버리지 못 했다(zlib -9보다 압축률이 낮다!!!). 대신 압축 속도와 압축-해제 속도 비율에서는 파레토 프론티어를 크게 개선한 걸 볼 수 있다. 압축 해제 속도만 좀 더 개선된다면 그냥 이 부류에서는 맞수가 없을 것으로 보인다.
확실해진 것은 지난 두 세 해 사이에 나온 알고리즘들이 2와 4를 크게 개선했다는 점인데, 다르게 말하면 이 부분이 개선이 더 쉬울 수도 있고 모두가 더 나은 알고리즘을 원했던 부류였을 지도 모르겠다. Zstandard의 개발자가 페이스북에 입사하고, 구글이 Brotli를 만드는 걸 보면 어쨌든 저기서는 수요가 있다는 얘기지 않겠는가. 앞으로의 상황을 예견하긴 어렵겠지만(무엇보다 둘이서 파레토 프론티어를 너무 많이 올려 놓아서…), 압축 알고리즘이 다시금 수면 위로 떠올랐으니만큼 그대로 다시 가라앉지는 않을 듯 싶다.
다루지 않은 것들
내가 깊게 살펴 볼 시간이 없어서(이 글 쓰는 데만 25시간 걸렸다! 내가 미쳤구나!), 또는 내가 자세히 알진 못 하나 얘기는 많이 나온다고 알고 있는 것들을 마저 소개하고자 한다.
비손실 압축 알고리즘 얘기를 하다 보면 흔히 나오는 또 다른 대안으로 Oodle 라이브러리가 있다. 왠지 본 것 같다면 착각이 아니다. 아까 전에 몇 번 언급되었던 그 압축 알고리즘 전문가들이 있는 회사에서 만든 라이브러리이다(이 회사는 원래도 Bink라는 동영상 코덱을 만든 바가 있다). Oodle에는 무려 4종의, 플랫폼 별로 최적화된 압축 알고리즘이 들어 있으며, 주장에 따르면 무려 모든 알고리즘이 파레토 프론티어를 깬다는 얘기까지 들리고 있지만 굳이 여기에서는 다루지 않았는데, 그 이유는 i) 일단 오픈소스가 아니니 세부 사항을 알 도리가 없으며 ii) 주 설계자인 Bloom과 ryg 모두 개발 과정에서 나온 일반적인 관측을 공개하는 데 인색하지 않아서 기법 자체는 비교적 평준화되어 있기 때문이다. 물론 기법이 공개되어 있다고 해서 Oodle에 들어간 엔지니어링 노력이 헛된 건 아니겠지만, 애초에 추가적으로 설명할 내용 자체가 별로 없을 것이다. (참고로 ryg의 언급을 토대로 하면 rANS가 여기 저기에서 쓰이고 있다는 모양이다.)
손실 압축 알고리즘에서도 지켜 볼만한 포맷들이 근년간에 여럿 나왔다. 재밌는 점은 비손실 압축 알고리즘에서 트레이드오프의 기준이 완전히 바뀌어 버렸듯, 손실 압축 알고리즘에서도 새로 나오는 경쟁자들이 트레이드오프를 바꾸려고 시도하는 경우가 많다는 점이다. 하나 하나 살펴 보자.
이미지 압축은 비손실 압축과 손실 압축의 수요가 함께 있었고 둘이 따로 놀고 있었다. 비손실 압축은 DEFLATE와 비슷하게 PNG가 사실상의 표준으로 자리 잡은 상태이다(PNG는 전처리 과정을 제외하면 압축 알고리즘은 그냥 DEFLATE이다). 손실 압축에서는 모두가 알고 있는 JPEG에서 시작해서, 사용하는 시계열-주파수 변환 방법을 바꾸어 선택적으로 비손실 압축이 가능한 JPEG XR가 등장한 뒤, 동영상 압축에 쓰는 키프레임 압축을 그대로 이미지로 가져 온다는 어이가 상실되는 접근2을 통해 WebP와 BPG가 등장한 상태였다. 그러다가 최근에 손실 압축과 비손실 압축의 장점만을 취하겠다고 FLIF가 새로이 등장한 상태. FLIF은 대강 살펴 본 바가 있는데, 앞에만 짜르면 손실 압축이 된다거나 하는 기능 자체는 훌륭하나 이걸로 인코더를 빠르게 만들 수 있는가를 잘 모르겠다(MANIAC이라고 맥락을 더 복잡하게 학습할 수 있는 적응형 산술 코딩을 쓰는데 맥락 학습이 매우 비쌀 것 같은 기분). 다시 말하지만 기능 자체는 훌륭하기 때문에 구현체만 빨라지면 가능성은 열려 있다고 본다.
동영상 압축은 보통 손실 압축이고, 크게는 키프레임 압축(근본적으로 이미지 압축과 크게 다르지 않다)과 보간 정보 압축으로 나눌 수 있다. 나는 동영상 압축은 솔직히 잘 몰라서(VP9 명세 다 읽어 보고 이해를 10%도 하지 못 함) 무엇이 크게 발전했는진 잘 모르겠으나, 최근 나온 것 중 유명한 것으로는 화려한 데모 페이지로 유명한 실험 코덱 Daala가 있었고, 현재는 업계 지원으로 AOMedia Video 1(AV1)이라는 오픈소스 코덱이 개발되고 있다(Daala 개발자들도 여기로 흡수되었다). WebM이 압축 알고리즘의 성능과는 상관 없이 구글의 푸시만으로 매우 성공했듯, AV1도 업계 지원이 뒷받침되고 있으므로 아마 성공하지 않을까 싶다.
음성 압축은 비손실 압축과 손실 압축으로 나뉘는데, 비손실 압축은 FLAC이 충분히 빠르고 충분히 압축이 되며 어차피 소장 목적이 주라 더 압축할 필요가 없다(?!)는 이유로 그렇게 부각되지는 않는 편이며, 손실 압축은 다시 디코딩 지연(latency)이 용납되는 분야와 그렇지 않은 분야로 나뉜다. 디코딩 지연은 보통 음질에 반비례하기 때문에, 전자는 일반 음성 코덱으로 후자는 음성 통신용 코덱으로 포지셔닝하는 경우가 많았는데, 이걸 동시에 잡겠다는 Opus 코덱이 근년간에 표준화되었다. (동영상에서도 보이고 음성에서도 xiph.org가 계속 보이는 게 참 고생한다.) 음성 코덱은 하드웨어 지원이 없으면 잘 퍼지지 않는다고 알려져 있는데, Opus의 품질 자체는 뛰어나지만 이것 때문에 잘 정착되지 못 할 위험성은 아직 많이 남아 있는 편이다.
여기서 별도로 구분한다는 건 같은 스트림에서 다른 부호로 코딩하는 것일 수도 있고, 스트림을 나누거나 모드를 달리 하여 별도의 부호 집합을 두는 걸수도 있다. DEFLATE의 경우 복사할 길이는 다른 문자들과 같이 코딩하고 복사할 위치는 복사할 길이 직후에 완전히 다른 확률 분포를 둬서 따로 코딩한다. ↩︎
근데 어이가 상실되는 것 치고는 너무 잘 동작했다는 게 함정. 동영상의 키프레임 코딩은 압축 후 데이터의 대부분을 차지하기 때문에 개선을 할 동기가 강하며, 키프레임을 너무 느리게 디코딩하면 프레임이 드랍되므로 성능도 어느 정도 받쳐 줘야 한다는 점이 이 접근을 가능케 했다. 다만 몇 차례 지적되었듯 동영상 압축에서 사용하는 후처리 기법들(loop filter 등)이 이미지에 쓰면 별로 괜찮지 못 한 경우도 있어서 어디까지나 가능하다는 수준. ↩︎
3 notes · View notes
arachneng · 7 years
Text
압축 알고리즘 르네상스 (1)
커다란 데이터를 그 구조를 살펴서 정확히 같거나(비손실) 거의 같은 결과(손실)로 복구할 수 있도록 데이터를 줄이는 걸 압축 알고리즘이라고 한다. 압축 알고리즘은 데이터의 크기를 줄일 수 있는 대신 그걸 압축하거나 압축 해제하는 데 연산을 소모하기 때문에 항상 트레이드오프가 따라 붙었고, 그래서 ZIP 포맷의 유명세와 함께 DEFLATE가 “대강 대충 쓸만한 적절한 알고리즘”의 자리를 먹어 버린 뒤로는, 특히 지난 십수년간은 더 나은 압축 알고리즘이라는 건 연산 비용을 쓰면서 데이터를 줄일 만한 이유가 있을 때나 고려해 보는 선택지에 불과했다. 그러던 것이 재작년에서 작년 사이에 갑자기 크게 바뀌었다. 모두가 압축 알고리즘에 대해서 관심을 가지고 모두가 그 얘기를 한 번씩은 하고 있다. 아마도 가장 상징적인 사건은 작년에 페이스북이 압축 속도나 압축률이나 모두 DEFLATE를 상회한다는 Zstandard라는 것을 내 놓은 것이리라.
도대체 왜 갑자기 압축 알고리즘이 다시 주목을 받게 되었는가? 아마도 짐작할 수 있는 이유로는 공짜 점심이 끝났기 때문일 것이다. 컴퓨팅 기술은 2000년대까지만 해도 착실히 지수적으로 증가해 왔고, 덕분에 우리는 30년 전보다 100배 가벼운데 1000배 빠른 CPU에 1000배 많은 데이터를 담을 수 있는 스마트폰으로 플래피 버드를 플레이할 수 있게 되었다. 플래피 버드 드립은 농담하는 것이 아닌게 연산력이 올라가면 그동안 시도하기 어려웠던 다양한 소프트웨어 개발 기술을 써 볼 수 있게 되기 때문이다(30년 전에 쓰레기 수집[GC] 같은 것을 아무 데서나 써 먹기는 쉽지 않았을 것이다). 근데 물론 연산력은 계속 올라가고는 있지만 성장세도 둔화되었을 뿐만 아니라 ���무 것도 안 하면 빨라지는 그런 성장은 이제 끝났다는 게 문제이다. 멀티코어도, GPU 연산도, 그리고 모바일 네트워크도 성능이 올라가긴 하는데 그 과실을 먹으려면 뭔가를 해야 하는 시대가 온 것이다. 그러하니 지금껏 경시되어 왔던 분야에 사람들이 다시 눈을 들이기 시작하고, 그 중에 압축 알고리즘이 있는 셈이다.
지난 십수년간 압축 알고리즘 동네에서는 무슨 일이 일어났는가? 그걸 알려면 압축 알고리즘이 실제로 어떻게 구성되고, 어느 부분이 공략 가능하며, 무슨 발견이 일어났는지를 살펴 봐야 한다. 그래서 이 글과 다음에 따를 글에서는 압축 알고리즘의 큰 구성 요소를 둘로 나눠서 각각의 근황을 살펴 보기로 한다.
압축 알고리즘의 구성
종종 압축 알고리즘의 큰 갈래인 비손실 압축과 손실 압축은 완전히 다르게 구현된다고 생각하는 사람들이 있다. 그도 그럴 것이, 손실 압축을 구현하려면 뭔가 복잡한 MDCT라고 하는 것도 구현해야 하는 것 같아 보이고 어셈블리도 짜 봐야 할 것 같고 그런데 비손실 압축은 그런 건 없어 보이기 때문이다. 아니다. 손실 압축과 비손실 압축을 막론하고 모든 압축 알고리즘은 크게 둘로 구성되어 있다.
모델링: 데이터에서 중복되어 있는 부분을 감지하고 그 중복을 (물론 복구 가능한 형태로) 해소한다.
코딩: 그래도 남아 있는 데이터를 최소 크기의 비트열로 부호화한다.
한 가지 중요한 점은, 압축 알고리즘은 일단은 출력은 비트열이긴 한데, 모델링의 결과로 나오는 것은 (적어도 개념적으로는) 비트열이 아니라는 점이다. 대신 부호화할 때 써 먹을 수 있도록 한정된 갯수의 부호와, 현재 시점에서 가능한 부호들의 확률이 튀어 나온다(이를테면 다음 문자로 A, B, C가 나올 확률이 5:2:3이고, 다음 문자는 B라고 알려 주는 식이다). 반대로 압축을 해제할 때는 코딩이 먼저 일어나는데, 당연하게도 매 시점마다 확률 정보가 알려져 있어야 할 것이다(이를테면 지금까지 나온 정보를 가지고 5:2:3이라는 확률을 알아 낼 수 없다면 말짱 황인 것이다).
최대 압축률을 기준으로, 코딩은 풀린 문제이다. 적어도 우리는 이 결과를 클로드 섀넌이 1948년에 소스 코딩 정리를 발표했을 때부터 알고 있었다. (같은 해에 섀넌은 채널 코딩 정리를 발표했고, 그렇게 섀넌은 정보 이론의 창시자가 되었다.) 오늘날 우리가 사용하는 코딩 체계는 딱 두 가지 밖에 없는데1, 모든 부호를 비트 단위로 코딩한다는 전제 하에 최적인 허프만 코딩과, 이론적으로 가능한 압축률에 거의 무제한적으로 근접하는 산술 코딩이 그것이다. 산술 코딩이 더 좋은데 허프만 코딩이 그동안 널리 쓰였던 이유는 순전히 산술 코딩이 느렸기 때문이며, 후술할 놀라운 알고리즘이 등장하기 전까지는 이 차이를 메꾸기는 어렵다고 생각되고 있었다.
모델링은 풀릴 수 없는 문제이다. 좀 더 정확히는 아무 데이터 집합을 던져 주고 최적의 모델링을 찾아 낼 수 있는지 알아내는 건 인공지능 문제이기도 하고 이게 최적인지 아닌지는 아예 계산이 불가능하다. 전자를 단적으로 보여 주는 예는 (마침 5만유로짜리 상금이 걸려 있는) 자연어 데이터이고, 후자는 (이제 상금이 만료된) 키 유도를 통해 만들어진 임의 길이의 데이터를 생각하면 되겠다. 그동안 비손실 압축보다 손실 압축 쪽에서 가시적으로 새 알고리즘이 많이 등장했던 이유는, 솔직히 말하면 손실 압축의 주 대상인 영상이나 음성 같은 건 딱 보면 더럽게 중복되는 데이터가 많아 보이지 않는가?(…) 비손실 압축에서도 중복되는 데이터를 많이 찾을 수는 있지만 그걸 빠르게 찾아내는 건 어려워 보이는 반면, 손실 압축에서는 그냥 눈에 보이고 빠르게 될 법한 다양한 가능성이 있어서 다 해 보고 되는 방향으로 가면 된다는 강력한 잇점이 있다.
모델링은 다양한 접근 방법이 존재하는 데다 성능과 압축률의 역상관관계도 잘 알려져 있어서 이리 저리 건들어 볼 노브가 많다. 예를 들어서 옛날에 쓴 글 중에는 JPEG의 모델링 노브 중 하나가 어떻게 이루어져 있고, 그게 어떤 예상치 못한 품질 차이를 만들어내는 지를 설명한 적이 있다. 코딩은 상대적으로 이런 여지가 적었는데, 그래도 각 접근 별로는 최적화가 이루어져 있어서 연산을 더 해도 되면 산술 코딩, 덜 해야 하면 (또는 1993년 이전까지는, 특허를 피하려면) 허프만 코딩, 그조차도 할 수 없으면 Snappy 같이 그냥 데이터를 건들지 않고 복사만 하는 식의 방법이 표준처럼 여겨졌다. 그러던 것이 하루 아침에 뒤집혔다.
코딩의 발전: 비대칭 기수법
비대칭 기수법(ANS, asymmetric numeral system)이라고 하는 아주 특이한 코딩 방법은 arXiv에 발표된 논문에 자세히 기술되어 있다. 2007년에 처음 나와서 몇 차례 개선된 이 논문은 솔직히 말하면 꽤 읽기가 어렵다. 왜 읽기가 어려운지는 그냥 읽어 보면 안다(…). 일단 코딩 방법을 서술하는 방법 자체가 특이하고, 그걸 기술하는데 쓴 변수명이 혼란스러우며(도대체 l과 ls가 왜 다른 변수지?), 그냥 전반적으로 개념도 바로 와닿지 않는다. 그래도 압축 알고리즘을 연구해 본 여러 존잘들께서 사람들이 이런 저런 훌륭한 요약을 해 주어서 ANS 논문을 직접 읽을 필요까지는 없는 듯.
여기에서는 비대칭 기수법이 (아무래도 한국어로 쓰여진 자료는 없는 듯 하여) 무엇인지를 최대한 노력하여 간단하게(…) 쓰며, 기술적인 내용에 관심이 없다면 FSE를 다루는 여기로 바로 건너 뛰면 되겠다. 앞으로 [a, b)라 함은 a 이상 b 미만의 범위를 나타내며, 나눗셈 a/b는 항상 정수 나눗셈을 가리킨다.
산술 코딩
ANS는 산술 코딩의 특이한 구현 방법이니만큼 산술 코딩을 먼저 알아 보기로 하자. 산술 코딩에서는 입력 비트열이 [0, 1) 범위의 거대한 소수 x라고 가정하고 앞에서부터 코딩 단위 크기에 맞춰서 잘라 낸다. 이를테면 각 비트가 나타날 확률이 반반이라고 할 때 “ABC”는 0x414243 나누기 224, 즉 0.2549173235… 쯤 하는 숫자일테고, 코딩 단위가 0~99까지의 십진법 두 자리라면 25, 49, 17, 33 순으로 코딩되는 것이다. 여기서 중요한 점은, 사전순으로 다음 데이터인 “ABD”는 0.25491738… 쯤 하기 때문에 마지막 단위가 33이든 37이든 상관이 없다는 것이다. 따라서 25, 49, 17, 33이라고 코딩된 데이터는 실제로는 [0.25491733, 0.25491734)라는 범위를 나타내는 것으로 해석된다. 이 점은 산술 코딩을 직관적으로 이해하는 데 무진장 중요하다.
x는 보통 거대하기 때문에 x를 바로 만들 수는 없다. 그러므로 x를 포함하는 게 확실한 [a, b)라는 범위를 가지고 있는데, x가 [0, 1) 범위였으니 a와 b도 소숫점 단위일 것이므로(…) 컴퓨터 친화적으로 만들기 위해 적절히 곱해서 정수로 만든다. 즉, 실제로 다루는 데이터는 [p, q) = [(a - d) / s, (b - d) / s)이고 d와 s는 암묵적이다. 매 부호를 인코딩할 때마다 현재 확률을 보고 p와 q를 갱신2하는데, 이를테면 처음 범위가 [0, 10000)이었을 때 5:2:3 확률에서 두번째 부호가 선택된다면 다음 범위는 [5000, 7000)이 된다. 이제 만약 p와 q에서 코딩 단위 기준으로 맨 앞부분이 같다면… 예를 들어 비트 단위로 출력한다면 첫 비트 0은 [0, 5000)을 나타내고 1은 [5000, 10000)을 가리키는데 새 범위가 후자에 완전히 포함되므로, 1을 출력한 뒤에 p와 q를 1이 원래 가리킬 범위가 [0, 10000)이 되도록 재정규화한다. 그럼 새 범위는 [0, 4000)이 될 것이고, 또 앞자리가 0으로 같으므로 재재정규화를 하면 [0, 8000)이 되는데, 이제 앞자리가 다르니 다음 부호를 처리하게 된다. 압축 해제는 이걸 정 반대로 하면 된다.
이렇게 써 놓으면 구현하기 쉬워 보일 것 같은데, 실제로는 온갖 함정이 도사리고 있다. 일단 실제로 코드를 짜 보면 이상하리만치 1씩 오류를 내거나 오버플로를 내기 쉽고 (이진 검색이 절대 구현하기 쉬운 알고리즘이 아닌 것과 비슷한 이유), 범위가 재수 없게 걸리면 [4999, 5001) 같이 첫 자리는 여전히 다른데 범위가 더 작아지지 않아서 무한 루프에 빠지는 경우도 발생하며, 범위를 갱신하는 방법을 잘못 잡으면3 지금까지 뱉었던 코딩된 비트열을 재처리해야 하는 불상사까지 생기는데 다 어떻게든 처리해야 한다. 성능을 위해서라면 더더욱 위와 같이 하면 안 되는데, 물론 당연하게도 여기에도 그냥 보면 최적화가 될 것 같은데 실제로는 안 되는 경우가 많다(산술 코딩 구현은 대부분 재정규화 루프에서 브랜치 예측이 안 되어서 성능이 깎인다). 압축 알고리즘 만드는 걸로 먹고 산다는(!) Charles Bloom 씨가 자기 블로그에서 맨날 하던 얘기가 코더를 어떻게 최적화하는지 이리 저리 궁리한 결과를 써 놓는 거였다. 후술할 ANS가 나오기 전까지만 해도 말이다. 이제는 ANS의 최적화에 대해서 얘기하고 있다
ANS와 rANS
ANS의 아주 기본적인 아이디어는 x를 소수가 아니라 커다란 정수로 생각하는 것이다. 산술 코딩에서는 x를 일단 [0, 1) 범위에 있다고 가정하고 부호를 읽어 가며 x의 다음 자리를 찾아 내려고 하는데, ANS 체계에서 인코딩은 들어 오는 부호로부터 커다란 x를 점진적으로 만들어 나가는 거고, 디코딩은 그렇게 만들어진 x로부터 다시 부호를 복원하는 것이다. 그런데 이렇게 하면 필연적으로 x는 스택과 같은 형태가 되어, 인코딩 방향과 디코딩 방향이 반대가 되어 버린다(!). 마찬가지로 압축 알고리즘(을 비롯해 다양한 것)으로 먹고 산다는 Fabian Giesen 씨(ryg)4의 얘기로는 이게 그다지 큰 문제는 아니라고 한다. 왜냐하면 어차피 대부분의 압축 컨테이너는 압축이 어려운 데이터를 바로 집어 넣을 수 있도록 일정 크기 단위로 블록을 나누기 때문이라고. 그러므로 일정 갯수만큼 부호와 그 확률을 저장해 놓은 뒤, 반대 방향으로 작업하면서 인코딩을 하고, 디코딩할 때는 정상적인 방향으로 압축을 풀게 된다.
이제 x가 무한히 크다고 가정하고 앞에서 나온 5:2:3 확률에서 두번째 부호를 인코딩하는 상황을 생각해 보자. (앞에서 얘기했듯 이 시점에서 x는 그 부호 앞에 있는 게 아니라 그 뒤에 있는 부호들을 가리킨다.) 확률의 공통 분모가 10이므로, 스택과 같은 형태로 인코딩하려면 10x + s이라고 인코딩하고 x mod 10을 가지고 디코딩하는 게 맞겠으나, 확률이 같은 상황이 아니기 때문에 두번째 부호의 경우 s=5와 s=6 모두 가능한 상황이 된다. 이제 x의 모든 가능한 값을 이 두 가지 경우에 우겨 넣는다. 말하자면 x가 짝수였다면 s=5로, 홀수였다면 s=6으로 넣은 뒤에, 10으로 나눈 몫에는 x/2를 넣으면 일대일 대응이 될 것이다. 마찬가지로 다른 부호에 대한 상황까지 채워 넣으면 이런 결과를 얻게 된다.
인코딩시,
첫번째 부호를 인코딩할 경우 새 x mod 10은 [0, 5) 범위에 들어가야 한다. 따라서 새 x는 10(x/5) + (x mod 5)가 된다.
두번째 부호를 인코딩할 경우 새 x mod 10은 [5, 7) 범위에 들어가야 한다. 따라서 새 x는 10(x/2) + (x mod 2) + 5가 된다.
세번째 부호를 인코딩할 경우 새 x mod 10은 [7, 10) 범위에 들어가야 한다. 따라서 새 x는 10(x/3) + (x mod 3) + 7이 된다.
디코딩시,
x mod 10이 [0, 5) 범위이면 첫번째 부호이다. x mod 10의 값은 인코딩할 때의 x mod 5로부터 결정되었으므로, 새 x는 5(x/10) + (x mod 10)이 된다.
x mod 10이 [5, 7) 범위이면 두번째 부호이다. x mod 10의 값은 인코딩할 때의 x mod 2로부터 결정되었으므로, 새 x는 2(x/10) + (x mod 10 - 5)가 된다.
x mod 10이 [7, 10) 범위이면 세번째 부호이다. x mod 10의 값은 인코딩할 때의 x mod 3으로부터 결정되었으므로, 새 x는 3(x/10) + (x mod 10 - 7)이 된다.
이걸 일반화하여 [s/m, t/m)이라는 범위에 속하는 부호를 인코딩하려면 (x/(t-s))m + (s + x mod (t-s))라고 하면 된다. 이렇게 하면 디코딩할 때는 반대로 x mod m을 계산하여 해당하는 부호를 먼저 알아낸 뒤, 대응하는 s와 t를 받아서 x를 (x/m)(t-s) + (x mod m - s)로 갱신하면 된다. 잘 생각하면 인코딩은 mod (t-s)를 mod m으로 우겨 넣는 과정이고 디코딩은 반대로 우겨 넣는 과정이라고 볼 수 있다.
물론 x가 정말로 무한히 클 수는 없으니 실제로는 산술 코딩과 비슷하게 재정규화를 해 줘야 하는데, ANS에서는 재정규화된 최종 범위가 코딩 단위의 크기가 b일 때 [L, bL)이라는 이상한(?) 범위를 지닌다. 재정규화 자체는 그냥 다음과 같이 간단하다.
인코딩시 x가 bL 이상이면 부호를 더 읽어서 x mod b를 내보내고 x를 x/b로 갱신한다. 조건을 만족할 때까지 반복.
디코딩시 x가 L보다 작으면 데이터 y를 더 읽어서 x를 xb + y로 갱신한다. 조건을 만족할 때까지 반복.
(인코딩시 x가 L보다 작아지거나, 디코딩시 x가 bL 이상이 되는 경우는 없다는 것 자체는 위의 인코딩/디코딩 식으로부터 쉽게 증명할 수 있다.) 왜 이런 이상한 범위를 쓰는가? ANS 논문에서는 b-유일성이라는 표현을 쓰는데, x가 이미 [L, bL) 범위 안에 들어 있었다면 m > 0이 b의 배수일 때 xm이나 x/m은 절대로 [L, bL)에 다시 들어 오지 않는다. (보통 b나 m이나 2의 거듭제곱인 경우가 많으니 큰 문제는 안 된다.) 따라서 x가 어떤 값이더라도 b진법으로 아랫자리를 더하거나 빼면 유한한 횟수 안에 재정규화가 종료되고 그 방법은 유일하다. 끝. L은 이 조건을 만족하는 충분히 큰 값이기만 하면 된다(ryg 씨의 구현에서는 223이다).
오버플로를 막으려면 재정규화를 인코딩 전에 해야 하는데, 재정규화가 필요한지 여부는 쉽게 판단할 수 있으므로5 최종적으로는 b와 m이 모두 비트 연산으로 퉁칠 수 있다고 칠 때 부호 하나 인코딩하는데 곱셈이 딱 한 번 필요하다! 산술 코딩에서는 범위를 직접 표현하느라 p와 q 모두에 곱셈 연산을 해야 한다는 걸 생각하면 발상의 전환으로 꽤 값진 이득을 얻어낸 셈이다. 게다가 범위를 다루느라 1 차이로 고생하거나 범위가 무한정 작아지는 경우를 다룰 필요도 없으며, 다른 코더에서는 다양한 삽질이 필요했던 여러 코더나 압축 안 된 데이터를 같은 스트림에 집어 넣는 게 쉽게 가능한 등, 이렇게 정리해 놓고 보니 ryg 씨가 왜 여기에 주목하고 있었는지 이해가 간다. 아, 이 섹션에서의 내용은 앞 링크의 내용을 상당수 참조해서 작성했음을 뒤늦게 밝혀 둔다.
FSE는 이상의 온갖 복잡한 디테일을 사실상 맨 처음으로 닥돌한 끝에 만들어낸 실용적인 tANS 구현이다. 이걸 만든 Yann Collet 씨는 안 그래도 원래 LZ4라는 매우 빠르고 적절한 결과를 내 놓는 압축 알고리즘으로 유명했던 사람인데, 산술 코딩을 어떻게 하면 더 빠르게 만들어 볼 수 없을까 하고 고민하다가 ANS가 자기가 생각하던 거랑 상당히 유사하다는 걸 알아 낸 뒤, 안 그래도 다들 이해하지 못하던 ANS 논문을 정리해서 2013년 말에 (앞에서 언급했던 전문가들도 중요성을 인지하지 못 하고 있던 시점에) FSE를 만들어서 공개해 버린 것이다! (tANS 자체는 FSE와 함께 개발되었는데 이 사람 왈 그 전에는 Matt Mahoney 말고는 이해하는 사람이 없었다 카더라…) FSE는 zlib을 압축률/압축속도/해제속도 세 분야에서 모두 “따위”로 만들어 버리며 그 동안 오랫동안 큰 발전이 없었던 코딩 분야에 파란을 일으키고 만다.
차회 예고
글이 더럽게 길어졌으므로 여기까지 읽으시는 분들도 참 대단하십니다 모델링과 기타 알고리즘의 근황에 대해서는 내일 올라올(갱신: 올라 왔다!) 다음 글로 넘기도록 한다. 커밍쑨.
뻥이다. 좀 더 정확히는 각 부호의 확률 분포에 대해서 잘 몰라서, 그 분포를 앞에 나온 정보를 통해서만 알 수 있을 경우에 쓸 수 있는 코딩 체계가 두 개 밖에 안 쓰인다는 얘기다(이런 것들을 엔트로피 코더라고 하는데, 사실 얘네 말고 다른 대안이 별로 없는 게 함정). 만약 각 부호가 지수적 분포를 따른다거나 하는 정보가 있다면 특수한 코딩 체계를 쓰면 되는데, 당연하게도 확률 분포를 수동으로 주면 이 두 체계로 얼마든지 근사시킬 수 있다. 물론 그럴 경우 분포를 별도로 코딩해서 보내 줘야 하므로, 확률 분포가 확실한 경우(음성 코덱 같은 데서 흔하다)에 굳이 이럴 필요까진 없다. ↩︎
이미 알아 차렸을 수도 있겠지만 대부분의 확률에서는 p와 q가 정수로 딱 떨어지게 나오지 않는다. 이 경우 인코딩과 디코딩 과정에서 똑같은 방법으로 정수로 만드는데 보통 버림이 빠르므로 버림을 한다. 산술 코딩이 이론상 최적에 “가깝다”는 건 이렇게 정수로 만드는 과정에서 오차가 발생하기 때문인데, p/q가 32비트만 되어도 어지간해서는 그 오차가 몇 만분의 일을 다투므로 별로 신경쓸 필요가 없다. ↩︎
위에서 설명했던 내용은 범위 코딩이라 하여 이런 문제가 존재하지 않는 산술 코딩의 변형을 썼기 때문에 해당 사항이 없는데, 좀 더 일반적인 산술 코딩의 경우 p ≤ q라는 제약 없이 최상위 비트가 같을 때마다 재정규화하도록 되어 있기 때문에 재수 없으면 01111까지 뱉어 놓고 나서 이걸 10000으로 변경해야 하는 상황이 생길 수 있다. 범위 코딩은 이 문제를 해결하는 댓가로 1비트 정밀도를 희생한다. ↩︎
사실은 Charles Bloom 씨와 같은 회사에 있고, 어쩌면 Farbrausch라는 걸출한 단체로 더 유명할지도 모르겠다(64KB 실행파일로 당시로서는 상상도 못한 데모를 만들어낸 것이 특히 유명하다). 이 아저씨가 압축 알고리즘에 대해서 다룬 10년 전 슬라이드는 지금까지도 여전히 유효하며 이해하기 쉬우니 꼭 볼 것. ↩︎
인코딩시 (x/(t-s))m + (s + x mod (t-s)) ≥ bL일 때 재정규화가 필요한 건데, m이 b의 배수이고 우리 모두 s + x mod (t-s) 8, L=24이다), 가능한 모든 bL개의 x에 대하여 무슨 부호들이 출력되고 그 다음 재정규화된 x가 무엇인지를 테이블에 박으면 된다. 디코딩의 경우 재정규화는 디코딩 이후에 일어나므로 재정규화가 k차례 일어날 경우 재정규화를 반영해 x에 bk를 미리 곱해 놓으면 된다. tANS의 주 복잡도는 rANS에서는 x에 따라 테이블이 달라지(는 효과가 나)겠지만, tANS에서는 아니기 때문에 테이블을 채울 때 각 부호 별로 사용하는 x 갯수 뿐만 아니라 그 위치도 적절하게 채워 놓지 않으면 작은 bL 때문에 나타나는 오차가 크게 증폭될 수 있다는 점이다. 여기에 대해서는 여러 가지 그럭 저럭 쓸만한 경험적 방법이 제시되어 있고, 잘들 쓰는 걸 보면 이상한 상황만 피하면 심하게 문제가 되는 경우는 없는 것 같다. ↩︎
3 notes · View notes
arachneng · 7 years
Text
교차성
가끔 좀 진지한 얘기를 하다 보면 내가 말하려는 개념이 일반적으로 있을 것 같아서 이를 나타내는 낱말을 찾아 보다가 못 찾아서 빙빙 돌려서 설명하는 경우가 제법 있는데, 최근 들어 알게 된 낱말 중에 하나로 “교차성(intersectionality)”이라는 것이 있다. 어떤 대상이 여러 특질을 가질 수 있다면 그 특질이 교차해서 생기는 세부 특질 또한 중요하다는 것. 집합론적으로 말하면 집합 A와 B가 있을 때 A와 B가 서로 소가 아니라면 당연히 교집합 A ∩ B가 존재하니 A ∩ B와 A ∩ BC, AC ∩ B를 서로 묶어서 생각하려는 것은 잘못된 얘기라는… 이렇게 써 놓으니 엄청나게 당연한 얘기 같아 보이지만 많이 까먹기도 하는 그런 것이다.
교차성이라는 낱말 자체가 사회학에서, 그것도 하필(?) 차별에 대한 연구에서 나온 이유는 아무래도 우리는 집단을 서로 소인 집합, 즉 분할(partition)으로 나누는 데에 익숙하지 서로 겹칠 수 있는 집합으로 나누는 데는 익숙하지 않다는 점이 크지 않았을까 싶다. 내가 저 낱말을 찾던 맥락 중 하나로는, 사람들은 어렴풋이 차별받는 집단에 대한 연민1을 가지고 있지만, 그 연민은 그 집단을 대표하는 이미지에 대한 것이지 집단 자체에 대한 것은 아니며, 그 이미지가 깨지면 사람들의 반응은 크게 바뀐다는 것이었다. 이를테면 현재 진행형인 유럽 난민 사태에서 난민에 대한 인식이 나빠진 이유 중 하나는 난민들이 작년 새해 벽두에 성폭행을 많이 일으켰다는 사건 때문인데, 거야 문화가 다른 사람들이 한 나라에 100만명이나 유입되면 일이 안 터질 리가 있나(…). 난민이 아니라 선진국 국가라도 그 정도 사람 수가 문화가 다른 나라에 짧은 시간 안에 유입되면 비슷한 규모의 사건이 안 터질 거라고 장담할 수 없다. 난민은 그 자체로 (사회에 섭동을 가져 오기 때문에) 다른 문제이기에 난민의 유입에 따라 예상되는 사회적 비용을 추산해서 정책에 반영하는 것이 옳지, 난민 그 자체가 옳지 않다고 난리가 나는 건 여러 의미에서 눈 뜨고 못 볼 일이다. 자, 이제 이 사태를 보고 “유럽인들은 멍청하게도 저런 걸 나눠서 생각하지 못 하네”라고 하는 사람들은 자신이 조선족에 대해서 어떻게 생각하는지 되짚어 보기로 하자. 눈에 보이는 사회 혼란만 안 일어났지 조선족에 대한 한국 사회의 취급은 한국 사람이 남 지적을 할만한 수준이 아니다.
교차성이라는 것은 사람들이 쉽게 바로 받아들이는 개념은 아니기 때문에 결국 이런 함정이 있으니까 조심해야 한다라는 걸 사람들이 다른 방법으로 깨달아야 하는데, 작년 내내 느낀 점은 역시 이걸 받아 들이는 것 자체가 근본적으로 불가능한 거 아닌가… 하는 쓰디쓴 결론이었다. 엄청나게 긴 시간이 지나 우리는 일부 집단을 사회에 반영시키는 데는 성공했지만 그건 각개격파이지 반영 자체가 자동화된 프로세스가 될 수는 없는 걸까 하는 비관적인 생각. 그나마 이런 생각을 나만 하고는 있지 않을 거라는 것이 위안거리인데, 2017년이 이미 쫌 지나긴 했지만 앞으로 어떻게 될지는 두고 봐야 겠다. 확증편향만 강해지는 일은 없었으면.
굳이 “연민”이라는 낱말을 쓴 이유는, 해당 집단이 사회에 충분히 반영되지 못한 상황에서 그걸 해소하려는 노력보다는 그냥 해당 집단을 불쌍한 또는 이상한 사람 취급하는 경향이 강하기 때문이다. 한국 사회에서 장애인을 어떻게 보는지 잘 생각해 보자. ↩︎
2 notes · View notes
arachneng · 7 years
Text
원래 오늘…은 자정이 지났으니 어제 쓰려고 한 글이 있었는데 회사 일이 좀 꼬여서 늦게 돌아온 데다가 황당한 소식을 하나 들어서 이걸 대신 쓰기로 했다. 황당한 소식이 뭐냐 하면… 이것이다. 이른바 리그베다위키 대 (구) 엔하위키 미러 간의 소송전이 2심에서 뒤집혔다는 소식. 여기에 대해 다루기 전에, 이 글은 기본적으로 이 기사에 언급된 판결 취지를 바탕으로 하고 있으므로 실제 판결문의 내용과 해석이 다를 수 있다는 점을 일단 참고하길 바란다.
메아리 저널에서는 리그베다위키(구 엔하위키)에 대해 2013년에 한 번, 2015년에 한 번 그리고 망한 뒤에(…) 한 번 더 다룬 적이 있다. 나의 기본 견해는 예나 지금이나 크게 달라지지 않았는데, 청동 씨가 사이트 운영 및 중재 과정에서 상당한 인적·물적 투자를 했음은 부정하지 않으나(그리고 그 이유 때문에 청동 씨에게 무제한적인 공격을 하지는 않으나), 그것이 저작권 및 저작권과 유사한 특성을 가지는 다른 권리를 보유할 정당성을 주진 않는다는 입장이다. 왜냐하면 사이트 운영 및 중재는 사용자가 많은 사이트의 동작을 위하여 필수 불가결한 작업이며, 사이트가 동작한다는 이유만으로 데이터베이스권1이 부여된다는 것은 상당히 납득이 가지 않는 결과이기 때문이다. 또한 저작권법에서 “데이터베이스제작자”의 정의는 그 데이터베이스를 체계적으로 구성하는 데 큰 역할을 한 사람이어야 하는데, 운영자 수가 극단적으로 적은 구조 때문에 리그베다위키의 결정 프로세스는 후기로 갈수록 전체 위키를 커버하지 못 했으며, 여기에 대한 불만이 여기저기서 터져 나왔으나 청동 씨는 사람을 충원하는 대신 이상한 인터넷 언론을 만들었다(…).2 이러한 상황이니 그의 위키에 대한 권리적 기여는 데이터베이스권보다는 무려 1만 수천 개에 달한다고 추정되는 편집저작물의 저작권자(중 하나)로 보는 것이 더 온당하다고 생각해 왔다. 1심에서 데이터베이스권을 인정하지 않고 대신 부정경쟁방지법에 의거하여 판결을 내린 것은 그래서 상당히 적절한, 기여를 인정하면서도 확대 해석을 피한 결정이었다고 생각했다.
그런데 이번 2심 결정은 매우 당혹스럽다. “체계와 카테고리, 항목 등을 설계했을 뿐만 아니라 인적·물적으로 상당한 투자를 했고 체계적 검색 기능도 도입했다”라니, 그가 설계했다는 계층적 체계(특히 첫 페이지에 있는 그것)와 카테고리는 나무위키에서 모조리 갈아 엎어졌다. 단지 리그베다위키에 대한 반감에서 생겨난 것 뿐만이 아니라, 애당초 현행 나무위키의 카테고리 구조는 리그베다의 그것과는 완전히 다른, 굳이 비교하자면 미디어위키에 가까운 형태로, 이는 위키 기여자들이 그가 만든 체계를 좋아서 쓴 게 아니라 그냥 존재하기 때문에 또는 바꾸는데 품이 들기 때문에 사용했다는 것을 방증하며 사용자 참여만 가능했으면 얼마든지 바뀌었을 것임을 함의한다. “통일되고 짜임새 있는 목차 구조와 페이지 작성 양식 등을 만들었다”라고? 그런 걸 만들었다고 치면 왜 수십만개의 문서 중 대부분은 문서의 통일성보다는 리그베다위키스러운 표현에 더 집착하고 있는 것인지? 현재 알려진 정보만 봐도, 나는 여러 의미에서 법원이 위키 사이트의 운영과 중재를 다른 사이트의 그것보다 더욱 더 어려운 것으로 평가하고 있는 (그래서 이를 “상당한 투자3”로 인정한) 것이 아닌가 의심스럽다.
이 판결이 대단히 우려스러운 것은, 청동 씨의 의도는 어쨌든간에 웹사이트가 유지될 수준의 운영을 통해서 데이터베이스권을 획득할 수 있다는 전무후무한 결론이 유도될 수 있는 길이 열렸다는 것이다. 이는 엔하위키 미러 같이 단순히 이를 복제하는 것으로 수익을 얻으려고 하는 회색지대에 가까운 사업자에게만 독이 되는 것이 아니라(차라리 그랬으면 좋으련만!), 커뮤니티가 사이트 구성원이 아닌 사이트 저작물을 통해 형성되는 위키 커뮤니티의 사이트 종속성을 강화시켜 사용자에게도 독이 될 것이다. 현재의 나무위키도 불만을 가진 사람들은 많이 있지만 이게 집단 불만으로 표출되지 않는 이유는, 적어도 나무위키는 리그베다위키 같이 저작물의 이동을 불가하게 하는 상황을 만들지 않을 거라는 믿음이 있고 현재까지는 좀 느리긴 해도 어떻게든 지켜지고 있다는 것 때문이다. 이 사건은 이제 3심으로 넘어갔고, 새삼스럽지도 않지만 이번에도 양자가 함께 상고를 한 상태인데 최종 판결에 따라서 상당한 파장을 불러 일으킬 거라 생각한다. 많은 사람들이 관심을 가지고 지켜 봐야 할 이유이다.
Sui generis database right. 기존의 저작권법이 커버하지 못 하는, 누구라도 ���정 기준을 가지고 체계적으로 만들 수는 있어서 창작물로 인정받진 않지만 그 과정에 상당한 인적·물적 자원이 소요되는 편집물을 위한 저작권의 마이너 카피. 예를 들어서 “해리 포터를 다룬 책들의 목록” 같은 것은 창작성은 없지만 이걸 제대로 만드는 건 꽤 많은 노력이 필요하기 때문에 (단순히 제목에서 해리 포터라는 이름을 찾는 것이 아니니까!) 데이터베이스권으로 인정받을 수 있다. 근데 리그베다위키 측의 주장은 결국 위키의 내용과는 별개로 그 색인이나 관련 문서의 나열은 데이터베이스권으로 인정받아야 한다는 것인데, 색인은 자동 색인이고 관련 문서의 나열은 개별 기여자들이 작성한 게 훨씬 많다(…). ↩︎
내가 첫 글을 썼던 2013년 즈음부터 이 문제의 사이트는 수면 아래에서 암약(?)하고 있었는데, 이는 저작권의 해석 문제로 쉽게 상업적 활동을 하기 어려운 위키는 현행 유지하고, 이와 연결된 별개의 인터넷 사이트를 상업적인 발판으로 삼으려는 시도였다고 본다. 물론 그가 금전적으로 여유롭지 않다면 이런 시도 자체를 나쁘게 생각할 이유는 없으나, 그렇다고 해서 위키가 잘 운영되지 않은 것을 부정할 수는 없을 것이다. ↩︎
위키 사이트의 운영이 얼마나 어려운 지에 대한 정량적인 평가의 부재도 포함하여. 2015년에 상당수 공개된 전말에 의하면 리그베다위키는 관리적인 측면에나 기술적인 측면에나 상당히 허술하기 짝이 없었으며, 파트타임으로 기술 지원이 한 명 붙고 나서야 상태가 안정되었다(달리 말하면 그 전에는 기술 지원이 상태를 호전시킬 것이라는 판단조차 못 한 것이다). ↩︎
4 notes · View notes
arachneng · 7 years
Text
지금은 위키백과에 어느 정도 회의적인 시각을 견지하고 있긴 하지만, 위키백과 자체는 인터넷 커뮤니티라는 엄청난 제한에도 불구하고 괜찮은 결과를 낸 프로젝트이긴 하다. 소싯적에 위키백과질을 한 사람은 알겠지만 위키백과에는 독자 연구 금지라는 정책이 있는데, 이른바 상식 수준에서 검증이 불가능한 모든 내용에 근본적으로 “믿을만한 출처”를 요구할 수 있게 하는 정책1이다. 이 정책의 필요성과 한계점에 대해서 새삼스레 여기에서 되풀이할 필요는 없을 것 같지만, 일부러 이걸 언급하는 이유는 난 자료의 분류에 대해서 이 정책에서 처음 배웠기 때문이다. 이 분류에 따르면 자료는 다음 세 가지로 나눌 수 있다.
1차 자료(primary source)는 정보와 매우 가까운 기록이나, 그러한 사람으로부터 유래한다. 큰 교통사고가 났다면 1차 자료에는 교통사고의 당사자나, 그 목격자, 경찰 기록, CCTV 등이 포함될 것이다. 1차 자료에도 당연히 그 정보의 해석이 들어갈 수는 있지만 편향을 배제할 수 없기 때문에 바로 쓰이지는 않는다(교통사고 당사자는 자기 책임을 당연히 부정하려 들 것이다).
2차 자료(secondary source)는 1차 자료를 일반화하거나 분석하고 해석한 결과이다. 교통사고의 예를 계속 이어가면 2차 자료에는 사건을 다루는 신문 기사 같은 것이 포함되겠다.
3차 자료(tertiary source)는 1차나 2차 자료를 요약하고 정리한 결과이다. 대부분의 백과사전 같은 것이 3차 자료에 포함된다. 3차 자료는 정보 그 자체를 얻기보다는 정보를 얻기 위해 어떤 문헌을 더 찾아 봐야 하는지에 더 많이 쓰인다. 만약 요약 정리한 결과에 추가적인 해석이 들어간다면 2차와 3차 자료 사이의 경계는 뚜렷하지 않을 수도 있다.
이상의 분류를 사용해서 독자 연구 금지 정책을 거칠게 요약하면 “공개 발표된 믿을만한 자료를 사용하되, 자료의 해석은 이미 2차 및 3차 자료로 발표된 것에 근거하여야 하며, 3차 자료는 자제”한다고 할 수 있다. 근데 보통 위키백과질을 하다 보면 이걸 아무 생각 없이 “믿을만한 주요 언론에 발표된 것”으로 줄여서 생각하는 경우가 있다. 믿을만한 주요 언론이 무슨 기준으로 정해지고, 언론에 일단 발표되면 문제가 없다는 것인지에 대한 성찰 없이 말이다. 이런 단순화는 두 가지 이유로 틀렸는데,
언론은 2차 자료의 주요 생산자이기도 하지만 3차 자료도 더럽게 많이 생산한다. 아니, 사실 지금같이 저널리즘의 상태가 좋지 않다면 2차 자료보다 3차 자료의 생산이 더 쉽고 빠르니 이걸 선호하게 된다. 요즘 미들 미디어라 불리며 열심히 세를 불리고 있는 새로운 형태의 언론은 아예 2차 자료를 완전히 건너 뛰고 3차 자료만 생산하기로 마음먹은 경우라고 할 수 있다.
그렇다고 언론이 1차 자료를 생산하지 않냐 하면 그럴 리가. 이는 보통 저널리즘의 주된 가치라고 생각하는 탐사 보도에서 여실하게 드러나는데, 탐사 보도를 통해 기자가 사건 당사자가 되는 경우가 적지 않게 있다. 최근의 예를 들자면 《시사IN》의 주진우 기자 같은 사람이 있을 것이다. 분명 이러한 사람은 언론에 필수 불가결하지만, 그렇게 생산된 자료가 2차 자료���고 하기에는 이미 기자가 너무 정보와 가까워져 버린 것이다.
이게 너무 길어 보이니 좀 더 간단하게 말하면 언론은 1차, 2차, 3차 자료를 모두 생산할 수 있는 매체이다. 즉, 언론에 올랐다는 것만으로 자료의 편향이 제거되고 자료의 해석이 의미가 있다고 단정할 수 없다! 어찌 보면 당연하지만, 이게 왜 당연한 지는 자료의 성격을 나누어 분석해야만 이해할 수 있다는 점이 내가 이 분류법에서 배운 가장 큰 깨달음이었던 것 같다.
어디까지나 요구할 수 있다는 거지 처음 작성시부터 요구하는 것이 아니다. 무엇이 의심할 만하며 따라서 출처를 요구할 수 있는지는 총의에 따른다. 물론 노련한 편집자는 처음 문서를 작성할 때부터 출처가 필요할 만한 경우를 인지하여 준비하곤 하지만. ↩︎
0 notes
arachneng · 7 years
Text
맞춤법
거대한 HTML 명세를 오랫동안 홀로 편집했던 걸로 유명한 (지금은 몇 명이 더 붙었다) Ian Hixie의 웹사이트를 예전에 둘러 보다가 Hixie English라는 페이지를 본 적이 있다. 이른바, 남들이 영어를 어떻게 쓰건간에 나는 이런 식으로 영어를 쓰겠다라고 선언하는 글이다. 언어의 사회성을 중히 여기는 사람이라면 경악할만한 선언이지만, HTML 명세가 이상한 영어로 쓰여져 있지 않다는 점도 그렇고, 사실 영어에서는 이를테면 각 언론사나 각 대학 별로 영어의 맞춤법에 가깝다고 할 수 있는 존중할 만한 스타일 가이드가 있는 경우가 많으며 The Elements of Style 같이 그와는 별개로 만들어진 스타일 가이드도 있다. 상황이 이러하니 개인이 자신만의 규칙을 만드는 것이 흔한 일은 아니긴 하지만 딱히 못 할 일은 아니기도 하다. 이 시점에서 질문을 던져 보자. 무슨 장점이 있을까, 그리고 한국어에는 왜 그런 게 없을까?
뒤의 얘기부터 먼저 하자면, 영어는 이미 오래 전부터 (현대 철자법의 모태가 되었다고 평가받는 셰익스피어는 500년 전 사람이다!) 근대적인 표기로 쓰여진 언어이자, 그 글이 많은 사람에게 읽히는 환경이 오래 전부터 조성된 언어이다. 스타일 가이드가 글을 쓸 일이 많은 언론사나 대학에서 유래하는 게 괜한 이유가 아닌 것이다. 반면 한국어는 한글의 창제 과정에서도 알 수 있듯이 국가가 주도해서 글쓰기를 확립시킨 언어로, 물론 세종대왕의 한글 창제가 전혀 비판받을 일은 아니겠으나 실제 글 쓰는 이들의 필요성보다 국가의 권장과 교육이 더 큰 압력으로 작용한 언어라고 생각할 수 있다. 게다가 현대 한국어 표기에서 가장 특징적인 요소들은 모조리 1933년 한글 맞춤법 통일안에서 왔는데(어원을 밝혀 적고, 토씨를 제외하고 낱말 단위로 띄어쓰는 게 모두 여기에서 명문화되었다), 이렇게 따지자면 진정한 현대 한글 표기는 100년도 안 된 것이니 필요성이 생길 시간조차 부족했을 것이다.
이런 역사적인 배경을 감안하고 국가 주도의 맞춤법과 민간 주도의 스타일 가이드의 장단점을 생각해 보자. 대부분 한 쪽의 장점은 다른 쪽의 단점인 경우가 많으므로 함께 다루는 것이 좋겠다.
맞춤법은 하나 뿐이고 스타일 가이드는 여러 개이다. 맞춤법을 중히 여기는 사람들은 규칙이 하나 뿐이기 때문에 서로 다른 표준이 충돌하면서 생기는 혼란이 사라진다고 주장하는 반면, 그렇지 않은 사람들은 상황에 따라 적절한 세부 규칙이 필요할 수도 있는데 이를 모두 담을 수 있는 표준안은 존재할 수 없다고 주장한다. 세부 규칙 자체는 이미 수많은 전문 용어와 고도로 분화된 사회에서 필요성이 증명되므로, 즉 이 사안의 중점은 “세부 규칙을 표준화할 필요성”이 “여러 표준으로 인한 혼란”을 감수할 수 있을 만큼 중요하냐에 달렸다.
국가 주도의 맞춤법은 사투리나 서로 다른 계층의 언어 사용자를 고려하지 않는/못한다는 지적이 많다. 한국의 예를 살펴 보면, 한국은 한동안 중앙 정부에서 사투리를 의도적으로 배제해 왔으며, 그런 명시적인 배제가 없는 지금도 사투리에 대한 인식은 완전히 나아지지 않았다. (이를 단적으로 보여 주는 것이 다름 아닌 제주어일 것이다.) 반면 지역 갈등과 세대 갈등은 단순히 언어의 분화를 인정하는 수준으로 해결 할 수 있는 게 아니지 않느냐는 이유에서, 이것이 왜 맞춤법의 책임이느냐는 반론도 있다.
국가 주도의 맞춤법은 국가의 프로파간다를 담는 경우가 많다는 지적도 있다. 멀지 않은 예로 저기 조선1의 문화어는 하나부터 열까지 프로파간다 투성이이며, 그 정도로 대놓고 하지 않는다 하여도 한국어의 “우리나라”의 정의처럼 은연중에 프로파간다가 드러나는 경우도 종종 있다. 민간이 주도한다고 프로파간다가 사라지진 않겠지만 적어도 선택의 여지는 있지 않겠냐는 논리인 것이다.
국가 주도의 맞춤법이 실제 언중의 언어 생활을 너무 늦게 반영하는 경우가 많다. 이건 맞춤법이 촘촘할수록 더 심해지는 문제인데, 한국의 맞춤법의 경우 낱말의 정의를 사실상 《표준국어대사전》에 맡겨 버리는 바람에 낱말의 경계에 민감한 띄어쓰기가 갱신 잘 안 되는 사전에 매여 버리는 결과를 낳았다.2 짜장면 같이 언중 모두가 쓰는 낱말이 한동안 비표준어였던 경우도 있고, 비교적 변화가 빠른 낱말 말고 문법의 변화에서도 이런 현상이 종종 보인다. 그러나 “다르다”가 “틀리다”로 흡수되는 현상 같이 언중의 언어 생활이 항상 올바르다고 할 수만은 없기 때문에, 반영이 늦다는 것이 단점이 될 수도 있지만 장점이 될 수도 있다.
이상이 내가 가능하면 중립적으로 국가 주도의 맞춤법에 찬성하는 시각과 반대하는 시각을 정리한 내용이다. 여러분은 어떻게 생각하시는지? 다음은 위 논의에 대한 (엄밀히는 그 중에서도 한국어에 국한된) 나의 대답이다.
나는 한글 맞춤법의 필요성에 대해서는 인정하되, 국립국어원이 더 적극적으로 한글 맞춤법의 탈독재에 앞장서야 한다고 생각한다. 앞에서 말했듯 한국어에서 맞춤법이 본격적으로 필요해진 건 길게 잡아야 백 몇십년에 지나지 않으며, 이런 짧은 역사를 생각하면 국가가 주도하는 모델 자체를 부정할 수는 없다. 그러나 그 결과로 한글 맞춤법이 다른 가능성을 모조리 날려 버리면서 독재를 하게 되는 것은 올바르지 않다. 근년간에 국립국어원이 짜장면 같이 모든 언중이 쓰는 낱말을 마침내 복수 표준어로 인정하는 등 조금 완화된 분위기가 있긴 하지만, 한글 맞춤법이라는 표준의 존재 자체가 다른 표기안들을 찍어 누르는 경향이 있으니만큼 그 경향을 완화시키는 책임 또한 국립국어원에 있다고 생각한다. 당장 생각해 볼 수 있는 예로는, 한글 맞춤법에서 크게 비판받고 실제로 지키기도 어려운 규정들을 어느 정도 자율로 풀어서 세칙을 민간에게 맡기는 타협책이 있겠다. 띄어쓰기 같은 경우 아예 규정도 없으니 국립국어원이 자신들이 최종 권위로 유권해석을 하는데 이런 태도도 좀 줄여 줬으면 좋겠다.
반대로, 현행 맞춤법에 반대하는 사람이나 단체, 집단은 그만한 책임을 져야 한다고도 생각한다. 구체적으로, 어떠한 맞춤법이든 스타일 가이드든 최우선 원칙은 일관성이다. 규칙 안에서의 일관성 말고 일단 세워진 규칙이 일관되게 지켜져야 한다는 의미에서의 일관성 말이다. “띄어쓰기”(현행 표준)와 “띄어 쓰기”라는 두 표기가 있으면, 둘 중 하나를 선택해서 일관되게 표기하는 것은 괜찮다. 하지만 둘을 섞어 쓰는 것은 괜찮지 않다! 일관성을 위해서는 보통 그 선택을 명문화하는 과정이 필요할 것이니, Hixie가 그랬듯이 자신의 선택을 구체적으로 정하고 가능하면 다른 데서도 참고할 수 있도록 공개해 주면 참 좋지 않겠는가.3 궁극적으로 내 소망은, 국립국어원과 일부 국어학자들에 국한된 맞춤법 논의를 좀 더 다양한 집단이 독립적으로 할 수 있게 하여 국가 주도의 맞춤법에서 생기는 많은 문제들을 최대한 완화할 수 있었으면, 한다.
조선민주주의인민공화국 얘기다. 사실 이 낱말의 사용 자체가 프로파간다긴 하다(…). 조선이라는 체제에 대한 비판과는 별개로, 저 북녘 땅을 실효 지배하고 있는 집단이 스스로 주장하는 이름인 이상 도덕적으로 문제가 되지 않는 수준에서 인정은 해 줘야 하지 않겠느냐는 생각에서 되도록 이 약칭을 쓰려고 하고 있다. (도덕적인 문제가 있다고 판단되는 경우는 버마 같은 경우가 있겠다.) 설마 싶어서 부연하지만 이 명칭을 쓴다고 절대로 저 집단의 사상 등을 인정하는 것이 아니다. 당장 이 주석만 봐도 내가 “국가”라는 낱말을 안 쓰려고 얼마나 노력했는지 보이지 않는가. ↩︎
아주 엄밀히 말하면 맞춤법 조항 자체에는 사전에 대한 언급이 없다. 하지만 맞춤법의 한 부분인 외래어 표기법 또한 최종적으로 국립국어원의 유권해석을 따르는 이상, 맞춤법의 낱말의 정의 또한 국립국어원의 해석에 따른다고 보아 국립국어원이 편찬하는 사전을 기준으로 삼는 것이다. 사실 다른 사전이 있으면 그거라도 써 보겠는데 요즘은 다른 사전들이 다 멸망했지 ↩︎
나도 몇 개 있는데 여기에 대해서는 생각 나는 대로 글을 몇 개 더 써 보려고 한다. 아싸 저널 글감 굳었다 대표적인 것이 띄어쓰기에 대한 다른 접근(특히 합성어의 붙여쓰기에 대한 약한 부정)과, 성과 이름을 띄어쓰는 정책(당사자의 의견을 물을 수 있을 경우에만 한정), 그리고 일부 명사의 시각차에 의한 다른 표기 같은 것들이 있겠다. ↩︎
1 note · View note
arachneng · 7 years
Text
버전 번호
꼭 소프트웨어에만 붙어 있는 건 아니지만(웹 2.0이나 정부 3.0 같은 것도 있으니), 어지간한 소프트웨어에는 버전 번호가 붙어 있게 마련이다. 버전 번호의 필요성은 거의 모든 소프트웨어는 한 번 만들어지고 나서 어떤 식으로든 바뀐다는 사실에서 출발한다. 바뀌기 전의 소프트웨어를 쓰던 사람이 이게 어느 시점의 소프트웨어이고 무엇이 바뀌었는지 알려면 그 사실을 간단한 형태로 알려 주는… 그래, 버전 번호 같은 것이 있으면 좋지 않겠는가?
버전 번호는 소프트웨어 개발자가 사용자에게 알려 주는 중요한 정보이기 때문에 함부로 정할 것이 아니다. (자기 자신만 쓴다면 상관이 없겠지만…) 역사와 전통 덕분에 버전 번호를 정하는 체계가 한 두 가지가 아닌데, 여러 가지 특징과 장단점을 가지고 있어서 이걸 정하는 것도 일이다. 그러하니 여기에서는 중요한 체계들을 살펴 보면서, 어떻게 쓰이고 어떤 함의가 있는지를 알아 보기로 한다. 중요한 체계가 너무 많다고? 하하하, 저널 하루 놓쳤으니까 이만큼은 써야죠(…).
숫자 두 개
그러니까, 0.1, 1.0, 1.1, 2.0, 42.1 같이 숫자 두 개를 점으로 이은 형식이다. 뒤에서 나올 유의적 버저닝(semantic versioning) 때문에 숫자 세 개짜리 버전이 흥하기 전까지는 아마 가장 널리 쓰이는 형식이 아니었을까 싶고, 현실에서 소프트웨어도 아닌데 버전이 나오는 절대 태반은 이 형식을 따른다. 이해가 어렵지 않으면서도 왠지 버전 번호 같아 보이는 장점이 있나 보다.
숫자 두 개는 아무렇게나 정해지는 것이 아니고 앞의 숫자는 주(major) 버전, 뒤의 숫자는 부(minor) 버전이라고 부른다. 주 버전이 올라가면 부 버전은 (보통 0으로) 초기화된다. 버전이 9를 넘길 경우 숫자끼리 비교해야 하며, 따라서 1.9는 1.10보다 이전 버전이다. 일반적으로, 어디까지나 일반적으로1 숫자 두 개는 0 이상의 작은 정수이고, 특히 주 버전이 0인 경우는 첫 공식 버전이 나오기 전까지 개발용으로 쓰던 버전들을 나타낸다(그래서 0.x에서 안정적이지 않다면 개발자가 아직 1.0이 안 되었다고 변명할 레퍼토리가 생긴다). 종종, 버전 번호가 꼭 시간순을 나타내지 않는 경우도 있는데 이 경우 버전 번호가 낮은 쪽은 이후 버전에서의 중요한 변경점을 가져오는(backport) 경우가 있다.
사용자 입장에서, 주 버전과 부 버전의 분리는 소프트웨어에서 두 종류의 변화가 있을 거라고 알려 주는 역할을 한다. “중요한” 변화와 “덜 중요한” 변화를 말이다. 라이브러리의 경우 이것이 확장되어 주 버전이 바뀌면 공개 API가 깨지고 부 버전에서는 그렇지 않다는 비공식적인 관측도 존재했다. 근데 이것만 봐서는 무엇이 중요한지 무엇이 덜 중요한지를 쉽게 알 수 없는 문제가 있는데, 다음에 볼 유의적 버저닝이 바로 이 문제를 명시적으로 해결하려는 시도이다.
숫자 세 개
그러니까, 0.0.1, 0.1.0, 0.2.3, 1.0.0, 1.0.7, 1.15.0, 23.1.5 같이 숫자 세 개를 점으로 이은 형식이다. 뒤에 패치(patch) 버전이라 부르는 숫자가 하나 더 추가되어 숫자가 세 개라는 점만 제외하면 숫자 두 개짜리 버전과 동작은 완벽하게 같으며, 한동안 숫자 두 개짜리 버전과 혼용되어 사용되기도 했다(그러니까 원래 2.2, 2.3 식으로 가다가 패치 버전이 필요하다고 생각되면 2.3.1로 스위치하는 식이다). 그러나 후술할 표준이 많은 것을 바꿔 놓았다.
유의적 버저닝은 깃헙의 창립자 중 하나가 만든 버전 번호 체계이다. 혹시 TOML이라는 걸 알고 있다면 이것도 이 아저씨가 만들었는데, 옛날에 내가 TOML을 가멸차게 까며 다른 표준을 제시한 적이 있어서 사실 그에 대해서 아주 좋은 감정은 없다(…). 그러��� 유의적 버저닝만큼은 잘 만들었다고 생각하는데, 그 전까지 암묵적으로만 전해 내려 오던 버전 번호 관리에 아주 좋은 가이드라인을 명시적으로 제시했기 때문이다. 유의적 버저닝의 기본 규칙은 이렇다.
소프트웨어는 무엇이 “공개 API”인지 명시적으로 정의해야 한다. 공개 API라고 써 놓긴 했는데 뭐 별거 없고, 앞으로 무엇을 유지할지 선언하는 것이니 함수 이름만 공개 API든 사용자 인터페이스의 골자가 공개 API든 프로그램의 바이트 하나 하나가 공개 API든(…) 상관 없다. 다만 0으로 시작하는 버전은 이 API를 지킬 필요는 없다.
버전 번호는 항구적이다. 버전 번호를 잘못 매겼다 하더라도 그 버전이 잘못되었다고 선언해야지, 새 버전을 같은 버전 번호로 내 놓는다거나 하면 안 된다.2
주 버전은 공개 API가 바뀌어 하위 호환성이 깨질 때에만 올라간다3. 부 버전과 패치 버전은 0으로 초기화된다.
부 버전은 공개 API가 바뀌었지만 하위 호환성이 유지되거나, 앞으로 공개 API의 일부가 사라질(obsolete) 거라고 선언할 때 올라간다. 물론 꼭 공개 API가 안 바뀌었어도 버그 수정이 충분히 중요하다고 판단하면 올라갈 수 있다. 패치 버전은 0으로 초기화되고 주 버전은 유지된다.
패치 버전은 오로지 공개 API가 바뀌지 않은 채 사소한 버그 수정만이 있을 때 올라간다. 주 버전과 부 버전은 유지된다.
보시다시피 사실 이전에 모든 사람들이 (공개 API가 뭔지 선언하지 않은 것만 빼면) 다 하던 것들을 명문화한 것 뿐이다(…). -나 +로 시작하는 부분(각각 릴리스 전 버전과 빌드 정보를 나타냄)은 차라리 부차적인 것이다. 그러나 유의적 버저닝이 하도 널리 쓰이다 보니, 근년간에 일부 소프트웨어 패키지 매니저에서는 버전 번호를 집어 넣을 때 유의적 버저닝에 따라 숫자 세 개짜리 버전만 쓸 수 있게 하는 경우도 생겼다! (이를테면 Cargo가 이렇게 한다.) 앞으로도 더 많이 쓰이게 될 전망이다.
유의적 버저닝과는 별개로, 두번째 버전 번호를 특수하게 처리해서 짝수 버전이 사람들이 실제로 사용하라고 만든 것이고 홀수 버전이 개발용 버전으로 만들어지는 경우도 존재한다. (이를테면 일반 사용자 용으로는 2.4, 2.6, 2.8 순이고 개발용으로는 2.3, 2.5, 2.7 등을 써서 둘을 동시에 개발하는 것.) 그러나 이런 구분은 소프트웨어 발표가 까다로웠던 옛날에나 쓰였던 거고, 소프트웨어를 간단하게 릴리스할 수 있는 요즘에 들어서는 잘 쓰이지 않는다. 유의적 버저닝 체계에서는 개발용 버전은 항상 -를 쓴 릴리스 전 버전에 따라 버전을 매겨야 한다(이를테면 3.0.0의 개발 버전은 3.0.0-dev.1 등).
숫자 네 개
이 쯤 되면 더 설명할 필요는 없겠지. 숫자 네 개짜리 버전을 쓰는 대표적인 사례로는 상당수의 마이크로소프트 및 .NET 계열 소프트웨어와 3.0 이전 리눅스 커널이 있겠다. (리눅스 커널은 3.0.0부터는 숫자 세 개짜리 버전으로 옮겨 탔다. 그러나 유의적 버저닝을 쓰진 않는데, 후술.)
기술적으로만 말하면 숫자를 점으로 이은 버전들은 그냥 사전순으로 (다만, 숫자들끼리는 숫자들끼리 비교해서) 정렬하면 되기 때문에 숫자가 네 개가 아니라 다섯 개 열 개가 와도 딱히 문제는 없다. 다만 그걸 사용해야 하는 사람들이 써 먹을 수 없을 뿐이지(…). 그래서 현실적으로는 숫자 네 개짜리 버전이 최대 한도인데, 네번째 숫자의 의미는 사용하는 프로젝트마다 제각기 다르다(세번째 숫자보다 덜 중요하다는 것만 공통적이다). 마이크로소프트 등에서 사용하는 네번째 버전 번호는 빌드 번호로, 일단 명목상으로는 새 (내부 외부 가리지 않고!) 빌드가 나올 때마다 하나씩 올라가기 때문에 다른 버전 번호와는 달리 리셋되지 않는 경우가 더 많고 훨씬 숫자가 크다는 차이가 있다. 반면 리눅스의 네 자리 버전(2.6.27.62 등)은 그냥 역사적인 산물로, 부 버전이 거의 바뀌지 않는 상황이 된 시점에서 패치 버전이 실질적인 부 버전의 역할을 수행하는 바람에 네번째 숫자가 실제 패치 버전이 된 사례이다.
이상에서 볼 수 있듯 숫자 네 개짜리 버전은 그다지 큰 의미를 가지지 않는다. 아마도 유의적 버저닝에서 제시하는 것 같이 네 종류의 변경을 딱 부러지게 구분할 수 있는 게 아니라서 그럴 것이다. 그러므로 앞으로 소프트웨어를 발표할 때 버전 번호를 어떻게 정할지 모르겠으면 일단 유의적 버저닝을 고려하고 시작할 것.
소수
겉보기에는 숫자 두 개짜리 버전과 같지만, 두번째 숫자를 소수로 취급한다. 따라서 두번째 숫자가 0으로 시작할 수도 있으며4 2.005, 2.05, 2.5는 모두 다른 버전이다(나열한 순서대로 버전이 올라간다). 아까 전에 숫자 두 개나 세 개짜리가 많이 쓰인다고 했는데, 과거, 특히 도스나 윈도 등에서는 이게 더 많이 쓰인 경우도 있었다. 플랫폼과 독립적인 주요 소프트웨어 중에 이 버전 번호를 쓰는 사례로는 다름 아닌 TeX가 있으며, 도널드 커누스 옹이 자기 죽으면 버전 번호를 π로 만들겠다고 버전을 3.14159265 따위로 만들고 있다(…). 펄의 소수처럼 보이는 버전은 표기만 그러할 뿐 실질적으로 숫자 세 개짜리 버전임에 유의.
소수 버전의 가장 강력한 장점은 임의의 크기의 변화를 매길 수 있다는 점이다. 이른바 “0.05 정도의 변화”나 “0.2 정도의 변화” 같은 걸 두 종류 또는 세 종류의 변화로 묶지 않고 명시적인 숫자로 나타낼 수 있다는 것이다. 이 때문에 소수 버전에서는 정수 부분의 버전이 큰 의미를 가지지 않는 경우도 많으며, 정수 버전 n.00은 그냥 거쳐 가는 버전인 경우도 있다(아예 그런 버전이 안 나올 수도 있다). 뭐 현실적으로는 그래도 n.00을 기념하기 위해서 일부러 변화의 크기를 맞추는 경우도 많긴 하지만… 그리고 보통 사람들이 잘 아는 소수에 따라 해석할 수 있으므로, 보통 사람들에게 다른 종류의 버전 번호를 비교하는 방법을 가르칠 필요가 없다는 장점도 있다.
소수 버전의 단점은 정확히 유의적 버저닝의 장점이라 할 수 있다. 즉, 변화의 크기가 사용자에게 어떤 영향을 미치는지 사용자가 가늠할 수 없다. 거야 0.05 크기의 변화가 0.01 크기의 변화보다는 크고 0.1 크기의 변화보다는 작을텐데, 그래서 그게 얼마나 나에게 영향을 주는데?라고 물으면 대답할 정보가 없다. 어떤 사람은 0.1 크기의 변화 정도면 공개 API(그게 뭐든간에)를 깨도 된다고 생각할 수 있고 어떤 사람은 0.2, 어떤 사람은 0.03이면 된다고 생각할 수도 있는데 정해진 게 전혀 없다. 소수 버전을 쓸 경우 공개 API를 제시하기 어렵고 사용자가 보기에 모든 것이 연속적으로 바뀌는 것처럼 보이는 소프트웨어에서 쓰는 것이 가장 합당할 것이다.
날짜 기반
해당 버전이 발표된/발표될 날짜를 기반으로 버전 번호를 정한다. 여러 변종이 있는데, 가장 심하게는 한 해에 최대 한 개의 버전을 내기 때문에 연도(yyyy)만 달랑 쓰는 경우도 있으며, 연도와 월을 쓰거나(yy.mm, yyyy.mm, yyyy-mm, yyyymm 등), 연월일을 모두 쓰거나(yy.mm.dd, yyyy.mm.dd, yyyy-mm-dd, yyyymmdd 등), 연도와 연도 안에서의 순번을 쓰거나(yy.nn, yyyy.n 등) 등등 다양한 방법이 있다. 꼭 날짜를 눈에 보이게 쓰지 않아도 숫자 두 개나 세 개짜리 버전이 올라가는 방법이 실질적으로 날짜 기반인 경우도 많다.5
날짜 기반 버전은 이런 관측에서 유래한다. 어떤 종류의 소프트웨어는 그냥 지나치게 크기 때문에 공개 API를 설령 제시한다 하더라도 너무 작아서 의미를 가지지 못 하거나 너무 커서 매 버전마다 깨져 나가게 된다. 때문에 이런 소프트웨어는 버전이 올라가면서도 최대한의 하위 호환성을 보장하려고 노력하며, 각 구성 요소마다 버전이라고 할만한 기준이 제각각인 경우도 많다. 이럴 바에는 언제 발표되었는지 알려 주는 것이 사용자에게 더 유용하지 않을까? 황당한 상황이라고 생각할 수 있지만 당장 여러분이 지금 사용하고 있는 웹 브라우저가 바로 이런 종류의 소프트웨어이다. 웹 브라우저의 각 구성 요소는 서로 다른 종류의 표준과 기준에 따라 관리되고, 따라서 웹 브라우저의 버전은 오로지 그냥 이전 버전보다 뭔가가 (원하건대) 더 좋아졌다라는 의미만을 가지게 된다.
날짜 기반 버전은 근본적으로는 변화의 크기를 매기는 게 의미가 없는 소프트웨어에서 유용하기 때문에, 그렇지 않은, 그러니까 변화의 크기를 세부적으로 매길 만한 소프트웨어에서 날짜 기반 버전을 쓰는 건 그냥 버전 매기기 귀찮다는 뜻으로 해석할 수 있다. 뭐 버전 번호를 정하는 거야 개발자 마음이니까 이렇게 한다고 나쁘다는 건 아닌데, 사용자에게 정보가 가는 게 거의 없다는 점은 감안해 둬야 할 것이다.
숫자 하나
그냥 숫자 하나를 쓴다. 점도 뭣도 없다. 0부터 시작하는 경우도 있고 1부터 시작하는 경우도 있다. 서브버전이 널리 쓰이던 시절에는 리비전 번호가 그대로 버전으로 쓰이는 경우도 종종 있었다.
숫자 하나짜리 버전은 날짜 기반보다도 더 정보가 없다는 점에서 버전 번호 체계의 끝판왕이라고 할 수 있다. 즉, 버전이 올라간다는 것 이상으로 사용자에게 정보를 줄 이유가 없거나 주기 싫다는(…) 의미를 가지게 된다. 차라리 상술했던 날짜 기반 버전을 숫자 하나로 표시하는 경우라면 모를까, 그것도 아니고 그냥 숫자가 툭 툭 올라가는 경우 그걸 보는 사용자는 뭘 어쩌라는 건지 알 수 없게 된다. 변경점을 일일이 기술하기 애매한 작은 프로그램이나, 날짜 기반 버전에서 제시했던 대로 변화를 일일이 서술하기 극히 어려운 소프트웨어에서나 쓰여야 할 것이다.
하지만 한편으로 생각하면, 숫자 하나짜리 버전이라 하더라도 변화가 있다는 걸 알려 준다는 점에서는 버전이 존재하지 않는 것에 비해서는 나을 수 있다. 실제로 소프트웨어의 도래 훨씬 이전부터 책이 새로운 판본을 찍으면 판 번호가 올라갔는데 이는 숫자 하나짜리 버전과 별반 다르지 않다. 어쩌면 최초의 버전 번호는 숫자 하나로 시작했을 지도 모르겠다.
기타
지금까지 숫자를 쓰는 버전들에 대해서 이야기를 했는데, 사실 숫자만 쓰이는 건 아니고 이를테면 2006a처럼 숫자가 올 자리에 문자를 넣는 경우도 있다. 굳이 설명하지 않은 이유는 문자를 넣는 거 빼면 이거랑 2006.1이랑 다를 게 없기 때문. 이게 2006이라는 (아마도 날짜 기반의) 버전 뒤에 같은 버전을 쓸 수는 없으니까 a를 붙인 경우도 있는가 하면, tzdata 같이 이게 아예 공식인 경우도 있다. 문자가 올 경우 z 뒤에 무슨 문자가 와야 하는지는 상황마다 다른데, aa가 올지도 모르겠고(이 경우 전단사 기수법이 되겠다) za가 올지도 모르겠다(사전 순서를 유지하고자 하는 경우)6.
버전 번호 체계가 중간에 바뀌는 경우도 제법 있다. 다들 잘 아는 윈도의 경우 공개 버전이 소수 버전(1.0 ~ 3.11)에서 날짜 기반(95 ~ 98, 2000)으로 갔다가 코드네임(ME, XP ~ Vista)을 쓰더니 이제 다시 숫자 하나~두개짜리 버전(7 ~)으로 회귀한 사례. 자바나 GNU 이맥스 같이 주 버전의 기준을 너무 높게 잡았다가 주 버전이 영영 바뀔 것 같지 않아서 부 버전을 주 버전으로 올려 버리는 경우도 제법 있다(앞에 나온 리눅스 커널도 사전순만 유지했다 뿐이지 이 경우에 속한다). 물론 대부분 내부 버전에는 옛날 버전 체계를 유지하는 경우가 많다.
다양한 소프트웨어 패키지를 다뤄야 하는 운영체제 배포판에서는 이 모든 버전 번호 체계를 최대한 포함하는 일반화된 체계를 쓰는 경우가 잦다. 데비안의 정책이 대표적인데, 이 정책에서 가장 흥미로운 점은 실수를 되돌릴 수 있게 epoch라고 부르는 별도의 필드가 있다는 점이다. 예를 들어서 3.9 뒤에 3.10이 와야 하는데 갑자기 31.0을 써 버렸을 경우, epoch를 하나 올리고 바로잡아 1:3.10으로 표기한다는 것이다(맨 처음에 epoch가 0일 경우 0:은 생략할 수 있다). 실수가 발생할 수 있다는 걸 인정하고 대처 방법을 만들었다는 점이 인상적이다. 물론 버전 번호 체계가 바뀌어서 순번이 꼬여 버렸을 때도 쓰인다…
맹세코 말하건대, 나는 지금까지 십이진법이나 십육진법7 버전 번호를 목격한 적이 전혀 없다. 십이진법 옹호자는 세상에 제법 있는 것 같은데 말이지
버전에 음수를 쓰는 경우�� 아주 어쩌다가 간혹 있다. 나도 써 본 적이 있다 한 술 더 떠서 데비안 패키지 버전 정책 문서엘 보면 버전 번호가 어떤 건 올라가고 어떤 건 내려가는 경우를 봤다는 끔찍한 얘기가 있다;;;; 반대로 주 버전이 오랫동안 바뀌지 않는 채 부 버전만 계속 올라가서 두 자리를 넘기는 경우도 존재하는데, MAME가 지금 보니까 0.180을 찍었다. 아마 금세기 안에 네 자리를 넘기지 않을까… ↩︎
현재(2.0.0)의 표준에서는 그럼 새 버전이 잘못 매긴 버전 번호보다 낮은 경우 어떻게 해야 하는지에 대한 가이드라인은 없다. 다만 추론은 가능한데, 이 경우 새 버전 번호는 여전히 잘못 매긴 번호보다 커야 한다고 본다. 아마도 주 버전이 실수로 올라가는 경우는 거의 없을 것이고, 부 버전이나 패치 버전이 실수로 올라가는 건 감수할 수 있을만한 불편함이라 생각할 수 있을 것이다. 비슷한 문제로 백포팅된 버전은 어떻게 처리하느냐는 문제가 있는데 논의가 (여전히) 진행되고 있다. ↩︎
내가 이해하는 바로는 올라가는 숫자는 1일 필요가 없다. 그냥 올라가기만 하면 된다. 그래서 농담 삼아 1.0.9 다음이 1.0.91, 1.0.92, …, 1.0.99, 1.0.991, …로 진행되는 사전순 유의적 버저닝을 제안해 볼까 하는 생각도 해 보았다… ↩︎
물론 2.0과 2.00, 2.000 등은 모두 같다고 취급한다. 숫자 두 개짜리 버전과 구분하려 두번째 숫자를 두 자리 이상으로 쓰기도 한다(2.0을 안 쓰고 항상 2.00이나 2.000을 쓰는 것). ↩︎
특히 일정 기간(6주, 3개월 등)마다 한 번씩 나오는 소프트웨어의 경우 버전 번호만 가지고 자동으로 날짜를 추론할 수도 있다. 유의적 버저닝 같이 일반적인 버전 번호 체계를 깨뜨리지 않으면서 날짜 기반으로 이행하는 경우 이런 경우가 제법 발생한다. ↩︎
선술한 tzdata의 경우 천만 다행으로 지금까지는 한 해에 26개를 넘는 버전이 나온 적은 없다. 2017년 1월 현재 최고 기록은 2009u. ↩︎
내부 표기가 십육진법인 경우는 물론 많다. 이를테면 파이썬 3.4.1a2는 sys.hexversion 찍으면 0x030401a2라고 뜬다. 정식 표기가 십육진법인 걸 본 적이 없다는 얘기다. 한 번 누가 써 보시죠? ↩︎
3 notes · View notes
arachneng · 7 years
Text
유니코드 한중일 잔혹사
작년에 Awesome Unicode라는 유니코드에 대한 이런 저런 사실들을 모아 놓은 저장소를 본 적이 있는데, 한글 채움 문자 얘기가 있는데 왜 공백인지 잘 모르겠다고 쓰여 있어서 옛날에 레딧에 올렸던 내용과 함께 내친 김에 한중일(CJK) 관련된 것들을 생각나는 대로 왕창 써서 올린 적이 있다. 그러고 나서 잊어 버리고 있었는데, 몇 달 뒤에 아는 분께서 저 글에 미묘한 오류가 있다면서 알려 주시길래 깜짝 놀란 적이 있다(“아니 저 이슈를 어디서 보셨길래?”).
사실 유니코드는 당연히 서양에서 개발이 시작되었고, 서양의 많은 문자들은 결국 페니키아 문자에서 출발하여 분화된 것이므로 알파벳에서 멀어지는 문자일 수록 유니코드에 매끈하게 통합되기 어려웠던 건 사실이다. 설계 과정에서 한중일 문자가 고려가 되지 않은 건 아니지만 (오히려 한자의 존재 자체가 유니코드 제정의 동력이 되었다는 얘기도 있다) 워낙 이질적인 구조다 보니 삐끗난 부분이 여럿 있는데, 그 때문에 지금까지 남아 있는 이상한 것들이 여럿 있다. 그리하여, 예전에 써 놓았던 글을 피드백을 반영해서 재작성에 가까운 번역을 해 보기로 한다. 내용 자체는 한중일 문자에 대해 잘 모르는 외국인을 위해 쓴 것이므로 이미 잘 알고 있는 내용이 있어도 그러려니 하시길… 대괄호 안에 있는 내용은 원문의 맥락이 부족해서 추가했거나 나중에 가필/수정한 부분.
한글 채움 문자
한글 채움 문자(U+3164 HANGUL FILLER)는 문자 집합이 선택할 수 있는 가장 멍청한 선택이라 할 수 있다. 한글은 간단한 알고리즘으로 조립되는 문자이기에 한글 문자 집합도 이상적으로는 그렇게 구성되어야 하겠으나, 기존에 널리 쓰이던 멀티바이트 인코딩들(ISO 2022, EUC)은 모두 94 × 94 = 8836자라는 상대적으로 작은 문자 집합1만을 사용할 수 있었고, 이는 현대 한글 음절 19 × 21 × 28 = 11172에 비해 훨씬 부족했다.
따라서 KS X 1001 문자 집합은 2350자의 자주 쓰이는 한글 음절(이랑 다른 이유로 유니코드에서 말썽이 된, 중복을 포함하는 한자 4888자)만을 포함하게 되었다. 다른 한글 음절이 지원되지 않는다는 점은 차치하고라도, 이 설계 때문에 한글을 지원하는 모든 소프트웨어의 복잡도가 늘어나 버렸고 유니코드 이전에 존재하던 또 다른 “조합형” 인코딩과의 충돌 및 논란이 있어 왔다. 표준화 기구는 나중에 설계가 부족했음을 인정했으나, 그 대안이라는 것이 문자 4개의 조합, 즉 8바이트의 열로 남아 있는 음절들을 임시로 표현할 수 있게 한다는 것이었다! 한글 채움 문자는 이런 조합을 표시하는 문자로, 이를테면 ㄱㅏ은 가를 나타내고 ㅂㅞㄺ은 KS X 1001에 포함되지 않은 뷁을 나타내는 식이다.
한글 채움 문자는 너무 늦게 추가되었기 때문에 소프트웨어 업계에서 사실상 외면받았다. 하지만 한글 채움 문자가 KS X 1001에 포함되어 있는 이상 유니코드에도 추가되어야 했고, 유니코드에는 한글 조합에 대한 표준이 존재하지 않지만 어쨌든 문자의 일부로 쓰일 수 있으니까 문자처럼 취급되어야 했다. [그래서 분명히 보이지 않는 문자임에도 불구하고 문자나 더 나아가 식별자의 일부로 쓰일 수 있게 된 것이다.] 이러어언.
한중일 통합 한자
시작하기에 앞서, 사실 “한중일” 통합 한자(Unified CJK Ideograph2)에는 베트남이 과거에 사용했던 쯔놈도 들어 있다는 걸 알아 두자. 물론 이제 이 이름은 고칠 수 없다.
한자 통합(Han unification)은 역사적으로 동일하게 취급되던 살짝 살짝 다른 이체자들을 하나의 한자로 합쳐서 인코딩하는 거대한 작업이었다. 문제는 역사적으로 동일하다고 실제 사용이 동일한 건 아니었다는 점인데, 이를테면 특정한 고유명사는 항상 똑같은 한자로만 쓰이는 경우가 있다. 확인해 보진 않았지만, 내 생각에 이건 기본다국어평면(BMP)의 제한된 크기와 관련되어 있지 않나 싶은데, 이런 한자들은 유니코드가 16비트로 제한되던 시절 초기에 추가되었기 때문이다. 물론 다양한 예외도 존재하는데 보통 한자가 유래한 문자 집합에서 통합이 되어 있지 않은 경우가 대부분이며, 원래 통합되었던 문자들이 나중에 쪼개지는 경우도 있다. 이체자 선택자(variation selector)와 한자 이체자 데이터베이스(IVD)가 이 오래된 문제를 마침내 제대로 해결할 거라고 다들 믿는데, 이들을 지원하는 글꼴과 소프트웨어 모두 아직 충분히 널리 쓰이고 있진 않다.
완전히 같은 모양의 한자가 여럿 인코딩된 경우도 있는데, 여러 종류로 나눌 수 있다. 일단 Big5에서 실수로 합치지 않은 U+FA0C랑 U+FA0D 같이 쌩 버그인 경우가 있고, 앞에서 언급되었던 KS X 1001의 경우 독음만 다른 같은 문자가 수백자나 인코딩되어 있는데 실제로는 신뢰할 수 없는 정보가 되어 버린 사례같이 그냥 멍청한 경우도 있으며, 물론 강희자전의 한자 부수도 중복이다.
아예 그 원전을 알 수 없는 한자가 등록된 경우도 있다. 가장 유명한 것으로는 JIS X 0208에서 유래한 문자들이 있는데, 그 중에서도 彁는 원전도 원전이 어떤 오류로 그렇게 인코딩된 건지도 알 수 없다는 점에서 두 배로 알 수 없는 경우이다.
[실은 한자 말고 한글에도 버그가 좀 들어 있다고 알려져 있다. 정확히는 옛한글 낱자의 문제인데, 옛한글 겹낱자 중에 옛이응이 들어가야 할 것이 이응이 들어 있다거나(초기 한글에서 그냥 이응은 음가가 없기 때문에 {2017-01-16 빠진 낱말 추가:} 종성 겹낱자의 일부로 들어와서는 안 된다), ㆅ(U+3185)의 오자로 보이는 ꥼ(U+A97C)가 문헌 출처도 없이 버젓이 들어 있는 것 같다거나… 현대 한글과는 달리 옛한글은 절대적으로 문헌 발굴에 의지하기 때문에 생길 수 있는 문제들이다.]
호환 기호
[한중일 문자 집합이 가져다 준 것은 다양한 보통 문자 말고도] 호환 기호들이 많이 있는데, 이들은 한중일에서 많이 쓰이는 네모나게 여러 문자를 뭉친 기호들(이를테면 U+3392는 “MHz”를 한 문자로 표시한다)에서 유래한 것으로 트윗을 최적화하는데 매우 매우 유용하다. 물론 실수도 있는데, 이를테면 Ken Lunde3가 설명하길 오타 때문에 네모나게 뭉친 기호가 실제로 사용되는 어떤 단위나 약자에도 대응되지 않게 된 경우가 있다고 한다.
아 물론 에모지도 일본에서 온 것이다. 이게 다 일본의 통신 삼사 때문이니 그 쪽을 탓하라.
[에모지에 대해서는 옛날에 긴 글을 쓴 적이 있으니 이 쪽을 참고. 6.0 이후에도 에모지는 크고 작은 사건을 일으켜 왔는데 최근의 사례를 하나만 들면 애플이 비난에 못 이겨 총을 나타내는 에모지를 장난감 물총(…)으로 바꾼 사례 따위가 있다. 그러���까 이런 거 넣지 말라고.]
정규화 문제
심지어 유니코드 정규화가 제대로 동작하지 않는 사례도 있다. 유니코드에서 한글 음절은 두 개의 정규화된 형식이 있는데, 하나는 완전히 합쳐진 형태(예: U+AC00 가)이고 하나는 완전히 풀린 형태(예: U+1100 ㄱ + U+1161 ㅏ)이다. 정규화 후 이들은 똑같이 취급되어야 하는데, 이 정규화 알고리즘에는 완전히 합쳐지는 게 불가능한 옛한글과 관련해서 중요한 오류가 있다. (예: U+1103 U+1172 U+11F0 듀ᇰ은 종성 때문에 옛한글로 분류되는데, 정규화 뒤 첫 두 낱자가 U+B4C0 듀로 합쳐지면서 두 개의 글자로 쪼개진다.) 유니코드 알고리즘을 더 이상 고칠 수는 없으므로, 결국 한국에서 쓸 목적으로 한글을 제대로 처리하는 정규화 알고리즘(KS X 1026-1)이 따로 나와 버리고 말았다.
유니코드 2.0 이전의 한글
이 사건은 워낙에 유명해서 "Korean mess"라는 용어가 따로 있을 정도이다. 한글은 유니코드 역사를 통틀어 통째로 움직여 다닌 유일한 문자일 것이다.4 유니코드 2.0 이전에 한글은 조합되는 형태로 인코딩되지 않았고, U+3400..4DFF 영역에 할당되어 있었다. 현대 한글이 종국에는 아무 의미 있는 순서 없이 유니코드에 통째로 인코딩될 때가 올 거라는 것이 시간이 갈수록 분명해지자, 긴 토론 끝에 한글은 U+AC00..D7FF로 옮겨 갔고 순서도 규칙적으로 바뀌었다. 이 때문에 현재의 한중일 한자 블록이 기존 한글 블록 바로 뒤인 U+4E00에서 시작하는 것이다. (자세한 사항. 여러 참석자들에 따르면 이 전체 과정은 거의 박빙이었고 개판 일보 전이었다 카더라.)
[이 부분에 대해서는 조금 더 설명할 필요가 있겠다. 요전에 신정식 씨가 한글의 11172자 인코딩 자체가 불합리하게 이루어졌다는 주장을 한 적이 있어서 개인적으로 이리 저리 찾아 본 적이 있는데, 실제로 어느 정도의 야바위(…)가 있던 건 사실로 보인다. 인터넷으로 접근할 수 있는 자료가 없어서 불명확한 부분도 많은데 내가 파악한 바를 간단히 요약하면, 유니코드 1.0의 한글은 KS X 1001이 존재한다는 이유만으로 사용자 피드백이 없이 대강 들어간 것이었고, 이 때문에 2.0 표준화 과정에서 한글을 재인코딩하려는 실 사용자측(국가가 아니다! 이 작업을 주도한 주체 중 하나는 다름 아닌 한컴이었다.)의 움직임이 있었는데 표준화 과정에 너무 늦게 들어 오는 바람에 국가들의 반대가 거셌던 것으로 보인다. (2017-01-07 추가: 문헌을 다시 읽어 봤는데 1.0 시절에도 움직임이 없진 않았던 것으로 확인.) (2017-01-16 추가: 정확한 문헌을 묻는 분이 계셔서 첨언하면, 강태진 저 《세상은 꿈꾸는 자의 것이다》 등을 참고하였다. 인터넷 상에 떠돌아다니는 텍스트가 있다…) 그래서 뭔 짓을 했냐 하면 반대파 대표에게 식사를 사 주기도 하고 미국이 중재안을 만들어 오자 아예 비토해 버렸다는 듯… 최종적으로는 한자와는 달리 한글은 현대 한글이 완전히 할당되면 추가 할당이 없을 것이라는 논리가 어떻게든 받아들여져서 통과되었다고 하는데, 과연 어떻게 보면 떼 써서 자기 원하는 바를 관철해 낸 나쁜 선례처럼 보이기도 한다. 다만 기술적인 참작 여지가 어느 정도 있고(유니코드 구현자 입장에서는 확실히 규칙적인 쪽이 편하다), 유니코드에서 허구한 날 싸우는 건 이 때에 국한된 일이 아니긴 하므로 개인적으로는 그냥 초창기 유니코드의 혼돈·파괴·망ㄱ…를 보여 주는 예시로만 보는 것이 옳지 않나 싶다.]
기술적으로는 3바이트 인코딩을 써서 94 × 94 × 94 = 830584자를 사용할 수도 있었다. 하지만 내가 아는 한 이러한 인코딩을 요구하는 문자 집합이 실제로 설계된 적은 없으며, 따라서 실제로 쓸 수 있는 구현도 없다고 볼 수 있다. ↩︎
[원문에서는 언급하지 않았지만 사실 한자는 한 문자가 (어떤 기준에서든) 낱말을 나타내는 표어문자(logogram)이지 완벽히 뜻만을 나타내는 표의문자(ideograph)가 아니기 때문에 이 영문 표기조차 틀렸다. 한자에는 오로지 문법적으로만 사용되는 문자도 들어 있기 때문이다. 근데 물론 이것도 바꾸기에는 늦었다.] ↩︎
[어도비에서 유니코드 표준화 과정에 오랫동안 참여해 온 유니코드 전문가. 만약 유니코드에 관심이 많다면 이 분의 블로그는 필독할 것.] ↩︎
[지적을 받았던 부분이 바로 이 문장인데, 움직인다는 것을 한 종류의 문자 전체가 삭제 없이 다음 버전에 이동한다는 것으로 정의한다면 한글이 유일한 사례인 것이 맞다. 그러나 삭제 후 재인코딩된 경우까지 포함하면 티베트 문자가 1.1에서 사라졌다가 2.0에서 다른 곳에 나타난 경우가 있다고 한다. 개별 문자까지 포함하면 몇 가지 사례가 더 있다는 듯. 아니 나도 유니코드 1.0을 세세히 읽어 볼 생각은 하지 않아서…] ↩︎
7 notes · View notes
arachneng · 7 years
Text
서버 이전
Tumblr media Tumblr media
  메아리를 비롯해서 이런 저런 사이트들이 호스팅되고 있는 루리넷 “오카리나” 서버를 마침내 집으로 옮겼다. 지난 몇 년간 서버를 운영해 왔는데, 옛날에는 웹 서버 등으로 많이 사용되었지만 이제는 쓰는 사람만 쓰는 셸 서버에 더 가깝게 운영되다 보니 굳이 비싼 돈 주고 IDC에 넣을 필요가 없어졌다. 특히 다른 사람의 간섭이 적은 주거를 찾으면서 이제 그냥 집에 옮겨야지… 옮겨야지 한 게 1년이 지나 버렸는데, 마침내 정산을 마치고 집에 제반 장비(주로 UPS)를 갖춰서 대강 정리를 마친 게 방금 전의 일. 나는 곱게 서버만 옮기지 왜 배포판 업데이트를 한다고 또 한바탕 난리를 쳤을까?
당연한 얘기긴 한데 집에서 서버를 돌리는 건 여러 가지 장애가 있고, 서버 자체에게도 그 정도로 좋은 환경은 아니다. 온도와 습도 조절이 잘 안 되는 건 물론이고 정전이나 네트워크 장애 같은 것도 IDC보다야 흔한 일이다. 실은 그래서 현재 루리넷은 집으로 옮겨 온 오카리나 서버와는 별개로 고가용성을 목적으로 AWS에서 돌아가는 작은 서버가 따로 있다. 그럼에도 불구하고 서버를 옮기게 된 건 이런 저런 조건이 맞아 떨어졌기 때문이다. 집에 서버를 들여 놓을 생각이 있는 분께는 참고가 될지도?
집에서 서버를 돌릴 때 가장 큰 비용은 전기료인데, 누진제의 혜택(?)을 가장 많이 받는 1인 가구라 돈 별로 안 내도 된다. 예상되는 추가 비용이 많아야 2만원.
집에 이사를 올 때 서버를 돌릴 가능성도 상정하고 약간의 추가 비용(이래봤자 만원 정도)으로 가정집에서 경제적으로 들일 수 있는 나쁘지 않은 회선을 들여 왔다. 그래서 회선 부분에서는 추가 비용이 없다.
의도하진 않았지만 현 서버를 맞출 때 미니 데스크탑으로 맞췄고 구성 요소도 그다지 유별나게 가동 환경을 따지지 않기 때문에, 가정에서 돌린다고 수명이 급격히 줄어들진 않을 거라는 확신이 있었다. 1U 서버였다면 어려웠겠지.
여름과 겨울을 나고 보니 집이 생각보다 외부 온도에 민감하진 않은 것으로 드러났다. 습도도 세탁을 하는 게 아니면 집 자체는 큰 문제가 없었다. 장기적으로는 먼지가 문제가 될 수는 있는데, 그래서 쑤셔 박은 장소가 사진에서 보이듯 외부와 가장 격리된 옷장 아랫쪽인 것이다.
거진 십수년 간의 공동 생활을 통해 일정 이하의 소음이 수면 패턴에 전혀 영향을 미치지 않는다는 점을 확인했다(…). 물론 아예 신경이 쓰이지 않는 건 아니므로 격리된 장소에 쑤셔 박아 놓았지만. 참고로 원래 갖고 있던 데스크탑이 저 서버보다 더 시끄럽다(왜??).
이제는 말할 수 있지만 서버 호스팅이 너무 비싸고 구렸다… 미니 데스크탑은 일반적인 서버 폼팩터가 아니기 때문에 받아 주는 곳이 한정되어 있는데(서버 랙을 별도로 마련해야 하기 때문), 이전하기 전에 쓰던 곳은 1U 서버 호스팅의 두 배나 되는 비용을 요구했었다. 차라리 비싸기만 한 거면 요구사항이 특이해서 그러려니 싶은데, 돈은 왕창 받는 주제에 공지 없이 요금제가 슥슥 줄어들고 IDC에 있다면서 네트워크가 한 달에 한 번 꼴로 장애를 일으키질 않나, 가장 압권이었던 건 UPS 이중화를 위해 서버 전원을 차단해야 한다는 기막힌 논리였다. 그래서 AWS로 다중화하는 것까지 포함해서 예상 비용이 서버 호스팅 비용의 반 정도 밖에 안 된다! 이 구린 서버 호스팅 업체가 어딘지에 대해서는 여러분의 상상에 맡긴다.
1 note · View note
arachneng · 7 years
Text
내가 원하는 정적 웹사이트 생성기
누누히 얘기가 나오는 거지만 메아리는 거의 모든 부분이 정적으로 구성되어 있어서 요즘 하루가 멀다 하고 나오는 정적 웹사이트 생성기를 쓰기에 적절한 구조로 되어 있다. 적어도 이론상으로는. 실제로는 뭐 보다시피 저널은 아무래도 야짤 플랫폼으로만 생명력을 유지하고 있는 듯한 텀블러고(…) 오래 전에 멸망한 풉;은 도쿠위키(의 상당히 오래된 버전)이며 뇌는 무려 ikiwiki이다. 몇 년 전부터 몇 차례 새로운 디자인을 시도하고는 있는데 몇 가지 이유로 잘 안 되고 있다. 그래서 오늘은 도대체 뭐가 문제이며, 내가 생각하는 이상적인 설계가 뭔지를 써 놓아 보고 잊어버리려고 한다.
정적 웹사이트 생성기의 얼개
보통의 정적 웹사이트 생성기는 이런 식으로 구성되어 있다.
사이트의 구조를 어떤 방법으로든 얻어 낸다. 보통 이 부분이 가장 변화무쌍하며, 사람들의 취향에 따라 다양한 접근이 있다. “정적” 웹사이트 생성기라고 해서 이 부분에 꼭 디비를 안 쓸 필요는 없다는 것도 주목.
텍스트를 적절한 마크업을 써서 HTML로 변환한다. 요즘은 거진 다 마크다운이지만 꼭 그럴 필요까지는 없다.
미리 지정된 HTML 템플릿에 지정한 텍스트와 메타데이터를 쑤셔 넣는다. 물론 템플릿 언어에 따라 모든 것이 갈린다.
위 과정을 소스 파일이 바뀔 때마다 적절한 방법으로 점진적으로 갱신한다. 예를 들어서 블로그 스타일이라면 y년 m월 d일에 쓰여진 글이 고쳐질 때 y년 m월 아카이브와 그 앞뒷글도 함께 갱신하는 식.
웃긴 것은 현존하는 어지간한 웹사이트 생성기들은 네 가지 점 모두 내 필요를 충족하지 못 한다는 점이다.
대부분의 정적 웹사이트 생성기는 블로그 스타일 또는 완벽한 평면적 웹사이트에 최적화되어 있다. 따라서 조금만 구조가 복잡해져도 그대로 쓸 수 없거나 상당히 불편해진다. 그런데 메아리는 블로그, 위키, 자동 리스트, 메타 페이지 따위가 어지럽게 뒤섞인 상당히 실험적인 웹사이트 포맷이란 말이다(…). Lektor가 나왔을 때 개인적으로 기대했던 것은 바로 이거였는데, 소스 코드를 뜯어 보니 여전히 이 쪽도 그다지 확장성은 없었다.
마크다운은 훌륭한 경량 마크업 언어긴 하지만 아주 옛날에 지적했듯 확장성이 심하게 떨어진다. 단적으로, 현재의 메아리 풉;에 해당할 부분에서는 위키 링크가 없으면 많이 곤란한데 마크다운에는 그 따위 거 없다. 그래서 neu.mearie.org 실험을 할 적에 Pandoc 후처리 필터까지 만들어가면서 위키 링크를 구현해 보았는데 못 해 먹겠더라. 최근에는 Pandoc을 그대로 쓸 수 없다는 판단 하에 다른 CommonMark 라이브러리들을 가지고 좀 실험해 보고 있다. (눈물이 난다…) Asciidoctor를 쓰면 어떻겠느냐는 얘기도 여기 저기서 나오고 있는데, 마크다운으로 쓰여진 텍스트가 수천페이지이고1 요구사항이 기괴해서 어차피 그대로 쓸 수는 없을 거라는 점은 동일할 듯. reST도 옛날에 심각하게 고민해 보았다(지금까지 나온 것 중에서는 그나마 확장성이 가장 높다).
현재의 메아리는 Mako 기반의 템플릿을 쓰고 있다. 이게 왜 문제인지 모르다면… Mako는 일반적인 템플릿 언어보다 훨씬 파이썬에 가깝다는 점을 상기하자.
1번과 연결되는데, 메아리의 전체적인 구조 때문에 단순한 의존성 그래프가 나오지 않는다. 이를테면 위키라면 해당 위키 문서를 링크하는 모든 문서에 의존성이 걸린다(해당 위키 문서가 사라지면 빨간 줄을 그어 줘야 하니까!).
그 밖에도 몇 차례의 실험 끝에 Travis 등을 사용해서 완전 자동화하겠다는 구상이 꼬여버린 점도 있다. 이 점에서는 Lektor에 배울 것이 많은데, Lektor가 제시하는 모델은 로컬에서는 텍스트 편집기를 써도 되고 좀 더 자동화된 웹 인터페이스를 써도 되지만 최종적으로는 정적 HTML이 생성되어서 배치(deploy)된다는 것이다. 어지간한 정적 웹사이트 생성기가 watch 모드(파일 변경을 모니터링하다가 바뀌면 바로 재빌드하는 모드)를 지원하긴 하지만 실시간 확인에는 미치지 못 한다는 점을 생각하면 장점만 취합한 것이다. 사실 원래 목표는 Travis로 자동화하고 편집을 깃헙에서 바로 하는 거였지…
무엇이 필요한가
위의 요구사항에서 추론되는, 새로운 정적 웹사이트 생성기의 구성 요소는 세 개로 정리된다.
동적 의존성 추적 엔진
1에 적합한 형태로 의존성 정보를 읽고 쓰는 확장 가능한 마크업 언어 구현
1에 적합한 형태로 의존성 정보를 읽고 쓰는 최종 템플릿 엔진
1이 매우 중요한 구성 요소라는 걸 깨닫는 데는 기나긴 삽질이 필요했다. 사실 아주 먼 옛날에 파이썬 기반 웹 애플리케이션 형태로 만들려고 했을 때도, 현재의 메아리를 구성하는 Makefile도, 그리고 neu.mearie.org에서 사용하고 있는 미친 크기의 Makefile도 모두 다 의존성 추적에서 상당한 삽질을 하고 있었는데 그걸 이제야 알았나 싶다. 1에 비해 2와 3은 상대적으로 마이너한 부분으로, 정 안 되겠으면 대강 만들어서 넘겨도 되는 수준이다! 그보다는 옛날에 삽질을 하도 많이 했기 때문에 더 할 여력이 없는 것이다
그럼 의존성 추적에서 널리 쓰이는 Make를 못 쓰는 이유가 뭐냐고 반문할 수 있겠는데, “동적”이라는 낱말이 중요하다. 예를 들어서 A.md라는 소스 파일에서 마크업 변환으로 A.content.html이 나온 뒤 템플릿 journal.tmpl.html과 합쳐져 최종 웹페이지 A.html이 되고, 중간에 삽입된 alpha.png 파일은 자동으로 썸네일이 생성되어 alpha.thumb.png와 함께 배치된다고 하자. 이러한 의존성 정보는 A.md를 파싱하기 전까지는 아무 것도 알 수 없다. 위에 링크한 Makefile에서는 다양한 꼼수를 사용해서 이를 어떻게 둘러 가고 있는데, 결국 Makefile은 대부분의 규칙이 미리 알려져 있는 의존성 그래프에서 위상정렬하는 걸 기반으로 하기 때문에 깊이가 조금만 깊어져도 규칙을 관리할 수 없다. Make 대신에 몇 가지 대안을 찾아 보았고, 특히 tup를 꽤 깊이 살펴 봤는데, 결국 의존성 정보를 실시간으로 수정하는 건 가능하지 않아서 (다른 점에서는 결격 사유가 거의 없었으나) 포기했다.
재밌는 점은, 사실 이러한 동적 의존성 추적 엔진은 제대로만 만들어지면 Make 같은 전통적인 정적 의존성 추적 엔진을 대체할 가능성이 있다는 점이다. 당장 동적인 규칙을 만들기 위한 Makefile 꼼수는 다름이 아니라 C/C++ 컴파일 규칙을 만드는 과정에서 나온 것이다.2 그래서 현재 방향은 1을 제대로 만들어 보는 것으로 선회한 상태. 혹시 이런 요건을 만족하는 라이브러리나 애플리케이션이 있다면 제발, 제발 연락해 주시라.
Asciidoctor가 마크다운 호환성이 있다고 주장하는 건 개뻥이니 믿지 말자. Asciidoctor의 마크다운 호환성은 인라인 문법 몇 개 가지고 주장하는 거다(…). 당장 리스트 문법부터 호환이 되지 않는다. ↩︎
그럼 현존하는 Make의 대체제들은 이 문제를 어떻게 해결하냐고? Make와 같은 방법을 취하거나 (왜냐하면 gcc 등이 뱉어내는 의존성 정보가 결국 Makefile 포맷이라서) 좀 더 잘 해 보겠다는 것들은 해당 규칙을 특수한 경우로 처리한다. 그러니 메아리의 용도에서는 쓸모가 없는 것이다. ↩︎
3 notes · View notes
arachneng · 7 years
Text
데이터 마이닝
여기에서 굳이 이야기를 꺼내진 않지만 요즘은 박근혜-최순실 게이트 덕분에 JTBC가 흥하고 있는데, 그래서 텔레비전이 없는 나도 JTBC 뉴스룸을 자주 챙겨 본다. 마침 어제 정유라가 덴마크에서 잡혔고 그 과정을 보여 준다길래 생방은 아니지만 뒤늦게 뉴스룸을 보다가 내 눈을 의심하게 하는 장면을 보게 되었는데, 아니 글쎄 군데 군데 잡히는 정유라 여권의 기계 판독 영역(MRZ)이 전부 다는 아니지만 일부 가려지지 않고 표시되어 있더라… (그것도 한 화면에서 안 나온다는 얘기지 다 합쳐 놓으면 MRZ를 복구하는 건 어렵지 않다!) 기계 판독 영역에는 생년월일, 국적, 성별, (대한민국 국민이라면) 주민등록번호, 여권 유효일 같은 게 거의 대놓고 들어가 있기 때문에 사실상 주민등록증을 까 놓는 수준의 파급력이 있다. 일부 영상에서는 완전히 가려져 있는 걸로 봐서 바쁘게 편집하다가 미처 눈치를 못 챈 거 아닌가 싶은데, 설마하니 정유라의 개인 정보를 공공재화하려는 게 아닌 이상 조금 더 ��심할 필요가 있지 않나 싶다.
꼭 이런 사례 뿐만은 아니어도, 정보화 사회(웃음)에서 개개인은 엄청난 양의 정보를 알게 모르게 뿌리고 다닌다. 개중에는 이를테면 신용 정보 같이, 원래는 노출될 것이 아니었음에도 사고 또는 무관심으로 노출되는 것도 많지만, 개중에는 자기 자신이 의식하여 정보를 노출하는 경우도 심심찮게 보인다. 정보를 수집하여 가공하는 것을 흔히 광산에서 광물을 캐내는 것에 비유해 데이터 마이닝(data mining)이라 한다. 나는 어떤 사람을 처음 봤을 때 그 사람이 그동안 뿌리고 다닌 정보를 문제가 되지 않을 범위에서1 이리 저리 찾아 보는 수동(…) 데이터 마이닝을 종종 해 보는데, 그 결과는 참 볼만하다. 더 볼만한 것은, 가끔씩 이렇게 스스로 노출시켜 놓은 정보가 다른 방법으로 사용되어 문제가 되는 사례가 여기 저기에서 보이는데(얼마나 많은지 네티즌 수사대라는 여러 의미에서 위험한 신조어가 있지 않은가), 그렇게 문제가 되어 놓고도 정보의 셀프 노출에 대해서는 문제를 삼지 않는 사람들이 태반이라는 점이다.
당연한 소리를 하나 첨언하면, 사실 현대 사회에서 정보를 아예 안 뿌리고 다닐 수 있는 방법은 없다. 당장 선술했던 데이터 마이닝의 주된 사용처도 개개인이 인식하지 못 하는데 전체를 모아 놓으면 파악할 수 있는 통찰을 얻고자 하는 것인데, 어떻게 개개인이 전체를 보지도 않고 통찰에 영향을 주는 그런 시그널을 잡을 수 있겠느냔 말이다. 여기에서는 그런 비현실적인 얘기를 하려는 게 아니다. 오히려, “무슨” 정보가 “어떻게” 뿌려지고 있으며 “누가” 그 정보에 접근할 수 있는지 파악하는 감각을 기르는 것이 중요하다고 생각한다. 몇 가지 예시를 들어 보자.
이름은 얼마나 많은 사람들이 알고 있을까? 이름은 원래 다른 사람이 부르라고 만들어진 것이니만큼 숨기고 싶어도 숨기기 어렵다. 내가 아는 어떤 한 사람은 총력을 다해서 자기 본명을 숨기고 다녔는데, 물론 그도 자기 이름이 그렇게 쉽게 숨길 수 있는 건 아니라는 걸 알고 있었지만(그래서 어디까지나 공개되어 있는 쪽에 협조해 달라는 수준이었다), 나는 그 얘기를 들었을 때 어렵지 않게 두 세 다리 건너 본명을 가리키는 웹 문서를 찾아내서 보여 줄 수 있었다(…). 상황이 이러하니, 원래 이름을 숨기고자 할 때는 그 이름과 최대한 연결을 짓지 않는 방향으로 별명을 사용해야 할 것이다. 특히 본명으로부터 유래한 이름은 의도하고 쓰는 것(본명이 복잡하다거나 하는 이유 등)이 아니면 최대한 피해야 한다.
나이나 성별, 직업은 얼마나 많은 사람들이 알고 있을까? 이것 또한 일반적으로 숨기기 어려운 종류의 정보인데, 평소의 언행과 이런 정보 사이의 상관 관계가 높기 때문에 충분히 오랫동안 관측하면 꽤 높은 정확도로 알아 낼 수 있다. 한 가지 안타까운 점은 이렇게 알기 쉬운 정보를 가지고 차별을 하는 문화가 여전히 널리 퍼져 있다는 점이다. 이런 것들을 숨기려면 오랫동안 관측될 수 없는 환경, 즉 명의를 오랫동안 쓰는 걸 피하는 생활을 하는 수 밖에 없는데 가능은 하지만 쉽지 않다.
주소는 얼마나 많은 사람들이 알고 있을까? 여기서부터는 자기가 대놓고 말하지 않으면 일정 정확도 이상으로 알기 어려운 것들이 나오기 시작한다. 상술한 방법론에서 예를 들어서 어느 나라에 사는지(평소 활동하는 시간대로부터 잠자는 시간대를 추론할 수 있다), 어느 지역에 사는지 정도는 알 수 있으나, 번지 단위의 세부 주소는 어지간히 대놓고 정보를 뿌리지 않으면 일반적으로는 얻기 매우 어렵다. 문제는 자기가 정보를 뿌리고 있다는 것을 인식하지 못 한 채 뿌려 버리는 것(…). 택배 운송장이라거나, 집 근처 또는 집 앞에서 찍은 사진, 해당 집에서만 찍을 수 있는 종류의 사진, 특정 시점에서 특정 위치에서만 일어난 사건 등이 주소를 노출시킬 수 있는 대표적인 예이다.
전화번호는 얼마나 많은 사람들이 알고 있을까? 기술적으로는 전화번호는 주소 정도로 알기 어려워야 정상이다. 주소든 전화번호든 장기적으로 아는 사람들은 지인들에 한정되고 단기적으로는 믿을만한2 기업들로 한정된다고 생각할 수 있으니까. 헌데 안타깝게도 전화번호는 주소보다 훨씬 잘 공유되는 게 일반적인데, 여러 요인이 있지만 i) 더 짧으니까 노출이 잘 되고 ii) 전화번호를 고유 키 삼는 데이터베이스가 하도 많아서 그 데이터베이스가 털리면 끝장이 나는데다 iii) 사업 관계나 학교 내 소그룹 같이 단기적으로 유지될 개인 관계가 크게 늘었기 때문이 아닌가 싶다.
한때 공공재 취급을 받았던 주민등록번호 같은 것은 시대가 마침내 변하여 그 정도로 노출이 심해지진 않게 되었다. (물론 이미 노출된 사람들의 주민등록번호가 다시 숨겨지는 건 아니다…) 여기에는 주민등록번호가 필수적으로 필요하지 않은 기업이 주민등록번호를 받지 못 하게 강제한 것이 크게 작용했는데, 앞에서 전화번호가 얼마나 그리고 왜 많이 노출되는지와 비교해 보면 시사하는 바가 크다.
이런 식으로 감을 잡으면 반대 방향으로 어떤 정보를 노출시킬 지 선택할 수 있게 된다. 예를 들어 집들이를 한다고 하면 적지 않은 사람들에게 주소를 공개해야 하는데, 이것은 필요한 것인가? 필요한 것이라고 판단했다면 그럼 정확히 그 사람들에게만 전달될 수 있도록 개별적으로 정보를 전달하거나, 세부 주소를 식별하긴 어렵지만 적절히 걸어서 집에 도달할 수 있는 장소에 결집을 시키는 걸 생각할 수 있다. 그리고 개인적으로는 이렇게 하였음에도 다른 사람들에 의해 (특히 인스타그램 같은 것들을 통해) 주소가 식별되는 위험성이 있을 수 있다. 위험성을 얼마나 묵인할 수 있는지에 따라서 접근이 달라진다(아예 믿을만한 사람들끼리만 집들이를 하고 싶을 수도 있겠다). 짐작한 분도 있겠지만 사실 이건 위협 모델링이라고 하는 매우 고전적이지만 매우 알려지지 않은 방법론을 그대로 가져 온 것이다. 보안이나, 개인 정보나, 문제를 인식하는 것이 최우선인 것이다.
일반적으로 용인되지 않는 방법으로 얻을 수 있는 정보 같은 걸 쓰지 않고, 검색 엔진 등을 사용해서 흔적들을 추적한다는 얘기. ↩︎
음, 사실 그다지 믿을만하지 않다. 하지만 개인이 노출시켰을 때의 책임에 비해 기업이 노출시켰을 때의 책임이 크기 때문에 문제가 생겼을 경우 그걸로 어느 정도 메꿀 수 있다는 차이가 있다. ↩︎
3 notes · View notes