메아리 저널

카일루아, 둘: 파서 (1)​

저번 글에서 카일루아가 왜 만들어졌는지, 그리고 초기 설계가 어떠했는지에 대해서 간략히 설명했다. 많은 언어가 그렇듯, 카일루아 또한 실제로 작성된 첫 코드는 파서였다. 이번 글에서는 카일루아의 파서가 어떻게 발전했는지를 이야기해 보기로 한다.

카일루아, 하나: 태동​

이 글을 쓰다가 지우다 한지는 꽤 오래 되었다. 첫 문단만 쓰여 있는 초안은 2015년 12월 15일이라는 날짜가 박혀 있고, IRC 로그를 잠시 뒤져 보니 처음으로 이 일을 하고 있다는 얘기를 은연중에 언급한 건 2015년 12월 3일이었다. 이렇게.

<lifthrasiir> 아 오랜만에 PL 관련된 걸 하려니까 아무것도 기억나지 않음…

그로부터, 지난 4월 25일에 카일루아라는 물건을 발표하기까지는 16개월이 걸렸다. 그동안 나를 아는 여러 사람들은 이제 나루를 다시 만드시죠라는 무시무시한 개드립을 치고 있었는데, 한 언어가 설계되고 구현되고 다시 설계된 뒤에 재구현되는 과정을 거쳐 보니 나루는 참 꿈도 컸구나 하는 생각이 든다. 이 정도 언어 만드는 데 이 정도 노력이 필요한데 나루 같은 걸 어떻게 만들 생각을 했단 말인가…

나에게 카일루아는 두 가지 의미를 가지고 있는데, 하나는 앞에서 말했듯 언어를 만드는 모든 과정을 처음으로 내 손으로 다 거친 사례라는 점이고, 다른 하나는 한국에서 드물게 언어 관련해서 밥 벌어 먹고 산 사례였다는 점이었다. (나는 이런 류의 일을 한 번이라도 해 본 사람이 학계 제외하면 한국에서 두 자릿수 정도 되지 않을까 싶다.) 여기에는 마침 상대적으로 필요한 인력이 적어도 되는 시기에, 되면 훌륭하고 안 되어도 큰 부담은 없는 프로젝트가 있었고, 그걸 수행하기에 적합한 사람이 마침 손이 비어 있어서 맡겨도 되는 몇 가지 행운이 함께 따라 주었던 것 같다. 물론 반대로 보면 혼자 하는 프로젝트는 언제나 번아웃의 위험이 있고 실제로 몇 차례 겪기도 했지만, 그럼에도 불구하고 이런 드문 기회를 얻은 것은 긍정적으로 생각한다. 어쨌든, 뭔가 나오지 않았는가? :)

이 글은 카일루아 연작의 첫번째 글이 된다. 어떤 부분은 당연한 부분도 있을 수 있고 어떤 부분은 나만 그렇게 생각했을 수도 있지만, 이번 연작에서는 카일루아의 처음부터 끝까지를 모두 하나 하나 다뤄 보려고 한다. 읽는 사람한테나 그걸 쓰는 나한테나 쉽지 않은 연작일 수 있는데 아무쪼록 모종의 도움이 되었으면 하는 바람이다.

어제 이런 무서운 트윗을 보았다. 사람들이 자바스크립트에서 소숫점 두 자리로 반올림을 하려고 하는데, 웬 문자열 연산이 들어가고 이상한 코드들이 난무하는 그런 상황이었다. 그래서 쓰게 된 트윗이 바로 이것인데…

일반적인 조언: 소숫점 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에 추가 인자를 넣은 것 자체가 문제가 있지 않나 싶은데 뭐 루비는 국제 표준(…)이 되었으니 어쩔 수 없을 지도 모르겠다.


  1. 내가 들었던 반론 중 하나는 사람이 계산 과정을 따라가도록 로직을 설계해야 하는 경우 (예: 게임에서 사용되는 숫자) 반올림이 중간 중간에 강제로 들어가야 할 수 있다는 것이었다. 틀린 말은 아닌데 나라면 부동소숫점 안 쓰고 소숫점 n자리까지 나타낸 고정소숫점 형식을 쓸 것이다. ↩︎

  2. Unit in the Last Place, 해당 숫자에서 가장 가까운 다른 부동소숫점 숫자 사이의 거리를 나타내는 (상대) 단위. (두 거리가 다를 수 있는데 일단 같은 경우만 생각하자.) “올바르게” 반올림되었을 때 계산의 최대 오차는 ±0.5 ulp, 오차 범위의 크기는 1 ulp가 된다. ↩︎

요전에 한겨레에서 기사를 하나 본 뒤 어이가 없어져서 간만에 긴 글을 써서 기자한테 보냈더니 사람들의 반응이 꽤 좋았다. 왜 기사가 어이가 없는지에 대해서는 저 글을 참고하기로 하고, 여기에서는 언론 그 자체에 대해서 쓴 소리를 해 보기로 한다.

글을 읽어 본 사람들은 눈치챘을 지도 모르지만 사실 내가 짜증난 건 네이버 때문이 아니라 (네이버는 내가 안 쓰기도 하고 욕 좀 먹으면 고칠 가능성이 높으니까…) 한겨레 때문이었다. 좀 더 구체적으로는, 내 글은 “너네 글 쓰고 검수하고 하는 사람은 몇 명 안 되지만 그걸로 영향받는 사람들은 수천 수만명인데 좀 책임감이라는 게 있어야 하지 않겠니 그래도?”를 아주 정중하게 쓴 것에 가깝다. 여기에 대한 기자의 반응은 “우리가 틀려도 공론화가 되면 우리의 책임을 다하는 것이라 생각한다”1에 가까웠는데, 개인적으로는 이 반응이 더 어이가 없었다. 아 물론 JTBC가 태블릿 입수해서 공개하는 것 같이 공론화 그 자체로 언론의 역할이 되는 경우도 있는데… 그런 건 보통 특종이라고 부른다. 사실 자체만으로 사회를 움직일 수 있다는 확신이 생기는 그런 보도는 많지 않다.

좀 더 구체적으로 말하면, 내 사견으로 언론이 제공하는 컨텐츠는 크게 세 종류가 있다. 하나는 단순 정보의 제공이다. 이를테면 리빙 포인트 같은 것. 물론 이 역할은 언론의 근본적인 역할이라기보다는 언론이 가장 쉽게 접할 수 있는 활자 매체였던 시절에 하던 역할이 지금까지 내려 오는 것에 가까우며, 지금 와서 단순 정보만 제공하는 언론은 살아남기 힘들 것이다. 두번째는 공적으로 알려지는 것이 마땅하다고 여겨질 사건에 대해서 자료를 수집하거나 취재원을 찾아 다니는 것, 즉 사건의 탐사이다. 마지막으로 앞의 두 컨텐츠를 가공하여 독자에게 제공하는 과정에서 그것이 무엇을 의미하는지 관점을 제시하는 것이다. 이상의 세 종류의 컨텐츠는 분리되어 제공되기도 하고 적절히 섞여서 제공되기도 하는데, 해당 기사에서 한겨레는 네이버의 입장을 복붙해 버리면서 사건의 탐사에도 관점의 제시에도 실패했다. 그나마 어디처럼 HTTPS가 국제 표준이 아니라는 미친 소리는 하지 않아서 다행이지.

한겨레를 살짝 변호해 주면, 뭐 이게 다른 나라라고 상태가 좋은 건 아니지만, 특히 한국 언론은 광고주에 지나치게 휘둘릴 정도로 재정 상태가 좋지 않으며, 재정 상태가 안 좋을수록 (기자 수와 능력에 크게 의존하는) 사건의 탐사가 취약한 경우가 많다. 그러니 무슨 관점을 제시하고 싶어도 잘 될 턱이 없는 것. 이게 변호인진 잘 모르겠다… 하지만 이건 한겨레 사정이고 최근의 퀄리티 저하는 한겨레가 한걸레(…)라는 멸칭으로 불릴 정도로 심각한 상황인데, 여기에 위기의식을 가질 망정 우리가 공론화를 하면 오피니언을 이끌 수 있다는 안일한 자세가 드러나는 반응을 보니 뒷골이 좀 많이 땡겼다.

내가 이 건에서는 한겨레가 관련되어 있어서 한겨레를 깠지만, 사실 이런 시각이 한겨레에만 국한되어 있지는 않을 거라고 생각한다. 기실 한국에서 정말로 탐사보도를 제대로 할 수 있을 정도로 자본도 있고 거기에 호응하는 대중도 많은 언론은 JTBC 밖에 남지 않은 것 같고2, 여기에서 그런 시각을 가진다면 뭐 그럴 수 있겠다 싶지만 다른 데라면 아니올시다. 꽤 잘 나가는 신규 인터넷 언론들을 보면 이런 점을 인식하여 컨텐츠 특화를 많이 하는데, 이를테면 뉴스타파 같이 사건 탐사에 올인을 하거나, 슬로우뉴스 같이 자원이 많이 드는 사건 탐사를 피하고 오피니언에 집중하는 등의 접근을 볼 수 있다. 더 이상 오피니언 리더가 될 수 없다는 걸 인식했으면 이런 시도라도 해야 하는 거 아닐까? 아니면 옛날에 내가 제안했듯 매체를 최대한 활용해 보시거나.


  1. 주의. 나는 답장을 받긴 했으나 이 답장을 공개해도 된다는 허락을 미리 구하지도 않았고 필요성도 느끼지 않아서 답장을 공개하지 않는다. 즉 앞의 따옴표에 쓰여진 내용은 이 답장에 대한 나의 해석으로, 내가 제대로 답장을 해석한 것인지, 또는 내가 의도적으로 답장의 내용을 왜곡하는 것인지에 대해서 나를 믿어서는 안 된다! 뭐 그렇다고. ↩︎

  2. 물론 JTBC가 나쁘다는 건 아니지만 이건 매우 안 좋은 상황이다. 왜냐하면 어느 언론도 실수를 피할 수는 없기 때문이다. JTBC가 둘 중 하나를 깎아 먹을 정도의 실수를 해 버리면 백업 솔루션이 없다. 모두가 대안 언론을 보고 있진 않을 거 아닌가? ↩︎

안티바이러스​

한국에서는 모 제품의 영향으로 백신이라고도 많이 부르는 안티바이러스(AV) 소프트웨어는 옛날부터 필수적인 것으로 인식되어 왔다. 컴퓨터는 일반 목적의 컴퓨팅1 기기이니만큼, 자칫 실수하면 내가 원하지 않는 계산을 할 수도 있는데 AV는 이 상황을 해결하기 위한 방법 중 하나이다. 그런데 그것을 아시는가? 대부분의 AV 소프트웨어가 생각보다 보안적으로 취약하다는 것을? 농담같아 보인다면 해커뉴스에서 작년에 나온 AV 뉴스만 모아서 보자. 노턴 안티바이러스가 사정 없이 뚫린 것이 아마도 가장 대표적인 사건일텐데, 기사화만 덜 되었지 거의 모든 주요 AV 소프트웨어에서 강력한 취약점이 하나 쯤은 튀어 나왔다. 도대체 무슨 일이 일어난 걸까?

컴퓨터는 기본적으로는 결정론적으로 돌아가고, (외부의 사건 등을 모두 포함해서) 입력이 고정되면 출력이 똑같이 나온다는 것이 가장 큰 특징이다. 그래서 컴퓨터에서 문제가 생길 경우 그건 컴퓨터(하드웨어)가 잘못 해서 그런 게 아니고 거기에 있던 소프트웨어가 잘못 했거나 모니터와 의자 사이에 있는 사람이 잘못해서 그런 것이다. 그리고 문제를 미리 막냐(방어)와 문제가 터졌을 때 확산을 막냐(감지)로 다시 세분화하면, 여섯 가지 상황이 가능하다.

  1. 시스템이 문제여서 방어
  2. 시스템이 문제여서 감지
  3. 소프트웨어가 문제여서 방어
  4. 소프트웨어가 문제여서 감지
  5. 사람이 문제여서 방어
  6. 사람이 문제여서 감지

그래서 뭔 말을 하고 싶냐면, 원래의 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가 이걸 얼마나 성공적으로 수행하는지(와 그에 수반되는 개발자들의 비용)를 봐 왔다—더욱 더 중요해지는 것은 후자가 될 것이다. 보안은 공짜가 아니다.


  1. 우리가 그다지 인식하지 않을 뿐 “계산”(computation)은 매우 본질적인 개념으로, 우리가 머릿속에서 하는 모든 생각들도 계산이요, 우주의 입자들이 상호작용하는 것도 계산의 일종으로 볼 수 있다. 단지 우리가 계산이라고 하면 숫자로 하는 극히 단순한 계산을 가리키는 경우가 많아서 컴퓨팅이라는 미번역어를 잠깐 썼을 뿐. ↩︎

  2. 소비자 대상 AV 시장이 거진 무료화되면서, AV 벤더들의 주 수입원은 기업을 대상으로 한 보안 솔루션으로 바뀌었다. (소비자 시장은 여기에 써 먹기 위한 악성코드 데이터베이스 수집 등에 대신 쓰인다.) 그리고 아시다시피 소비자들은 뉴스에 민감하지만 기업은 뉴스가 있어도 계약 때문에 다른 벤더로 갈아 타기가 어렵다… 어차피 다 고만고만한 것도 한 이유 ↩︎

A Regular Crossword 풀이

그러니까, 옛날에 이런 정신나간 크로스워드 퍼즐1을 보고 와 미쳤네ㅋㅋㅋ 하고 실제로는 풀지 않고 넘어간 적이 있었다. 이걸 푸는 데 얼마나 시간이 걸릴지 조금만 해 보고 짐작할 수 있었기 때문이다. 근데 어제 누군가가 풀려고 하고 있길래… 낚여 버렸다.

처음에는 PDF 파일 위에다 텍스트를 올려 놓아서 편집을 하다가, 실수로 잘못 건드는 경우를 도저히 참을 수 없어서 (그리고 한 번 백트래킹을 할 뻔 해서…) 아예 각을 잡고 풀이 과정을 텍스트로 써 놓고 보니 왠지 공개하고 싶어졌다. 이 접근은 옛날에 스도쿠 퍼즐을 인간이 푸는 방법대로 푸는 프로그램을 본 적이 있어서 떠올린 것인데, 물론 적절한 알고리즘2을 쓰면 풀리기야 하겠지만… 원래 퍼즐이라는 건 자동으로 풀면 재미가 없는 것이니까. 그런 고로 뭐 이런 그지 깽깽이 같은 퍼즐을 어떻게 풀어 라고 생각한다면 스포일러를 당하고 더 풀지 않도록 하기로 하자.


  1. MIT Mystery Hunt 2013 문제 중 하나였다고 한다. ↩︎

  2. 대강 예상하기로는 정규식의 상태 기계를 역으로 추적할 수 있는 게 있으면 그냥 백트래킹으로 풀릴 듯 하다. 결국 손으로 풀 때도 현재 시점에서 올 수 있는 문자의 집합을 구하는 게 병목이었기 때문에. 이렇게 보면 어려운 스도쿠보다는 쉬운 문제일지도? (하지만 단서가 6방향으로 쓰여 있어서 목을 돌리는 게 힘들었다…) ↩︎


텀블러를 씁니다.