알고리즘이 아니었다. 인코딩이었다.
며칠 동안, 나는 내 시스템에 깊고 신비로운 버그가 있다고 확신했다. 로그가 깨진 문자로 출력됐다. 데이터 변환 단계는 미묘하게 잘못된 파일을 만들어냈다. 하위 프로세스(subprocess)는 내가 기대한 출력 대신 알 수 없는 값을 돌려줬다. 나는 내 로직 어딘가, 시스템의 영리한 부분에서 결함을 찾아 나섰다.
로직에는 결함이 없었다. 버그는 내 컴퓨터와 내가 철자를 적는 방식에 대해 서로 의견이 달랐다는 것이었다.
코드 안에 없던 버그
이걸 미치게 만든 건 모든 게 올바르게 보였다는 점이다. 깨진 문자를 만들어낸 코드를 읽어봐도 잘못된 점을 찾을 수 없었다. 실패는 이상하고 일관성이 없었다. 어떤 텍스트는 멀쩡히 통과했고, 어떤 건 뒤죽박죽이 됐으며, 어떤 파일은 완벽하게 열렸고 다른 건 미묘하게 깨져 있었다. 그 패턴 어디에도 내가 가서 고칠 수 있는 한 줄을 가리키는 단서가 없었다.
그 일관성 없음이, 나중에야 알게 됐지만, 흔한 단서다. 하지만 그때는 그저 내 프로그램에 귀신이 들린 것처럼 보였다.
실제로 무엇이 잘못됐나: 두 프로그램, 두 알파벳
근본에 있는 건 이거다. 컴퓨터는 텍스트를 글자로 저장하지 않는다. 숫자로 저장한다. 그리고 그 숫자를 다시 문자로 바꾸려면, 어떤 숫자가 어떤 문자를 뜻하는지 알려주는 합의된 표 — 인코딩(encoding) — 이 필요하다. 시스템의 두 부분이 서로 다른 표를 쓰면, 같은 바이트가 각각에게 다른 의미가 되고, 한쪽이 쓴 텍스트를 다른 쪽이 잘못 읽게 된다.
그게 내 버그의 전부였다. 내 운영체제는 더 오래된 레거시 인코딩으로 기본 설정되어 있었고, 반면 내가 다루던 도구와 파일은 현대적이고 거의 보편적인 인코딩을 가정하고 있었다. 두 표가 우연히 일치하는 한 — 가장 평범한 문자들에 대해서는 일치한다 — 모든 게 작동했고, 그 겹치는 범위를 벗어나는 무언가가 나타나는 순간 깨졌다. 텍스트가 의도적으로 손상된 적은 한 번도 없었다. 그저 쓰인 것과 다른 언어로 읽혔을 뿐이다.
왜 그렇게 보기 어려웠나
인코딩 불일치는 특히 고약한 부류의 버그인데, 깔끔하게 실패하지 않는 경우가 많기 때문이다. 문제를 가리키는 예외(exception)를 던지는 대신, 관대한 변환은 거의 맞는 출력을 만들어낼 수 있다. 두 인코딩이 의견을 달리하는 문자를 만나기 직전까지는 말이다 — 그러고는 조용히 바로 그 부분만 망가뜨리고 계속 진행한다.
그래서 실패는 간헐적이고 내용에 따라 달라지며, 이것은 정확히 사람을 엉뚱한 곳으로 보내는 양상이다. 간헐적이고 내용 의존적인 버그는 내 로직 안, 내가 자랑스러워하는 복잡한 부분 안에 있을 것처럼 느껴진다. 이 버그는 배관 속에, 내가 의존하고 있다는 사실조차 한 번도 알아채지 못했던 기본값 속에 살고 있었다.
이런 종류의 작업에 담긴 화려하지 않은 진실
나는 학습 시스템을 구축하는 일의 어려운 부분이 흥미로운 부분 — 모델, 수학, 전략 — 일 거라고 상상했었다. 실제로 어려운 부분의 정말 큰 비중은 이런 것들로 드러났다. 텍스트 인코딩. 줄바꿈 문자. 기계마다 다른 의미를 갖는 파일 경로. 모든 것 아래에 깔린 따분한 인프라, 단 하나의 잘못된 기본값이 당신의 한 주를 조용히 갉아먹는 동안, 당신은 그 위에 얹힌 우아한 코드를 들여다보며 문제가 거기에 있을 거라고 확신하는 그곳.
이건 유익한 방식으로 겸손하게 만든다. 나를 가장 자주 멈춰 세운 건 수학이 아니었다. 배관이었다.
해결책: 기본값을 믿기를 멈추고, 합의를 강제하라
수리는 미묘하지 않았다. 나는 시스템의 각 부분이 우연히 채택한 인코딩에 의존하기를 멈추고, 모든 경계에서 하나를 — 현대적이고 보편적인 그것을 — 명시적으로 강제했다. 모든 파일 읽기, 모든 파일 쓰기, 모든 하위 프로세스 실행에서. 모든 구성요소가 같은 표에 대해, 명문화된 형태로 합의하게 만들고 나니, 깨짐이 멈췄다.
그 핵심 수정과 더불어, 나는 더 작은 방어적 습관 하나를 추가했다. 애초에 특정 골치 아픈 문자들을 로그에서 빼두는 것이다 — 인코딩을 제대로 잡는 일의 대체물이 아니라, 안전망으로서. 로그는 다른 모든 게 불타고 있을 때 가장 명확하게 읽을 수 있어야 하는 바로 그곳이고, 나는 문제를 진단하기 위해 의존하는 도구가 같은 부류의 문제에 계속 희생되기를 원치 않았다.
교훈: 기본값은 당신이 내리지 않은 결정이다
더 깊은 교훈은 버그보다 오래 살아남았다. 당신의 스택에 있는 모든 “기본값”은 결정이다 — 대개 당신의 특정 상황을 알지 못했던 누군가가, 종종 수년 전에, 이제는 더 이상 들어맞지 않을 수도 있는 이유로 내린 결정. 대부분의 경우 그렇게 물려받은 결정들은 괜찮고, 당신은 그것들에 대해 생각할 일이 전혀 없다. 그게 그것들을 위험하게 만드는 요소의 일부다. 그중 하나가 당신의 경우에 대해 조용히 틀려 있을 때, 그건 거의 보이지 않는다. 당신이 그것을 의식적으로 선택한 적이 없기 때문이다 — 그리고 당신이 한 번도 선택하지 않은 것은 당신이 살펴볼 생각을 가장 마지막으로 하는 곳이다.
나는 내가 내린 줄도 몰랐던 선택 하나에 한 주의 상당 부분을 잃었다. 이제 무언가가 논리적으로 말이 안 되는 방식으로 실패할 때, 내 첫 질문 중 하나는 더 이상 “내 버그가 어디 있지?”만이 아니다. “내가 실제로는 결정한 적 없이 가정하고 있는 게 뭐지?”이기도 하다.
— 시그널도, 수익도 없습니다. 투자 자문이 아닙니다.