메아리 저널

공공재가 된 것들의 운명​

몇 달 전에 잘 알고 지내는 정 진명 군이 이런 트윗을 올린 적이 있다:

갑자기 생각나는 것은 한국어 문화 향유자들의 공동 재산이라고 생각했던 것들이, 놀랄 정도로 단순히 어떤 개인에 부담이 강하게 걸리는 정도인 상태로 존속되고 있다는 것이다. 모 IRC 네트워크부터…

내가 그 모 IRC 네트워크를 만든 사람인지라 이 트윗은 꽤 뇌리에 깊게 박혀 있었는데, 그저께(2013-04-16)의 사건을 보니까 이 트윗이 함의하는 바가 생각보다 넓은 듯 하다. 사실 이 사건이 꽤 넓은 부류의 사람들에게 영향을 미쳐서 이리 시끄러워진 것 뿐이지, 한국 인터넷에서 이런 종류의 일들은 흔히 일어난다. 대강 살펴 보면,

  1. 어떤 사람이 그냥 심심해서, 또는 재미로, 또는 아는 사람들끼리만 쓸 목적으로 사이트를 만든다.
  2. 사이트가 무럭 무럭 커간다.
  3. 사이트가 개인의 역량을 넘어갈 정도로 커진다. 이 시점에서도 운영자는 사이트의 목적을 본래 만든 목적으로 간주하고 있으며 그렇게 운영한다.
  4. 사이트 안팎에서 사건이 터진다. 운영 미숙으로 인한 사건일 경우도 있지만 사실 어떤 사건이든 상관 없고 충분히 파급력이 있기만 하면 된다.
  5. 운영자는 지탄을 받고 그제서야 지나치게 커진 사이트를 어떻게 할지 생각하기 시작한다. 크게 다음과 같은 선택지가 있다.
    • 사이트를 폐쇄한다. 보존주의자한테는 아주 짜증나는 일이긴 하지만 개인에게는 가장 속 편한 선택일 것이다.
    • 사이트를 폐쇄에 가깝도록 방치한다. 대부분의 경우 서서히 잊혀지지만, 이미 사용자가 많을 경우 안 그럴 수도 있다.
    • 사이트 운영을 도울 사람들을 모은다. 다만 이 사람들이 나중에 문제가 되는 경우도 있고, 신뢰할 만하거나 기술적으로 도움을 줄 수 있는 사람이 별로 없을 수도 있다.
    • 사이트를 다른 개인이나 기업 같은 다른 운영 주체에게 넘긴다.
    • 사이트를 부업 또는 전업으로 운영할 수 있도록 상업화한다. 보통은 욕 먹는 지름길이다.
    • 아주 드문 사례로, 사이트의 운영 주체가 크게 중요하지 않은 경우 사이트를 누구나 운영할 수 있는 형태로 배포하는 경우도 있다. 대형 사이트에서는 보기 힘들다.

이는 내가 “온라인 커뮤니티의 사이클”이라 부르는 일반적인 시나리오의 앞부분에 해당한다. (뒷부분은 지금 얘기하기에는 굉장히 길다.) 몇 가지 실제 사례를 통해 이게 얼마나 일반적인지 느껴 보자.

IRC 네트워크

IRC 네트워크는 IRC라는 공개된 프로토콜을 써서 특정 주제에 대한 채팅을 즐길 수 있는 서비스…라는 건 훼이크고 그냥 온라인 채팅 커뮤니티이다. 그래서 일반적인 “웹 사이트”가 아님에도 불구하고 앞에서 언급한 시나리오를 꽤 충실하게 따르는 경우가 많다.

많이 아시겠지만 HanIRC라고 평균 1만명 정도가 사용하는 한국 최대이자 사실상 유일하게 남은 IRC 네트워크가 있다. (다른 주요 네트워크였던 단군넷은 몇 년 전에 망한 듯 하다.) 이 네트워크는 2008~2009년에 이른바 HanIRC 지라인 사건이라 불리는 일에 휘말렸는데, 간단하게 말하면 불법 파일 공유를 이유로 꽤 많은 사용자들이 차단당했는데 소명 기회를 갖지 못한 일부 사용자들이 이를 문제삼자 무시하고 해당 사용자들을 다시 차단한 사건이다. 그냥 보면 운영진이 잘못한 것 같아 보이지만, 이게 간단하지 않은 문제인데는 다 이유가 있는 법이다:

  • 운영진들의 일반적인 견해는 “HanIRC는 개인 목적으로 운영되고 있다”는 것이었다. 실제로 IRC 네트워크는 비교적 적은 서버 자원으로 운영할 수 있기 때문에, 개인 목적으로 서버 한 대에서 돌리는 걸 사용자 수가 수백~수천명이 되도록 그대로 유지하는 것도 가능하다(실제로 HanIRC는 현재 4대의 서버를 쓴다고 알려져 있다).
  • HanIRC는 그냥 보면 운영진들이 꽤 많은 걸로 보이지만, 실제로는 1~2년 활동하고 잠수를 타는 사람들이 많았다고 알려져 있다. 의사 결정을 할 수 있는 활동적인 운영진은 한정되어 있으니(매 순간에 활동하는 운영진 숫자를 한 손으로 꼽을 수 있을 정도라고…), 소명을 위해 필요한 자료 수집이나 각종 판단은 자연히 어려워지게 된다. 물론 당연하지만 HanIRC 운영진 중 직업적으로 HanIRC를 운영하는 사람은 한 사람도 없다.
  • 결정적으로, 그 몇 안 되는 운영진들이 HanIRC 관련해서 몇 번 데여 본 적이 있기 때문에 법적인 이슈에 대해서는 보수적인 정책을 취할 수 밖에 없었다.

그래서 꽤 많은 사람들이 HanIRC에 대해 반발했던 것과는 달리 나는 왜 그렇게 행동하는지 이해는 할 수 있었다. 다만 그 행동이 좋다는 말만은 할 수 없었다.

이 사건은 최종적으로는 오징어 IRC 네트워크가 만들어지는데 결정적인 역할을 했다. 사실 UTF-8 지원을 비롯한 이런 저런 기능이 필요해서 IRC 네트워크를 만들 필요성은 느끼고는 있었다만. 우리는 HanIRC의 사례를 반면교사 삼아 몇 가지 정책을 세웠다.

  • 네트워크 전체에 명백히 피해를 주는 것이 아닌 채널 안에서의 분쟁은 운영진이 개입하지 않는다.
  • 닉 등록이나 채널 등록과 같은 편의 기능은 최대한 사람 손이 갈 필요가 없도록 만든다.
  • 서버 통계나 옵 복구 등의 기술 지원을 제외한 용도로 관리 권한을 쓰지 않는다. (그래서 나는 심지어 그 흔한 /LIST도 사용하지 않는다.)
  • 기타 서버 관리, 예를 들어 특정 IP에 대한 제한 완화 등은 위 정책에 어긋나지 않는 수준에서 재량에 맡긴다.

뭐, 지금 살펴 보면 완벽한 건 아니었다. 예를 들어 초창기에 서버 관리자를 확충하던 과정에서 이상한 사람이 들어 와서 권한을 제거해야 했다거나(전적으로 내 불찰이라고 생각한다), 사람 손이 안 가는 관리 시스템을 만드는 데 사람 손이 필요해서(…) 한동안 시스템이 돌아만 가는 수준에서 방치되고 있었다거나, 서비스 개발이 가능한 사람을 확충했더니 서비스 변경으로 인한 불만 사항이 좀 접수된다거나(어쩔 수 없다고 보고 있음) 뭐 그런 것들. 그래도 아예 분쟁에 개입하지 않겠다고 못박으니 기술적인 이슈에만 신경쓸 수 있어서 부담이 덜하다. 앞으로는 어쩔지 모르겠지만.

한편 다른 대규모 IRC 네트워크는 어쩐가 하면, 상황은 크게 다르지 않다. 사실 IRC의 초창기에는 모든 서버가 하나의 네트워크로 연결되어 있었기 때문에 할 수 있으면 서버 연결로 기여를 하고 못 하면 내리고 하는 게 일반적이었는데, 네트워크가 여럿으로 쪼개진 지금도 딱히 크게 다르지는 않다. 여기의 대표적인 예외로 Freenode가 있는데, Freenode의 경우 자유 및 오픈 소스 소프트웨어를 지원하기 위해 만들어졌기 때문에 PDPC라 하는 비영리 단체를 통해 기부를 받는 식으로 서비스를 유지해 왔다. 다만 PDPC의 경우 설립자가 꽤 강하게 밀어 붙였고 그 과정이 항상 순탄치만은 않았다는 점을 감안할 필요는 있다.

리듬게임 기록 서비스

리듬게임은 장르 특성상 게임을 이전에 비해 얼마나 잘 하게 되었는가에서 즐거움을 찾는 사람들이 많다. 그래서 자기의 역대 플레이 기록이나 가장 좋은 플레이 기록을 어떤 형태로든 보존하는 수요가 높았고, 특히 코나미의 웹 서비스를 크롤링하거나 해석해서 좀 더 보기 좋은 형태로 기록을 보여 주는 사이트는 우후죽순으로 생겨나게 마련이었다.

유비트의 경우 이러한 사이트의 대표적인 예제로 유비그래프유비트인포가 있는데, 지금 와서는 그렇게 비밀은 아닌 것 같지만 난 유비트인포의 서버를 지속적으로 제공해 왔고 일부 버전에서는 개발에 참여하기도 했다. 유비트인포는 지금은 거의 망(…)했지만 한때는 사용자가 6천명을 넘기고, 한국에서 유비트 하는 사람들은 대부분 사용하는 나름 규모가 큰 서비스였었다.

유비트인포의 역사 또한 위의 시나리오에 정확히 부합한다. 실제 사건과 맞춰서 나열해 보면,

  1. 내 서버를 사용하던 사람 중 누군가가 iOS 앱에 사용할 목적으로 사이트를 만들었다. 이게 유비트 리플즈 시절.
  2. 유비트 니트가 나올 무렵게 마침 나도 유비트를 하는데 사이트가 좀 거지같으니 잘 고쳐 보자(…) 하고 나도 소스 코드에 손을 대기 시작했다.
  3. 이 당시 유비그래프는 코나미의 기록 열람 유료화 때문에 직접 크롤링을 못 하고 있었기 때문에, 많은 사람들이 크롤링을 자동으로 해 주는 유비트인포를 대신 쓰게 되었다. 이 흐름은 유료화가 풀린 유비트 코피어스에 와서도 크게 바뀌지 않았다.
  4. 개발에 참여했던 사람들이 모조리 생업에 바빠지면서 유비트인포의 운영이 느슨해지기 시작한다. 사실 유비트인포 코피어스에서 체감할 수 있는 유일한 변화는 디자인 변경 뿐이었다.

그 다음에 무슨 일이 일어났냐고? 우리는 유비트의 다음 버전인 소서가 나오는 대로 PHP로 만들어진 낡디 낡은 유비트인포 소스 코드를 버리고 파이썬으로 재작성하기로 했다. 사실 나 말고 다른 개발자가 프로토타입으로 파이썬 코드를 짜긴 했으나, 서버 설정 이슈도 있고 깔끔하지만 문제가 제법 있는 소스 코드보다는 우선은 잘 돌아가는 더러운 코드가 낫다고 내가 강력히 주장하여 보류되었던 것이었다. 근데 정작 소서가 나올 즈음에 개발자들 전원이 매우 바빠져서 개발을 진행할 수 없었고, 그대로 한 두 달 정도 사이트가 방치되니 사람들이 대체제를 만들기 시작하더라(…). 그렇게 해서 나온 것이 지금의 소서게이트 같은 거다.

사실 유비트인포의 결말(?)은 어느 정도 이전에 예견되어 있었다. 리플렉 비트 라임라이트가 나올 적에 우리는 (가칭) 리플렉인포도 만들려고 준비하고 있었으나 이런 저런 이유로 빠르게 진척되지 않았다. 그래서 리플렉인포가 정말로 런칭되었을 때 우리는 놀고만 있지 않았다는 걸 보여 주기 위해 당시 작성하던 코드를 대강 대충 마무리해서 급하게 런칭해야 했는데, 안타깝게도 리플렉의 경우 곡 정보 추가를 수동으로 하는 게 상상 이상으로 귀찮아서1 완벽하게 망하고야 말았다.

지금 생각해 보면 나와 다른 개발자 사이의 입장차는 꽤 명확했다. 나는 “우리가 잘 할 수 있는 것을 계속 유지하자”는 입장이었고, 다른 쪽은 “일단 시작은 해야 다음을 볼 수 있다”는 입장이었으니까. 그래서 나는 문제의 RBinfo(저 쪽 리플렉인포와 혼동되지 않기 위해 이름을 바꿨다)를 그렇게 좋아하지 않았고 어느 시점에서는 유지도 안 되는 거 그냥 폐쇄하자고 했지만 어느 쪽도 실제로 폐쇄를 시킬 여력은 이미 사라진 상태였다.

IRC 네트워크와는 달리, 유비트인포가 방치된 이후에도 사실 아무 것도 바뀐 건 없었다. 그런 점에서 나는 유비트인포가 망한 건 안타깝긴 하지만 어쩌면 이게 더 올바른 길이 아니었을까 하는 생각을 하게 되었다. 대체 가능한 서비스라면 그 서비스를 당장 잘 운영할 수 있는 사람이 운영하는게 더 나았을 거라는, 그런 종류의 생각 말이다. 소서게이트도, 리플렉인포도, 그리고 그 밖에 소규모로 운영되는 꽤 많은 서비스들(유비트만 해도 이러한 서비스가 최소 서너개는 있는 걸로 안다)까지, 결국 우리가 하지 못 했던 일을 아쉽게나마 해 주고 있었다. 그래서 유비트인포 소서가 뒤늦게 시작할 때도 더 이상 개발에 참여하지 않기로 했다. 이미 나는 내가 감당할 수 있는 일은 다 했으니까.

위키

당연하지만 위의 시나리오는 위키백과 같은 위키 사이트에도 적용된다. 단지 위의 사건들과 다른 점이 있다면, 위키백과는 위기를 제대로 극복해 냈다는 점이다. 위키백과가 비판받는 점은 위키백과의 특성으로 인해 생기는 복잡한 규칙과 사건들이지 위키백과의 운영이 개판이라는 종류의 지적은 별로 존재하지 않는다. 오히려 운영이 깐깐해서 문제가 되었다면 모를까.

위키백과는 본래 누피디어라는 온라인 백과사전 사이트에서 시작되었다. 누피디어는 인터넷이라는 매체를 쓰고 강력한 동료 평가(peer review)를 한다는 점에서 지금의 위키백과와는 많이 달랐는데, 동료 평가 과정의 일부를 사용자 참여로 하게 하려고 위키를 도입했던 것이 주객전도가 된 것이다. 그래서 위키백과 극초창기의 충돌은 창립자인 지미 웨일스와 누피디어의 편집장이었던 래리 생어의 충돌로 표현할 수 있다. 결과는 래리 생어가 나가서 새로운 백과사전 프로젝트를 만드는 것으로 끝났고, 다들 알다시피 그 프로젝트는 망했고 위키백과는 세계에서 가장 큰 백과사전이 되었다.

여기까지 읽어 보면 지미 웨일스를 위인으로 불러도 손색이 없을 것 같아 보인다. 하지만 그의 행적을 좀 더 자세히 살펴 보면 그가 오로지 공공의 이익만을 위해 이 일들을 해 왔다는 생각에는 회의가 들게 마련이다. 예를 들어 기본적으로 위키백과에서 자기 자신에 대한 문서를 수정하는 건 금기시되는데, 그는 자기가 운영했던 사이트에 대한 내용을 고쳐 쓰거나(그가 공동 창립자이며, 누피디어를 운영했었던 보미스는 사실 포르노 검색 등등을 갖춘 남성향 성인 포털 사이트였다) 위키백과의 초창기에 결정적인 기여를 했던 것으로 판단되는 래리 생어를 공동 창립자가 아니라고 자의적으로 바꾸면서 욕을 꽤나 먹었다. 그 밖에도 그가 위키미디어 재단을 운영하는 방식에 대한 비판이 제법 있었는데, 특히 2010년에 음란물로 간주될 수도 있는 이미지를 허용할 것이냐에 대한 논란에서 공동체를 무시하고 이미지를 대거 삭제하면서 욕을 제곱으로 먹었다. 결국 그는 이 사건을 계기로 위키백과의 기술적인 권한을 대부분 반납하게 되었다.

이 일련의 사건들이 시사하는 것이 무엇인가? 위키백과가 성공한 것은 이러니 저러니 해도 결국 위키백과를 이끌어 나가는 일련의 기여자들의 참여에 따른 것이었다. 게다가 그들은 지금까지 언급했던 대부분의 사례와는 달리 위키미디어 재단을 비롯한 “운영자”로 간주될 수 있는 사람들을 효과적으로 감시할 수 있었다. 그렇기 때문에 심지어 프로젝트의 창립자이며 자칫하면 신성 불가침의 영역으로 여겨질 수 있는 웨일스조차도 비판하고 그가 바람직하지 않은 행동을 할 때 거기에 따른 제재를 내릴 수 있는 힘이 있었다. 여기에는 위키미디어 재단이 비교적 빨리 만들어진 것도 한몫했다(프로젝트 시작 후 2년만인 2003년에 위키백과의 상업화를 원천적으로 금지하기 위해 설립되었다). 이 때문에 위키백과는 민주주의가 아님에도 불구하고 위키백과에서의 활동은 민주주의의 이상향—투표보다 총의를 바탕으로 하는 사회—과 많이 닮아 있다.

엔하위키

위에서 세 가지 예제를 들긴 했지만, 사실 앞의 시나리오는 거의 모든 한국 사이트에도 적용되는 일이다. 그럼에도 불구하고 굳이 저 세 가지 예제를 든 이유는 바로 이들 사이트가 인터넷 상에서 일종의 공공재로 기능하기 때문이다. 그리고 한 가지가 아닌 굳이 세 가지 예제를 든 이유는, 각각의 예제가 i) 운영진의 독단이 유지되는 사례와 ii) 대체재가 나타나는 사례와 iii) 운영진이 커뮤니티로 교체되는 사례를 잘 보여 주고 있다고 생각하기 때문이다. 특히 내가 주목하는 것은 세번째 사례이며, 그리하여 우리는 이 글의 원래 주제로 되돌아간다.

엔하위키를 명백한 공공재로 생각하는 사람으로서, 그리고 이런 저런 이유로 공공재에 속하는 서비스를 몇 개 운영했던/운영하고 있는 사람으로서, 나는 엔하위키가 처한 딜레마에 대해 십분 공감하면서도 다시금 되물을 수 밖에 없다. 엔하위키의 운영진들은 엔하위키가 앞으로 어떻게 되길 원하는가? 그들이 운영할 수 있는 수준의 적절한 사이트가 되길 원하는 것인가? 서브컬쳐를 지원하고 부흥시키기 위한 전진 기지와 같은 사이트가 되길 원하는 것인가? 아니면 단순히 운영진들에게 모종의 이익이 되는 사이트가 되길 원하는 것인가? 어떤 답이 나오든간에 내가 그 답에 대해서 뭐라 할 자격은 없다(사실 난 엔하위키가 공공재가 되지 않고 싶어하고 거기에 맞도록 행동한다 해도 불만을 가지진 않을 것이다). 하지만 여기에 대한 답이 제대로 나오지 않는 한, 엔하위키가 당장 처한 문제가 커뮤니티의 행동을 통해 회피되거나 해결될 수 있을 거라고 생각하진 않는다.


4월 18일 0시 50분 추가: 아, 클리앙에서 내 글을 퍼간 것 때문에 이슈가 된 것 같은데, 내 글을 퍼가는 건 별로 괘념치 않지만 갱신되는 건 웬만하면 좀 제대로 따라가 줬으면 좋겠다. 또한 내 글에 대해 의문이나 반론이 있으면 메일로 보내 주시면 시간 나는 대로 응답해 드리겠다. (왜 블로그같이 생겼는데 댓글이 없는지에 대해서는 이 글을 참고.)


  1. 사실 유비트인포도 마찬가지였다. 유비트인포 소서에서 코드 재작성을 하려 했던 이유 중 하나는 기본적인 곡 정보를 자동으로 받아 올 수 있게 하기 위해서였다. 

CSON: Cursive Script Object Notation

해커뉴스에서 때아닌 TOML 열풍이 불길래 좀 보다가, 도대체 이 사람이 무슨 생각으로 TOML이 모호하지 않느니 술 드립을 치느니 하는지 알 수가 없어서 IRC에서 열심히 까던 도중 그럼 어떻게 만들어야 제대로 만드는 건지 얘기하다 정신을 차려 보니 밤 2시 반이 되어 있고 기괴한 이름을 가진 데이터 포맷 스펙(의 초안)이 나와 있다. 이런.

그래서 간단하게 말하자면, CSON은 JSON의 상위 집합인 동시에 항상 JSON으로 변환 가능한 문법 설탕(syntactic sugar)의 모음이다. TOML이 하나 잘 한 것, 즉 설정 파일에 쓸 수 있도록 고려했다는 것을 벤치마킹하여 JSON의 좀 번잡한 문법을 간소화시켰고, JSON을 손으로 짤 때마다 짜증나서 미칠 것 같던 작은 따옴표로 문자열 쓰는 거라거나, 배열/오브젝트의 마지막 콤마를 항상 쓰지 말아야 한다거나, 여러줄 문자열 쓰기가 정말 힘들다거나 하는 것들을 고쳐 놓고 나니 은근히 괜찮아 보이는 스펙이 나와 있다. 기타 설계 상의 선택은 일단 내가 생각나는 대로는 다 썼다고 생각한다(까먹었을 수도 있다).

뭐, 구현체가 없으니 아직은 별로 쓸만한 건 아니지만, 그래도 관심 있으신 분께서는 좀 읽어 보시고 질문이나 요청이나 개선안이나 비판 같은 걸 해 주시면 감사하겠다(댓글은 없지만 메일은 읽어 본다). 구현체도 만들어 주시면 아주 감사하고(사실 TOML 까는 이유 중 반 정도는 GitHub 공동 창업자라는 타이틀을 가지고 뭔가 덜 여문 포맷을 자기 이름 붙여서 구현체 만들도록 유도하는 것도 있다). 일단 나도 이번 주 안에 뭔가 만들어 보긴 할 것 같다.

엔하위키 미러의 법적 문제​

나는 물론 변호사가 아니고, 저작권법에 대해 아주 잘 아는 건 아니다. 하지만 다년간의 위키백과질을 비롯한 이런 저런 삽질을 통해 저작권에 대해 주워 들은(…) 건 많은데, 그렇기에 최근 들어 제기된 엔하위키미러에 대한 위키 게시판의 여론은 개인적으로 매우 염려스럽다. 전후 사정이 좀 복잡하니, 맥락에 대해서는 kwj2772 님의 글을 참고하길 바란다.

간략히 설명하자면, 최소한 내가 알기로 엔하위키 측에서는 엔하위키 미러를 폐쇄하도록 요구할 권리는 없다. 엔하위키한테 최대한 호의적으로 가정을 한다 하더라도 다음과 같은 시나리오 이상의 결과는 안 나온다.

  1. 엔하위키는 어떤 저작물 A를 크리에이티브 커먼즈 저작자표시-비영리-동일조건변경허락 2.0 대한민국 라이선스(이하 CC-by-nc-sa)에 따라 공중(이 경우, 인터넷)에 공개했다.
  2. CC-by-nc-sa 3장 c항을 비롯한 주변 조항들에 따라, 엔하위키 미러는 저작물 A를 배포·전송·전시·공연할 권리를 부여받았으며 따라서 저작물 A를 복제하여 자기 사이트에 공개했다.
  3. 이제 엔하위키가 저작물 A의 배포를 중단했다. 이는 CC-by-nc-sa 6장 b항에 의해 저작권자에게 부여된 정당한 권리이다.
  4. 그러나 CC-by-nc-sa 6장 b항을 잘 보면1 저작물 A에 부여되었던 이용 허락은 철회되지 않았다는 걸 알 수 있다. 따라서 이 시점에서도 엔하위키 미러는 저작물 A를 기존 조항 그대로 사용할 수 있다.

즉, 엔하위키가 저작물을 모조리 날리거나 라이선스를 바꾸는 상황이 와도 엔하위키 미러가 기존에 크롤링했던 문서에 대한 이용 허락은 바뀌지 않는다! 사실 문제의 6장 b항은 저작권자가 이용 허락을 받은 측에게 횡포(…)를 부리는 걸 막기 위해 의도적으로 추가된 구절이니 당연한 결과이다. 이러한 보호 조항은 다른 라이선스에도 흔히 나타난다(예를 들어서 GNU GPLv3 8장). CC-by-nc-sa의 경우 이용 허락이 종료되는 방법은 단 두 가지 뿐인데, 하나는 원 저작권이 소멸되었을 경우고, 다른 하나는 이용 허락을 받은 측이 라이선스를 어겼을 때 뿐이다.

자, 이제 좀 더 재밌는 가정을 해 보자. 엔하위키가 만약 정말로 미러를 폐쇄하고 싶을 경우 한 가지 대안이 있는데, 엔하위키 미러에 달려 있는 광고를 CC-by-nc-sa의 비영리 조항의 위반으로 간주하여 이용 허락을 종료시키는 것은 가능하다. 하지만 이것도 문제가 있으니, kwj2772 님의 글에서 지적했듯 엔하위키 또한 광고가 붙어 있기 때문이다. 물론 엄밀히 말하면 저작권자는 자기가 그 저작물을 사용할 때는 자기 맘대로 이용 허락을 따로 내리는 것이 가능하지만, 문제는 엔하위키는 (그 쪽에서 착각하고 있는 것과는 달리) 사실 저작권자가 아니다! 엔하위키가 실제로 가지고 있는 권한은 다음 두 가지 뿐이다.

  1. 실제로 글을 쓴 저작권자에게 CC-by-nc-sa나 호환되는 라이선스를 쓰도록 요구할 권리
  2. CC-by-nc-sa에 따른 저작물의 배포를 제어할 권리

엔하위키가 저작권자가 아닌 가장 결정적인 이유는 명시적인 저작권 재지정을 받은 적이 없기 때문이다. 물론 익명 기여자의 경우 저작자의 표시(attribution)를 포기한 것을 저작권의 포기로 받아들일 수도 있긴 하겠지만, 그런 게 아니면 저작권은 원래 글을 쓴 바로 그 사람에게 있지 어디로 가지 않는다. 또한 엔하위키는 저작권자로부터 저작물을 다른 라이선스로 재라이선싱할 권한도 갖고 있지 않으니(CC-by-nc-sa의 경우 4장 a항에서 불가능함을 명시하고 있다), CC-by-nc-sa가 허용하는 수준에서만 저작물을 배포할 수 있게 된다. 엔하위키 미러가 라이선스 위반으로 폐쇄된다면, 엔하위키도 같은 이유로 일부 기여자들의 항의를 통해 폐쇄되는 것이 불가능하지만은 않다.2 적반하장인 셈.

실질적으로 엔하위키가 미러를 없애고 싶다면 취할 수 있는 행동은 아이피를 막는 것 하나 밖에 없다. 이는 앞에서 언급했듯 CC-by-nc-sa 6장 b항에 따라 허가되며, 청동씨가 착각하는 것과는 달리 라이선스의 변경이 아니다. 애초에 라이선스를 바꿀 거였으면 모든 기여자들을 찾아 다니면서 라이선스 변경에 찬성해 주십시오 하고 바짓가랑이를 붙잡거나, 위키미디어가 그랬듯이 라이선스 자체를 고쳐 버리거나 했어야 한다(후자는 물론 매우 어렵다). 그리고 아이피를 막은 뒤에도, 만약 미러가 서버에 부하를 안 주는 방법으로 미러링을 시도할 경우(어렵지만 기술적으로 안 되는 수준은 아니다) 그걸 막을 방법은 전혀 없다. CC-by-nc-sa는 사람을 가려서 이용 허락을 해 줄 수 없기 때문이다.

여담으로 하는 얘기지만, 한때 엔하위키와 엔하위키 미러의 내용이 서로 동기화가 안 되어 있거나, 심지어 엔하위키에는 있는 내용이 미러에는 없던 적이 종종 있는데 이 또한 내 판단으로는 CC-by-nc-sa에서 큰 문제가 없다. 후자는 엔하위키 미러가 제공하는 것이 원 저작물이 아닌 편집저작물이고, 이 편집저작물은 CC-by-nc-sa 3장 a항에 따라 명시적으로 허용된다는 점에서 문제가 없다. (뭐 왜 짤렸는지에 대한 설명이 있으면 좋겠지만 강제 사항은 아니다.) 전자는 일단 동기화가 안 되는 것 자체는 앞에서 언급했듯 별 문제가 없고, CC-by-nc-sa 4장 e항에서 언급하듯 “저작자의 명예훼손에 해당할 정도로” 동기화가 안 되어 있을 경우에나 문제가 되겠지만 내가 알기로 이 정도로 심하게 어긋난 적은 없다. 그리고 어차피 저작물의 전송으로 인한 법적인 책임은 기본적으로 전송한 사람에게 있기도 하다. 물론 한국의 저작권법도 DMCA 같이 법적인 소지가 있을 수 있는 저작물을 요청 하에 삭제하는 것으로 그 책임을 면제받는 조항이 존재하고, 실은 이 조항이 내가 아는 한 엔하위키 미러에서 일부 문서가 사라진 바로 그 이유이기도 하다. 누가 신고를 먹였다고 하더라.


원래는 별도의 글로 쓰려고 했던 내용이지만(이거 분명 1년 전부터 쓰고 있었는데…) 요약하면, 내가 엔하위키를 바라보는 관점은 명백하다. 엔하위키 운영진들은 자기들이 얼마나 중요한 일을 하고 있는지, 그리고 그들의 선택이 얼마나 큰 영향을 미치는지에 대해서 잘 모르고 있는 것 같다. 엔하위키를 그냥 좀 많이 큰 잡학·오덕 위키로 보면 그들의 선택이 크게 비판받을 점이 있는 것은 아니다. 어쨌거나 관계자들이 요청하니 작성금지는 늘 수 밖에 없고, 자기네 일도 아닌데 질문은 계속 들어 오니 미러를 싫어하진 않을 망정 좋아할 리는 없으며, 서버 비용 많이 드니 설령 위와 같은 법적 문제가 있을 수 있어도(인지하고 있는진 잘 모르겠다) 광고를 안 달 수는 없었을 것이다. 하지만 그들이 하고 있는 일이 대한민국 서브컬쳐의 꽤 큰 부분으로 자리잡고 있다는 걸 생각하면 좀 더 진중한 접근을 취했어야 하지 않을까? 뭐, 이건 한 명 내지 소수의 집단만으로 운영되는 모든 프로젝트의 숙명일지도 모르겠다. 당장 나부터도 그렇고.


2013년 2월 15일 11시 추가: 위키 게시판에 누가 저작권 관련 장문의 토론을 벌인 모양이다. 엔하위키 측이 CC를 변경하려는 움직임을 보이고 있다는 관측이 많았는데, 여기에 대해서 엔하위키 측의 입장은 CC를 고치려는 게 아니라 배포에 제약을 가하는 것임이 명확해진 걸로 보인다. 이에 따라 글 살짝 수정.


  1. 한국어 번역본의 “이용허락자는 언제든지 본 이용허락과 다른 조건으로 저작물에 대하여 이용허락하거나 저작물의 배포를 중단시킬 수 있는 권리를 가집니다.”라는 문장은 사실 다소 문제가 있는 번역이다. 왜냐하면 뒤의 문장이랑 비교해 보면 알겠지만, 저작권자는 자기가 배포하던 것을 중단할 권리만을 갖기 때문이다. 영문(이 쪽은 7장 b항)의 “… to stop distributing the Work …”과 비교해 보면 뉘앙스 차이를 확연히 느낄 수 있다. 

  2. 실은 엔하위키 미러가 광고를 먼저 포기할 경우 엔하위키만 폐쇄되고 엔하위키 미러는 폐쇄되지 않는 것도 가능하다. 이는 CC-by-nc-sa 6장 a항에 따라 1차 배포자(이 경우, 엔하위키)의 이용 허락이 종료되었더라도 그로부터 배포를 받은 2차 배포자(이 경우, 엔하위키 미러)의 이용 허락은 종료되지 않기 때문이다. 아, 물론 광고가 상업적 이용에 해당하는지 아닌지에 대한 논의는 꽤 복잡한 문제이다. 

C를 위한 확장 가능한 배열

새해 복 많이 받으시길! (참 일찍도 말한다…) 연말과 연시에 걸쳐서 신상에 변화가 있는 까닭에 글을 거의 못 썼는데, 결론부터 말하면 아이플래테아로 이직을 하게 되었다. 딱히 회사와 안 좋은 일이 있어서 이직을 하게 된 건 아니고, 일하는 스타일의 차이 때문에 고생을 좀 한 게 있어서 회사와 상의 끝에 결정한 것이다. 뭐 신상은 신상이고 글은 글이니, 사족은 여기서 끝내고 본론으로 들어가겠다.


이직 때문에 한 달 정도 시간이 비어서 못 하던 해피 해킹을 몰아서 하고 있는데, 그 중 가장 독보적인 것은 역시 앙골모아가 아닌가 싶다. 잘 하면 올해 안에 2.0을 낼 수도 있을 듯(…). 앙골모아 2.0 alpha 1 이후 코드를 엄청나게 뜯어 고치긴 했는데 그 와중에 들어간 것이 일반화된 자료구조였다.

C++라면 모르겠지만, C는 의외로 없을 법한 것들이 있는 반면(앙골모아가 scanf를 쓰는 방법을 유심히 살펴 보라) 있을 법한 것들은 거의 없는데 그 중 하나가 자료구조에 관련된 것이다. 뭐 C++의 템플릿이나 다른 언어의 제너릭 같이 일반화된 코드를 짜기 매우 귀찮은 언어 특성 때문이겠지만, 그래도 확장 가능한 배열(크기를 바꿀 때 자동으로 realloc을 해 주는) 같은 것도 없는 건 좀 많이 귀찮다. 무슨 AVL 트리 같은 게 필요한 것도 아니고, 대부분의 realloc 수요는 (심지어 문자열도) 사실 저것과 qsort/bsearch로 대부분 퉁칠 수 있으니 나쁘지 않은 투자일 것 같은데 말이다. 앙골모아의 경우에도 확장 가능한 배열은 많이 필요한 반면, 다른 자료구조는 사실 그렇게 많이 필요하진 않았다.

어쨌든 없으니까 넣어야 하는데, 처음에는 Sean Barrett의 stb.h를 좀 참고하고 있었다. 혹시 C 프로그래밍을 하는데 이 코드를 모르신다면 한 번 정독을 하실 걸 권한다. (거의 생각 가능한 모든 것들이 들어 있고, 심지어 TiddlyWiki로 문서도 있다.) 그런데 보아하니 이게 C 표준에서 정의되지 않은 동작(undefined behavior)을 유발할 우려가 있어서 한참 생각하다가 포기하고, 몇 가지 대안을 찾다가 최종적으로는 옛날에 이상한 프로그래밍 언어 구현에 짜 넣은 간단한 코드를 수정 및 재활용해서 만든게 링크되어 있는 바로 그 코드이다. 오늘은 이 코드에 대해서 얘기해 보기로 하겠다.

인터페이스 설계: 1차 시도

사실 메모리 관리라는 측면에서 보면 확장 가능한 배열은 매우 간단한 축에 속한다. 크기 바꿀 수 있고 안의 원소 포인터를 받을 수 있으면 장땡인 것이다. 포인터는 크기를 바꾸는 와중에 메모리가 재할당될 경우 무효화될 수 있으며, 그렇지 않으면 무효화되지 않는다. 고로 확장 가능한 배열은 할당된 크기(이하 alloc), 사용하는 크기(이하 size), 그리고 할당된 메모리에 대한 포인터(이하 ptr) 세 가지 요소로 구성된다. 타입 정보가 없다면 각 원소의 크기(이하 itemsize)도 합쳐서 네 가지. 기본적으로 allocsize보다 크거나 같으며, 이 불변성(invariant)이 깨지면 alloc을 두 배 늘리거나 좀 배열이 커진다 싶으면 일정 크기만큼 늘려서 불변성을 다시 되살린다. 참 쉽죠?

문제는 우리는 쌩 메모리를 보고 싶은 게 아니라 우리가 원하는 타입의 원소로 접근되는 배열 비스무리한 뭔가를 원한다는 것이다. 고로 일반화된/매개화된 타입이 없는 C의 특성상 각 원소 타입 별로 배열 타입이 따로 하나씩 있거나, 아니면 배열 타입이 보통의 포인터 타입과 동일하게 보여야 한다는 얘기가 된다. 후자가 좀 이해가 안 가실 분을 위해 코드로 설명하면, 이런 식으로 실제 원소 앞에다가 메타데이터를 넣을 공간을 더 할당한 뒤에 포인터 자체는 첫 원소를 가리키게 만들어 놓는단 말이다.

typedef struct { size_t size, alloc; } vec_meta;
T *vec = (T*) ((vec_meta*) malloc(sizeof(vec_meta) + sizeof(T) * 10) + 1);
#define meta(vec) (*((vec_meta*) (vec) - 1))
meta(vec).size = meta(vec).alloc = 10;

그냥 보면 후자가 더 좋아 보인다. k번 원소는 vec[k]로 보통 배열 쓰듯이 하면 되고, 나머지는 매크로로 묶으면 되니까. 이 기법 자체는 MFC의 CString에서도 사용되고 있으며, 앞에서 언급했던 stb.h도 이 접근을 사용한다. 처음에는 나쁘지 않아 보여서 비슷한 형태로 다시(어쨌든 앙골모아에 맞도록 고쳐야 하니) 짜 보려고 했는데, 실은 이 코드는 C 표준(개정 연도에 따라 C99와 C11라고 흔히 부름)에 따르면 상당히 문제가 많은 코드이다. 여기에 대해 알려면 strict aliasing이라는 개념을 비롯해 C 표준이 포인터에 대해 뭐라고 말하는지를 알 필요가 있다.

Strict Aliasing

다음과 같은 코드를 예로 들어 보자.

struct row { int n, v[]; };
int average(const struct row *r, double *avg) {
    if (r->n <= 0) return 0;
    *avg = 0.0;
    for (int i = 0; i < r->n; ++i) *avg += r->v[i];
    *avg /= r->n;
    return 1;
}

이 코드는 배열의 원소 수와 각 원소를 담는 struct row를 받아서 평균값을 계산하는 간단한 함수이다. 원소 수가 0일 때 오류를 반환하기 위해서 반환값은 포인터로 받도록 했다. 얼핏 보면 아주 멀쩡하게 돌아갈 것 같지만, 이런 코드를 짜 보면 어떨까?

struct row *r = malloc(sizeof(struct row) + sizeof(int) * 3);
r->n = 3;
r->v[0] = 1; r->v[1] = 2; r->v[2] = 3;
average(r, (double*) &r->n);

이 경우 코드 안에서 *avg를 변경할 때마다 r->n도 영향을 받으므로 루프 안에서 r->n이 계속 바뀌면서 원하는 대로 동작하지 않을 것이다. 물론 호출한 쪽이 잘못했다고 보는 게 합당하지만, 어쨌거나 이 동작이 허용되면 위의 코드를 컴파일러가 다음과 같이 고쳐 쓰는 것도 불가능하게 된다(의미가 달라지니까).

int average(const struct row *r, double *avg) {
    int n = r->n;
    if (n <= 0) return 0;
    *avg = 0.0;
    for (int i = 0; i < n; ++i) *avg += r->v[i];
    *avg /= n;
    return 1;
}

뭐, 실제로는 이 최적화가 성립하려면 ravg가 서로 다른 메모리 영역을 가리키고 있는 것 말고도 몇 가지 조건이 더 필요하다. 일단 산술 연산에서 오버플로가 발생하면 안 되고(일부 구현체에서는 오버플로를 바로 abort로 보내 버릴 수도 있다), 이 함수가 실행되는 동안에 r->n이 다른 쓰레드에 의해 변경되어서도 안 된다. 하지만 이 두 가지 조건은 보통 최적화를 할 때는 그런 일이 일어나지 않는다고 흔히 가정하는 조건이다.1

이런 류의 최적화를 허용하기 위해 C99에서는 strict aliasing이라고 하는 개념을 도입했다(6.5장). 귀찮아서 안 찾아 봤지만 C11에도 거의 같은 내용이 있을 것이다. 쉽게 말하면 이런 것이다.

  • 모든 연속된 메모리 공간에는 연결된 타입이 최대 하나만 존재한다. (C99에서는 이걸 effective type이라고 부른다.)
  • 대입문은 해당 메모리 공간에 현재 대입되는 값의 타입을 연결한다. 다만 대입되는 값이 char면 아무 일도 일어나지 않는다.
  • 모든 메모리 공간은 i) 해당 메모리 공간에 연결된 타입과 호환되는 타입이나 ii) 그 타입을 포함하는 구조체/공용체나 iii) char 타입으로만 접근할 수 있다.
  • memcpy 등의 메모리를 쌩으로 다루는 라이브러리 함수는 연결된 타입을 바꾸지 않는다.

즉, 어떤 값이 한 번 저장된 뒤 다른 엉뚱한 타입의 포인터로 그 값을 읽어 오려고 하는 건 정의되지 않은 동작(undefined behavior)이 된다. 비슷하게 포인터도 막 캐스팅해서는 안 되고, 처음에 주소를 따온 타입에서 void*로 변환하거나 다시 원래 타입으로 되돌리는 것만 가능하다.2 C 표준에서 정의되지 않은 동작이라는 건 현실에서 일어날 수는 있지만 컴파일러가 그런 일이 일어나지 않는다고 가정하고 컴파일이나 최적화를 해도 된다는 의미를 지닌다. 그리고 정말로 정의되지 않은 동작이 일어날 경우, 프로그램은 정말로 아무 일이나 할 수 있다. GCC 초기 버전에는 #pragma once를 쓰면 게임을 띄우는 이스터 에그가 있었다고 카더라.

앞의 average 함수로 돌아가면, strict aliasing 덕분에 r->ndouble 포인터로 바꾸는 건 아예 불가능하다고 컴파일러가 가정하는 게 가능하다. 코드 안에서 r->n을 바꾸는 코드는 없고(애초에 상수로 선언되어 있다), 다른 쓰레드는 코드에서 참조하지 않으니 무시해도 되며, r->n을 암시적으로 바꾸는 함수를 호출하지도 않으니 r->n을 별도의 변수로 빼내는 게 가능하다. 실제로 대부분의 C 컴파일러는 이 코드를 어렵지 않게 최적화해 주는데, 이 과정에서 필요한 가정들을 생각해 보면 사실 놀라운 일이다.

우리의 확장 가능한 배열 코드로 돌아 오기 전에 한 가지 사족. 앞에서는 r->navg가 다른 타입이기 때문에 strict aliasing의 도움을 얻을 수 있었지만, 만약 avgint였다면 어떻게 될까? 이 경우 타입이 같아서 최적화가 불가능하다. C99에서는 이 가능성도 원천 차단하고자 한 가지 기능을 더 넣었는데, 안타깝게도 자동으로는 안 되고 프로그래머가 서로 겹칠 수 없는 포인터를 함수 인자에 지정하는 것으로 가능하다. 이렇게.

int average(const struct row *restrict r, int *restrict avg) {
    /* ... */
}

별 것 아닌 것 같아 보이지만 실제로 컴파일되는 걸 보면 restrict가 있을 때와 없을 때 서로 다른 코드를 생성하는 걸 제법 볼 수 있다. 참고로 포트란에는 한동안 포인터가 없었는데, 이 때문에 aliasing 문제가 안 생겨서 같은 코드를 짰는데 배열로 짠 포트란이 더 빠른 경우가 제법 있었다(…). 표준 위원회가 restrict를 넣은 이유에는 이런 까닭도 있다.

인터페이스 설계: 2차 시도

원래 코드로 돌아가 보면, 일단 T*vec_meta*로 캐스팅하는 것 자체가 표준에서 허용되지 않고, 설령 된다 해도 vec_meta가 요구하는 정렬 요건을 충족하지 않을 수 있다. (예를 들어 T가 4바이트고 int가 3바이트면…) 다행히도 한 가지 대안이 있는데, vec_meta를 포함하는 vec_T 타입을 만들어서 정렬을 강제하는 건 가능하다. 아래 코드는 내가 아는 한 적법한 C 코드이다. (물론 malloc은 항상 성공한다고 가정하자.)

typedef struct { size_t size, alloc; } vec_meta;
typedef struct { vec_meta meta; T data[]; } vec_T;
T *vec = ((vec_T*) malloc(sizeof(vec_T) + sizeof(T) * 10))->data;
#define meta(vec) ((vec_T*) ((char*) (vec) - sizeof(vec_T)))->meta
meta(vec).size = meta(vec).alloc = 10;

몇 가지 이유가 있는데, 우선 명시적인 포인터 캐스팅이 사라졌다. malloc의 반환값이나 meta 매크로에 있는 캐스팅은 뭐냐고 물으신다면, 전자는 새로 할당한 void*vec_T*로 캐스팅하는 거니 적법하고(이게 안 되면 일단 malloc의 존재 의의가 없다), 후자는 C 표준에서 char*를 매우 특수하게 취급하고 있다는 걸 악용(?)한 것이다. 내가 앞에서 포인터 캐스팅 대부분이 위험하다고 언급은 했지만, 사실 엄밀히 말하면 이건 이해를 돕기 위한 단순화였고 정확히는 두 가지 조건을 충족하면 void*가 아니더라도 캐스팅하여 사용할 수 있다.

  1. 캐스팅된 포인터가 해당 타입의 정렬 요건을 충족하고,
  2. 포인터가 가리키는 메모리 공간이 (strict aliasing 요건에 따라) 기존에 해당 타입으로 할당된 적이 있거나 아예 할당된 적이 없을 경우.

그런데 흥미롭게도, C 표준에서 char는 이 조건을 만족하는 매우 드문 자료형 중 하나이다. 우선 char보다 작은 자료형이 없으므로 정렬 요건은 신경 쓸 필요가 없고, strict aliasing에서도 char로 할당한다고 해서 연결된 타입이 바뀌지는 않게 특수한 조항이 존재하기 때문이다. 실은 이런 이유로 C 표준 자체가 void*char*와 거의 유사하게 취급하는 면이 있다. (void*에 포인터 연산을 할 수 없다는 정도의 차이가 있다.) 따라서 T*char*로 캐스팅하는 것도 적법하고, 거기서 포인터 연산을 해서 vec_T*로 변환하는 것도 (원래 vec_T로 사용되던 메모리 공간이므로) 적법하다.

그럼 과연 이 적법한 코드를 매크로로 감싸는 게 가능할까? 안타깝게도 이건 불가능해 보인다. vec_TT에 대한 언급을 안 하고 정의할 방법이 없기 때문이다. (C에는 아직 C++의 decltype 같은 게 없다…) vec_T가 필요한 결정적인 이유는 T의 정렬 요건을 충족하기 위해서 vec_meta 뒤에 패딩이 들어갈 수도 있기 때문이다. sizeof(vec_T)sizeof(vec_meta)로 바꿀 수 없는 이유도 마찬가지이다. C11에는 alignof/alignas 매크로가 있기 때문에 T에 대한 언급 없이도 어떻게든 vec_meta를 뜯어 오는 게 가능은 하지만, C99라면 적법한 방법은 없다고 보는 게 맞다.

이리하여 C의 미묘한 구석을 사용하여 확장 가능한 배열을 포인터처럼 보이게 하는 기법을 쓰려는 우리의 모든 시도는 실패로 돌아 갔다.

왕도는 없다

자, 이제 후자는 안 된다는 걸 알았으니까 전자를 그럴듯하게라도 만들어 보자. 앞에서 말한 설계를 포함해서 몇 가지 설계를 검토한 결과, 실제로 사용하게 된 코드는 대략 다음과 같다.

typedef struct { int size, alloc; } vec_meta;
typedef struct { vec_meta meta; T *ptr; } vec_T;
vec_T vec;
vec.ptr = malloc(sizeof(T) * 10);
vec.size = vec.alloc = 10;

그리고 크기를 바꿀 때는 이런 식으로 한다. (실제로는 매크로로 숨겨져 있다.)

void *vec_resize(vec_meta *meta, void *ptr, int newsize, int itemsize);
vec.ptr = vec_resize(&vec.meta, vec.ptr, 20, sizeof(T));

일면 간단해 보이지만, 이 설계도 strict aliasing을 피하려면 상당한 고민을 해야 한다. vec_resize는 타입에 상관 없이 동작하는(type-generic) 연산인데, 이걸 간단하게 한답시고 이런 식으로 하면:

typedef struct { vec_meta meta; void *ptr; } vec_generic;
void vec_resize(vec_generic *vec, int newsize, int itemsize);

void*T*가 서로 호환이 되지 않아서 (아까도 말했듯 크기가 다를 수 있다) ptr을 접근하는 것 자체가 적법하지 않게 되어 버린다. 앞에서 vec_meta가 필요한 이유도 바로 타입에 따라 바뀌는 부분을 안 바뀌는 부분과 나누기 위한 것이다. 비슷한 이유로 이렇게 하는 것도 안 된다:

void vec_resize(vec_meta *meta, void **ptr, int newsize, int itemsize);

당연하지만 void**T**는 서로 호환이 되지 않는다. 그래서 ptr을 제자리에서 고칠 수는 없으니, 인자로 받아서 결과로 새 포인터를 받는 형태의 코드가 나오는 것이다. 앞에서 깊게 다루진 않았지만, sort 같이 배열의 타입에 상관 없이 동작하는 코드를 짤 때도 이런 점을 심각하게 고민해야 한다. (짜는 게 불가능하진 않은데 metaptr을 별도로 받아야 한다.)

앞에도 링크되어 있지만, 이 과정을 거쳐서 짠 확장 가능한 배열 코드는 gist에 들어 있다. 안 되는 영어로 사용법이랑 여기에 안 써 놓은 다른 설계에 대해 적어 놓은 것도 있으니 한 번 보시라.

이게 뭐 하는 짓이람

열심히 실용적인 얘기를 했으니 이제 뜬구름 잡는 얘기를 해 보도록 하겠다.

언어 설계자와 구현자의 입장에서 strict aliasing은 충분히 정당화될 수 있는 제약이라고 생각한다. 당장 내가 앞에서 예제를 여럿 들면서 설명했듯, strict aliasing 같은 류의 제약이 없으면 보통은 잘 될 거라고 생각하는 많은 최적화가 적법하지 않게 된다. 비단 strict aliasing 말고 정수 오버플로라거나 포인터 연산이라거나 하는 것들도 마찬가지이다. 물론 분석을 좀 더 빡세게 해서 strict aliasing 같은 가정 없이도 안전한 최적화라는 걸 증명하고 실행할 수 있으면 참 좋긴 하겠지만, 분석을 잘 하려면 무한정으로 많은 시간과 노력이 필요하기 때문에 전혀 현실적으로 가능한 소리가 아니다. 즉 현실적으로 최적화를 잘 해 줬으면 하는데 가정 없이 하긴 어려우니 들어 간 가정이 strict aliasing 같은 거라는 얘기이다.

언어 사용자의 입장에서 strict aliasing은 굉장히 위험한 가정이라고 생각한다. 당장 이런 가정 때문에 생기는 보안 버그가 한둘이 아니다. 심지어 일부 가정(strict aliasing은 아니다)은 현대적인 플랫폼에서는 아예 무시해도 되는 것들도 있다. 배열의 끝(past-the-end)을 넘는 포인터는 참조하지 않아도 만드는 것 자체로 정의되지 않은 행동을 할 수 있다고? 무슨 carcdr로 포인터를 흉내냈다는 전설이 전해지는 리스프 머신도 아니고… 이 가정을 빼고 배열 바깥의 포인터는 비교할 때 정의되지 않은 “값”을 반환한다는 정도의 가정으로 대체해도 전혀 문제가 없다.

그럼 내 입장은 뭐냐? 내 입장은 도대체 대관절 왜 C가 그런 위험한 최적화를 수행할 수 있도록 허락하는지 모르겠다는 것이다. C의 위치에 대해서 많은 논란이 있지만, 내가 보기에 C는 사용하는 사람은 저수준으로 쓰는데 컴파일러는 고수준으로 컴파일하는 굉장히 이상한 언어이다. C는 사용자에게 매우 많은 자유를 안겨 줘서 기계에 가까운 많은 최적화를 수동으로 시도할 수 있게 하지만 (옛날에 strlen 최적화에 대한 글을 쓴 적이 있다), 한편으로 C 컴파일러가 자동으로 최적화를 수행하려면 그 자유 중 상당수를 여러 가정으로 제약해야 한다. 두 가지 상충되는 목표를 함께 수행하려다 보니까 이렇게 꼬인 셈이다.

C(와 C++)는 미래에 크게 바뀔 것이다. John Regehr가 지적하듯, C 표준 위원회는 언젠가 두 가지 상충되는 목표 중 하나를 버릴 것이다. 아니면 커먼 리스프와 Scheme이 분리된 것처럼 각 목표를 지향하는 비슷한 언어 두 개로 분화되거나. 어느 쪽으로 결판이 나든, 당신이 생각하는 C에 대한 가정은 대부분 틀렸거나 앞으로 틀리게 될 것이다. 당신이 C 프로그래머라면 지금이라도 정신을 똑바로 차릴 필요가 있다.

정신을 차려 보니까 C 까는 글이 되어 버렸다…


  1. 멀티쓰레딩의 경우 제대로 하려면 거의 모든 가정이 깨지기 때문에 C11에서조차 코드 상에 안 나와 있는 쓰레드가 메모리에 간섭하는 건 깔끔하게 무시하고 있다(5.1.2.4장 참고). 산술 연산에서 오버플로가 나는 경우에도 정확히 돌게 하려면 많은 최적화를 포기해야 하는데, 예를 들어 for (int i = -1; i-1 < n; ++i)을 (n+1을 루프 바깥으로 빼내기 위해) for (int i = -1; i < n+1; ++i)로 변환하는 것도 불가능해진다. 

  2. 이걸 쉽게 이해하려면 C 표준에서는 타입 별로 포인터의 크기가 다를 수 있다는 걸 알아 두면 된다. 옛날에 터보 C 같은 걸 써 봤다면 이게 왜 표준에 들어 갔는지 알 수 있을 것이다. 참고로, int*의 값들 대부분이 올바르지 않은 포인터(trap representation)라면 void*int*보다 작은 상황도 가능하다. 자세한 건 C99 6.3.2.3장 참고. 

BMS command memo

우와, 어쩌다가 이런 링크를 찾아 냈는데 문서 길이가 장난이 아니다. 그리고 먼 옛날에 써 놓고 방치한 이 링크되어 있는 걸 보고 격뿜… 내가 테스트를 할 수가 없어서 정확도는 모르겠으나 실제 구현체에서 수많은 실험을 거쳤기 때문에 문서의 완성도는 매우 높으니(사실상 스펙이라고 봐도 됨) 관심 있는 사람은 한 번 읽어 보시길. 일본어-영어 기계번역의 한계(…)를 감안하면 읽는데 큰 지장은 없다.

개9 인터넷 트렌드 해부

최근 피로해서 저널이고 개인 개발이고 거의 못 하고 근데 왜 리듬게임은 많이 하는 거지? 사는 와중에 좀 기분전환을 할 겸 요즘 만드는 개9의 인터넷 트렌드에 관한 글을 블로그에 올렸다. 너무 딱딱한 글이 되지 않았나 싶어서 걱정하긴 했는데 생각보다(…) 반향이 좋아서 다행이다. 이하 쓸데 없는 사족.

뭐, 사실 글은 저 모든 것이 유기적으로 돌아간다는 느낌으로 쓰긴 했지만, 실제로는 그냥 필요에 따라서 만들기 시작했던 것이 어찌 하다 보니까 커져서 저 모양이 된 것이다. 본래는 여러 사이트에서 이미지 크롤링해서 올릴 걸 선택해 보려고 했던 건데, 하다 보니까 자주 나오는 이미지가 있길래 묶어 놓은 거고, 그러다가 그걸 그대로 제공해도 되지 않겠느냐는 얘기가 나와서 세부 작업을 한 게 저렇게 발전한 셈이다. 당연하지만 처음부터 저런 모양으로 만들 수 있는 건 아니다.

저기서는 자세히 쓰지 않았지만, 웹 스크래핑의 기술적 문제에 대해서는 Hartley Brody의 블로그를 참고하면 괜찮을 것 같다. 해싱은 글에서 언급했듯 pHash DCT 해시1인데, 텍스트가 많거나 공백이 많은 이미지에서 영 잘 안 돌아서 다른 대안도 찾아 보고 있다. (pHash Radial 해시는 우리 용례에서는 DCT에 비해 나은 점이 전혀 없었다.) 그거 빼면… 다 노가다 내지 휴리스틱이라고 봐도 된다;


  1. 기술적으로 말하면, 우선 이미지를 정규화(흑백 변환, 리스케일링 등)한 뒤에 2차원 이산 코사인 변환(DCT)을 돌려서 왼쪽 위 DC 하나와 AC 63개를 가지고 8x8 행렬을 만든다. 이제 행렬의 값들 중앙값을 구해서 각 coefficient가 중앙값보다 큰지의 여부를 64비트 해시로 삼는다. 짐작할 수 있듯, 이 기법의 가장 안 좋은 점은 텍스트와 같이 자잘한 feature들은 DCT의 입장에서는 high frequency 정보인데 다 날라가서 해시에 별로 반영이 안 된다는 것이다(…). 


텀블러를 씁니다.