리허설

테스트가 전부 통과해도 만든 것이 맞다는 뜻이 아닙니다. 관객이 몰리는 순간을 미리 재현하는 리허설 그리고 그 리허설마저 잘못 보고하는 세 가지 방식을 정리했습니다.

리허설

어느 오후 팀에 작은 웹 게임 하나를 공개했습니다. 실시간으로 가위바위보를 하는 게임인데 사람이 하나둘 접속하는 동안에는 문제가 없었습니다. 그런데 여러 명이 같은 순간에 접속하자 곧바로 망가지기 시작했습니다. 한 명만 대결이 성립하고 나머지는 상대가 다른 대결 중이라며 튕겼고 구호에 맞춰 1초 안에 낸 손이 창이 열리기 전이라는 이유로 조용히 버려졌으며 뒤늦게 들어온 사람은 남들 화면에 아예 뜨지 않았고 남의 대결이 제 화면에 나타났습니다. 정상적으로 플레이할 수 없었습니다. 이 문제들을 미리 막지 못했고 실제로 사람이 몰린 그 짧은 시간에 게임이 제대로 돌지 않자 관심은 곧바로 다른 곳으로 옮겨 갔습니다. 이후 약 한 시간에 걸쳐 원인을 하나씩 고쳤습니다. 계단 맨 아래 지상은 계단이 아닌 광장이라 여럿이 서는데 도전 상대를 늘 바로 윗칸 한 명에서만 골랐다는 것, 입력 개방을 프레임 루프가 아니라 타이머로 옮겨야 했다는 것, 지각 유예를 관측된 최소 왕복시간의 절반에 고정 지터를 더해 잡아야 한다는 것 등입니다. 고쳤을 때는 이미 초기 관심을 유지하는 데 실패한 뒤였습니다.

가위바위보 게임의 지상 광장입니다. 여덟 명이 계단 앞에 나란히 서서 머리 위에 이름표를 달고 있고 계단 첫 두 칸은 봇 "마리오"와 "루이지"가 지키고 있습니다.

이것이 이번 주에 가장 깊이 배운 교훈입니다. 여러 사람이 동시에 몰릴 때의 동작을 미리 정하고 인프라를 갖춰 두지 않으면 아무리 잘 만든 로직도 사람들 앞에 놓이는 첫 몇 분에 무너진다는 것입니다. 지난 '모형'에서 한 주 동안 온전히 작동하는 게임 네 개를 만든 이야기를 했습니다. 이번 주는 달랐습니다. 저 가위바위보 게임이 망가진 바로 다음에 만들기 시작한 게임 하나에 한 주의 절반 넘는 작업이 쏠렸고 그 게임을 만드는 내내 저를 고민하게 만든 것은 무엇을 만들 것인가가 아니라 만든 것을 어떻게 신뢰할 것인가였습니다. 그래서 이번 주의 이야기는 리허설에 관한 것입니다. 사람들 앞에 막을 올리기 전에 관객이 몰리는 그 순간을 미리 재현해 무대가 버티는지 확인하는 일 말입니다. 저 가위바위보 게임은 관객을 위한 리허설 없이 막을 올렸다가 첫날 무너진 것이고 그다음에 만든 게임은 그래서 리허설부터 준비했습니다. 그런데 리허설에는 함정이 있었습니다. 테스트 스위트 143개를 모두 통과하고 대표 시나리오의 상태를 해시로 고정해 둔 골든 테스트가 모두 성공했지만 사람이 15분만 플레이해도 결함이 일곱 개쯤 나왔습니다. 그리고 그 일곱 개의 공통점은 하나같이 시스템이 자기 약속을 완벽하게 지키고 있었다는 것입니다. 리허설은 매끄러웠는데 무대는 무너졌습니다. 이번 주에 배운 것을 한마디로 줄이면 통과했다는 표시가 세 가지 방식으로 잘못 보고한다는 것입니다.

지난 글을 관통한 주제는 측정이 직관을 이긴다는 것입니다. 사람 눈이 자꾸 놓치는 것을 기계가 대신 재도록 만드는 이야기였습니다. 이번 주는 그 측정 장치 자신을 의심하는 데까지 갔습니다. 무언가를 실행하는 부분과 그 결과가 맞았는지 판정하는 부분으로 테스트가 나뉜다면 뒤쪽 판정하는 부분을 소프트웨어 공학에서는 오라클이라고 부릅니다. 이 글에서 오라클이라고 하면 매트릭스의 바로 그 사람이 아니라 이 판정 근거를 가리킵니다. 이번 주에 반복해서 깨달은 것은 결함 자체가 아니라 오라클이 틀리는 방식이었습니다. 판정 기준이 잘못되면 테스트가 전부 통과인데 제품은 틀린 상태가 됩니다. 세 가지 형태가 있습니다.

먼저 하지도 않은 일이 통과로 나타나는 것입니다. 검증이 실행조차 되지 않았는데 통과로 보고됩니다. 제출된 변경을 감시하는 루프를 저장소 바깥에서 돌리는 바람에 형상관리 명령이 연결 거부 오류를 냈는데 필터가 그 오류 출력을 훑어 걸린 게 없다고 판단하고는 전부 제출됐다고 보고했습니다. 실제로는 아무것도 제출되지 않았습니다. 촬영 하네스에서는 셸의 변수 확장 한 번이 파트 이름 여러 개를 한 덩어리로 뭉치는 바람에 존재하지도 않는 파트 하나가 전달되어 아무 파트도 돌지 않은 채로 통과되었습니다. 보안 레드팀 테스트에서는 신규 계정의 초기 보유금액이 길드 생성비용에 못 미쳐 생성에 실패하고 그 뒤에 딸린 여섯 개의 공격 케이스가 통째로 스킵됐는데 화면에는 통과되었습니다. 이런 종류는 한번 습관을 들이면 잡을 수 있습니다. 자동화 결과가 통과면 한 번 더 확인하도록 만드는 것입니다. 이게 무언가를 한 일의 통과인가 아무것도 안 한 일의 통과인가 하는 것입니다. 검증 루프의 출력이 입력에 비례해 늘지 않으면 그 루프는 돌지 않았습니다. 요약 한 줄은 자기가 실행됐다는 것조차 말하지 못합니다. 전제가 무너졌을 때 조용히 건너뛰지 않고 실패로 처리하도록, 못 도는 이유 자체를 양성 검증 조건으로 바꾸면 그게 오히려 새 검사 항목이 됩니다.

다음은 항등식이 되어 버린 통과 사례입니다. 판정식이 검사하려는 코드의 계산을 그대로 빌려 오는 바람에 무슨 일이 있어도 통과하는 것입니다. 착륙한 우주선이 지면 위에 제대로 서 있는지 재려고 그려진 지면 높이를 렌더가 쓰는 바로 그 공식으로 재계산해서 선체 바닥과의 간극을 측정했습니다. 대수적으로 정리하면 그 간극은 항상 정확히 0이었습니다. 절대 실패할 수 없는 오라클이었습니다. 화면에서는 선체가 지면에 파묻혀 있는데 판정은 언제나 통과였습니다. 고친 방식은 경로를 나누는 것입니다. 지면은 화면에 실제로 그려지는 변위 텍스처의 픽셀에서 레이캐스팅 해서 읽고 배는 씬 그래프에서 읽어 두 경로가 어긋나면 그 자체가 신고되도록 했습니다. 오라클을 쓸 때 이 판정식에서 검사 대상 코드의 식을 빼면 무엇이 남는가를 확인해야 합니다. 아무것도 안 남으면 그것은 테스트가 아니라 주석입니다. 지도를 확대해도 천체가 안 커지던 문제를 고칠 때도 같은 일이 일어났습니다. 게이트 판정이 전부 통과였는데 줌을 6배로 올린 스크린샷에서 태양이 화면 폭의 절반 가까이를 차지했습니다. 게다가 하네스가 검사 대상의 상한값을 자기 안에 복제해 두고 있어서 그 상한을 낮추자 하네스에 적힌 이전 값이 거짓 양성 판정을 냈습니다. 테스트가 검사 대상 코드의 상수를 복제하면 그 상수를 고칠 때 테스트가 잘못된 결과를 냅니다.

마지막이 가장 잡기 어렵습니다. 계약은 완벽하게 지키면서 사람을 속이는 통과 사례입니다. 서버가 자기 상태 계약을 전부 지켰는데 화면은 틀렸습니다. 착륙 판정도 통과였고 접지 고도가 지형 표고 범위 안이라는 판정도 통과였는데 선체는 지면에 파묻혀 있고 엔진 화염이 계속해서 분사되고 있었습니다. 튜토리얼이 지목한 버튼을 눌렀다는 응답은 정상인데 화면에서는 안내와 다른 것이 눌렸습니다. 착륙 상태가 유지된다는 판정은 통과인데 카메라가 지면을 뚫고 내려가 천체가 사라졌습니다. 이런 종류가 세 번쯤 반복되자 개별 수정 대신 관측의 축 자체를 새로 정했습니다. 서버 판정과 화면 판정은 다른 축이라는 것입니다. 그 축을 정한 뒤에 마지막으로 잡힌 것이 이번 주에 가장 중요한 것입니다. 로버의 주행 화면에 지면이 아예 없었습니다. 코드가 좌표를 전부 우주선 기준 상대값으로 계산하는데 씬의 원점은 천체 중심이라 로버도 목표 지점도 카메라도 통째로 행성 중심에 놓였고 지면이 렌더링에서 사라졌습니다. 그런데 서로의 상대적 배치는 전부 맞았기 때문에 사람 눈으로도 현행 오라클로도 심지어 이미 만들어 제출된 플레이어 가이드 스크린샷까지도 이 결함을 통과시켰습니다. 결함을 등록할 때 이걸 잡았어야 할 오라클이 무엇인지를 함께 기록하도록 했고 이 값이 없으면 그게 곧 오라클의 구멍이고 구멍 목록이 곧 다음에 만들 검사의 목록이 됩니다.

착륙 상태가 유지된다는 판정이 통과인데도 화면에서 지면이 사라진 순간입니다. 아래 상태줄과 우주선 이름표는 여전히 "Landed — Moon surface"인데 달 표면은 어디에도 없고 별만 보입니다.

이 셋에 더해 몇 가지 규칙을 추가했습니다. 새 게이트는 반드시 결함을 일부러 심어 양성 판정을 확인한 다음에야 신뢰합니다. 한 세션에서 결함 주입을 세 번 돌렸는데 두 번이 통과였습니다. 카메라가 지면을 뚫는 조건이 특정 각도 너머에서만 성립하는데 첫 스윕이 그 경계 1.5도 앞에서 멈춰 있어서 클램프를 아예 제거해도 통과였습니다. 범위를 넓히자 그제야 양성 판정이 나왔습니다. 게이트는 양방향이어야 합니다. 시설이 화면을 너무 많이 덮는다는 상한만 설정했더니 배율을 낮추는 것만으로 통과가 됐고 실제로 과보정해서 대상이 화면의 10분의 1도 안 되게 줄었는데 게이트는 통과했습니다. 그건 애초에 작아서 안 보인다고 지적받았던 바로 그 상태였습니다. 이후로는 오라클 초안을 쓰면 곧바로 이걸 부정행위로 통과시키려면 무엇을 하면 되는지 자문하고 답이 있으면 그 반대편에 하한을 같이 설정합니다. 결정론적인 계측에서 넉넉한 허용오차는 안전 마진이 아니라 결함을 받아들이는 한도입니다. 그리고 게임 규칙을 고쳐서 여러 테스트가 깨질 때 그중에는 옛 취약점을 명세로 굳히고 있던 테스트가 섞여 있습니다. 여기서 위험한 선택은 고쳐서 통과로 만드는 것이고 대신 하나씩 왜 깨졌는지 확인하고 잃어버린 보장을 새 검증 조건으로 갚아야 합니다.

이 모든 것을 감당하려고 만든 것이 게임과 완전히 분리된 자율 검증 체계였습니다. 검증 코드는 게임의 서버나 클라이언트 코드를 한 줄도 가져다 쓰지 않고 실제 배포와 같은 방식으로 배포한 실제 빌드에 웹 요청과 실제 브라우저로만 접촉합니다. 이 체계의 설계 문서는 스스로를 배포 리허설이라고 부릅니다. 실제 배포와 똑같이 세운 무대에 가짜 관객을 올려 보는 일이기 때문입니다. 특히 저 가위바위보 게임을 무너뜨린 바로 그 상황 곧 여러 사람이 한꺼번에 몰리는 경우를 브라우저 없는 봇 여럿과 실제 브라우저 몇을 섞어 미리 재현합니다. 플레이어 하네스는 실제 포인터 조작과 좌표 판정으로 조작하므로 버튼을 못 누르면 그것이 곧 결함입니다. 개발이 계속되어 검증 대상이 자꾸 움직이는 문제는 소스 트리의 내용 해시로 불변 이미지를 만드는 방식으로 해결했습니다. 지금 제출본을 14초 만에 만들어 두면 그 뒤의 개발과 무관하게 언제 돌려도 같은 대상을 검사합니다. 이 기능을 만드는 도중에 1분 사이에 저장소의 최신 리비전이 병행 세션 제출로 움직였는데 그게 문제가 실재한다는 증거이자 이미지 생성이 그 위에서 정확히 동작한다는 증거였습니다.

처음에는 지라나 깃헙 이슈 같은 외부 이슈 트래커를 도입하려 했습니다. 결함이 주당 수십 건씩 기계에서 나오자 줄글로 적는 인수인계 문서만으로는 중복을 병합할 수도 수정 후 재검증을 관리할 수도 재발을 감시할 수도 결함 밀도나 평균 수명 같은 통계를 낼 수도 없어서 상태를 가진 레코드가 필요해졌기 때문입니다. 그런데 외부 트래커를 정식으로 검토한 끝에 채택하지 않았습니다. 이유가 넷이었습니다. 형상관리가 퍼포스라 커밋 참조로 이슈를 자동으로 닫는 것 같은 깃헙의 전제가 전부 없다는 것, 작업자가 사람 팀이 아니라 기계 세션이라 담당자나 스프린트나 알림 같은 협업 기능이 쓸모가 없다는 것, 야간에 무인으로 도는 검증이 외부 API 키와 네트워크에 의존하면 트래커가 죽는 날 검증도 함께 죽는다는 것, 그리고 결함의 증거인 스크린샷과 프레임 기록이 저장소 안에 있어야 다음 세션이 맥락과 함께 읽는다는 것입니다. 그래서 채택한 것이 기계가 관리하는 문서 집합이었습니다. 이슈 하나가 곧 파일 하나이고 앞머리에 심각도와 상태와 지문 같은 메타데이터를 넣고 그 아래에 재현 절차와 원인 가설을 줄글로 기록합니다.

docs/qa/issues/
  INDEX.md          런이 끝날 때마다 다시 쓰이는 자동 인덱스
  QA-0001.md        이슈 하나가 곧 파일 하나 (지금 98건)
  QA-0002.md
  ...

docs/qa-runs/
  2026-07-22-user-camera-underground/
    camera-001-at-surface.png                결함의 증거는 저장소 안에 둔다
    camera-002-underground-surface-gone.png
  ...

qa/regressions/
  QA-0001.mjs       닫힌 이슈의 재현 스크립트 (지우지 않고 상시 실행 · 지금 29건)

이 문서 집합이 잘 작동하는 이유는 세 가지입니다. 첫째로 지문입니다. 시나리오와 오라클과 정규화한 실패 서명을 해시로 묶은 것이 지문인데 같은 지문이면 새 파일을 만들지 않고 관측 횟수만 올립니다. 이것이 기계가 만든 이슈가 저장소를 가득 채우는 것을 막는 유일한 장치입니다. 둘째로 상태 전이가 자동입니다. 열림에서 수정 커밋을 적고 다음 실행에서 재현 스크립트가 통과하면 수정됨으로, 그러면 그 재현 스크립트가 영구 회귀 감시로 승격되어 감시 중으로, 그러다 다시 실패하면 심각도가 한 단계 오른 채로 재발로 넘어갑니다. 재발은 최초 발견보다 나쁜 신호로 취급합니다. 수정이 증상만 지웠다는 뜻이기 때문입니다. 닫힌 이슈의 재현 스크립트는 절대 삭제하지 않고 이후 모든 빌드에서 상시 실행됩니다. 셋째로 이미 접수된 결함은 실행을 실패로 만들지 않습니다. 실패로 만드는 것은 신규 지문과 재발 전이뿐이라 게이트가 결함 목록에 잠기지 않고 유의미하게 유지됩니다. 그리고 이 체계 자신을 검증하는 메타 검증이 따로 있어서 알려진 결함을 일부러 심어 검증되는지 확인합니다. 오라클을 고칠 때마다 이걸 돌리지 않으면 오라클이 죽은 채로 통과가 됩니다. 이 체계가 잡은 첫 실제 게임 결함은 수성 표면에서 이륙 버튼을 누르면 추락으로 판정되어 지구로 회수되고 회수비까지 청구되는 것입니다. 서버의 단위 테스트 130종이 전부 통과이고 착륙-재이륙 테스트도 통과하는 인프로세스 시뮬레이션으로는 보이지 않던 사각이었고 이 체계를 만든 이유의 실례였습니다. 그리고 자동 검증을 만드는 일의 태반이 게임이 아니라 자기 오라클을 고치는 일이라는 것도 배웠습니다. 이 체계가 잡은 실제 결함 하나에 자기 위양성이 열다섯 개였습니다. 위양성은 결함보다 나쁩니다. 거짓 경보가 반복되는 검증은 꺼지고 꺼진 검증은 없느니만 못하기 때문입니다.

그 수성 결함을 잡은 과정이 이 체계가 왜 필요한지를 가장 잘 보여 줍니다. 전 천체 착륙을 자동으로 돌리다 수성에서 걸렸는데 실제 자동비행으로 수성 광산기지에 착륙한 뒤 이륙 버튼을 누르면 우주선이 지구 정거장에 도킹한 채로 나타나고 잔고에서 회수비 1,500₡이 빠졌습니다. 처음에는 좌표가 튄 순간이동 버그로 보였습니다. 원인을 알려 준 것은 증거로 남은 스크린샷의 토스트 한 줄이었습니다. "기체 회수 — 구조선이 선체를 모항으로 견인했습니다"라는 안내가 떠 있었습니다. 즉 순간이동이 아니라 정상 이륙이 추락이나 조난으로 오판되어 소프트 실패 경로가 발동했습니다. 화면을 안 찍었다면 순간이동으로 오진했을 사건입니다.

수성 표면에서 이륙했는데 지구 정거장에 도킹한 채로 나타난 화면. 오른쪽 아래에 "기체 회수 — 구조선이 선체를 모항으로 견인했습니다"와 회수비 1,500₡ 정산 안내가 떠 있어 순간이동이 아니라 정상 이륙이 조난으로 오판된 것임을 알려 줍니다.

범위를 좁힌 방법은 같은 시나리오를 달로 돌려 보는 것이었고 그러면 전부 정상이었습니다. 천체를 바꿨을 때만 갈리는 값이 회수 판정을 넘긴다는 뜻이므로 이 대비가 원인 추적에 가장 빠른 방법이었고 범위를 좁히는 데 3분이 걸렸습니다. 원인은 결국 이륙 첫 틱의 좌표계 어긋남이었습니다. 시뮬레이션이 한 서브스텝마다 세계 시각을 먼저 전진시키는데 그러면 이전 시각의 함선 위치와 전진한 시각의 천체 위치가 섞여 천체의 공전 속도 곱하기 1초만큼 어긋납니다. 수성은 공전 속도가 초속 47킬로미터라 그 어긋남의 반경 성분이 8킬로미터쯤 되고 그래서 이륙 직후가 지표 아래로 읽혔습니다. 같은 어긋남이 궤도 진입에서는 도착 반경이 수천 킬로미터라 무해하지만 착륙 상황에서는 즉시 추락으로 이어졌습니다. 하나의 결함이 스케일에 따라 증상이 갈린 것이고 흩어져 있던 착륙 관련 이슈 스무 건 남짓이 이 한 가지 원인으로 통합됐습니다. 그리고 이걸 고치고 나니 이번에는 그 어긋남 때문에 우주선이 지면에 너무 빠르게 닿던 것을 선체를 뒤집는 회전 속도를 6분의 1로 낮추고 최종 접근 구간을 반경의 2퍼센트로 좁혀 상쇄해 두었던 옛 상수 몇 개가 착륙 지연이라는 성능 결함으로 남았습니다. 버그가 남아 있던 시절의 상수가 원인을 고치자 드러났습니다.

검증을 하며 배운 판정 규칙 두 가지도 있습니다. 하나는 테스트가 예상보다 자릿수 단위로 어긋나면 테스트를 느슨하게 하기 전에 제품을 의심해야 한다는 것입니다. 자율 주행이 240초에 예상의 20분의 1밖에 못 간 것을 하네스가 느린 탓으로 넘길 수 있었지만 20배나 어긋난 숫자를 확인해보니 제출된 지 오래된 실결함 둘이 나왔습니다. 예산만 늘렸다면 둘 다 그대로 남았을 것입니다. 다른 하나는 오염 의심이 판정이 아니라 가설이라는 것입니다. 전체 실행에서 나온 신규 결함 30건을 병행 세션의 작업과 섞였다는 이유로 통째로 보류했다가 단독으로 다시 판정하니 대부분은 실제 결함이었고 오염은 몇 건뿐이었습니다. 오염 의심 라벨이 실결함 여섯 개를 반나절 동안 가렸습니다. 무효로 처리하려면 오염의 기전이 규명되고 그 기전이 수정된 빌드에서 해당 시나리오가 통과로 완주하는 세 조건이 모두 갖춰져야 합니다. 그전까지는 미재현일 뿐이고 조용히 닫지 않습니다.

이 문서 기반 이슈트래킹 방식이 영원한 답은 아닙니다. 지금 조건에 맞는 답일 뿐이고 외부 트래커를 사용하지 않기로 결정한 네 가지 이유가 곧 이 방식의 수명을 정하는 조건이기도 합니다. 지금은 작업자가 기계 세션 하나이고 증거가 저장소 안에 있어야 하며 지문 병합이 파일 수를 결함의 종류 수로 묶어 두는 한 지금 규모인 이슈 몇백 개는 검색과 인덱스로 충분히 다룰 수 있습니다. 첫 전환 압력은 저장소가 비대해지는 것입니다. 이슈가 수백에서 천 건을 넘고 증거 묶음이 저장소 크기를 지배하기 시작하면 신호가 오는데 이때의 자연스러운 다음 단계는 외부 트래커가 아니라 증거만 저장소 밖 스토리지로 빼고 이슈 파일에는 포인터만 남기는 절충입니다. 두 번째 압력은 사람이 합류하는 것입니다. 담당자와 알림과 중복 신고 처리가 값을 갖기 시작하면 기계가 자동으로 옮기는 상태 기계와 사람이 손으로 옮기는 흐름이 충돌합니다. 이때는 기계용 문서 집합을 원본으로 유지한 채 사람용 뷰를 외부 트래커에 단방향으로 비추는 구조가 유력합니다. 세 번째 압력은 형상관리를 깃으로 옮기거나 게임이 공개되어 실사용자가 버그를 신고하기 시작하는 것입니다. 커밋 자동 닫기가 생기고 저장소를 못 읽는 외부 신고자가 생기는 이 지점이 문서 집합에서 외부 트래커로 원본 자체를 옮기는 실제 전환점입니다. 다만 그때에도 영구 회귀 감시와 지문 기반 자동 중복 병합은 외부 트래커가 대신하지 못하므로 저장소에 남습니다. 그러니 이 방식의 수명을 정하는 축은 규모가 아니라 전제입니다. 이슈가 몇 개인가가 아니라 작업자가 누구이고 형상관리가 무엇이며 증거를 누가 읽고 신고자가 저장소를 볼 수 있는가가 바뀌는 순간에 이 문서 집합은 원본의 지위를 내주게 됩니다. 그전까지는 이번 주가 보여 주듯 외부 트래커보다 이 방식이 낫습니다.

이런 과정을 통해 지금 만들고 있는 게임을 조금 소개하면 태양계 안에서 화물을 사고 나르고 파는 우주 트럭 운전 게임입니다. 엔진은 Space 프로젝트에서 그대로 포크해 오고 그 위에 경제와 계약과 평판과 건설과 해적 요소를 추가했습니다. 만들면서 나온 설계 판단 중에 마음에 드는 것이 몇 가지 있습니다. 계약은 화주가 화물을 무상으로 주기 때문에 그대로 두면 받자마자 팔아 순수한 화폐 발행기가 되어버립니다. 서버가 파는 수량에서 계약 약정분을 빼고 클라이언트도 같은 규칙으로 버튼을 막았습니다. 서버가 거부할 버튼을 화면이 제공하면 안 된다는 것 그건 미관이 아니라 규칙을 두 곳에 기록하는 문제입니다. 항구를 잇는 위상차 게이트는 진입할 때 잔고를 잠그기만 하고 출구에 도착해야 요금을 확정하며 완주하지 못하면 전액 환불하는데 버튼에 보인 요금과 실제로 낸 요금이 같은 함수로 계산하게 해서 구조적으로 어긋날 수 없게 했습니다. 플레이어가 없는 항로에도 시세가 변동해야 하는데 게임 경제에 이 이유로 추가 화폐를 공급해서는 안 되니 위치를 기준으로 한 사인파의 증분으로 시세를 흔들어 한 주기의 적분이 0이 되게 했습니다. 그런데 이렇게 다 만들어 놓고 시간 스케일을 수정할 때 그동안 연료와 정비 요금이 사실은 이동 거리에 비례하는 지속 비용이 아니라 항구에 닿을 때마다 무는 정액이었다는 것을 뒤늦게 발견했습니다. 편도 이동에 드는 속도 변화량이 탱크 기준의 28배여서 미터가 첫 구간에서 포화되고 그 뒤는 무료였습니다. 상수를 다시 설정하면서 주석에 적은 문구가 이번 주의 태도를 보여준다고 생각합니다. 승인된 것은 왕복에 마모 30에서 50퍼센트라는 목표이지 제가 괄호에 적어 둔 임시 수치가 아니라는 것입니다. 목표와 짐작이 어긋나면 목표를 따릅니다.

지구 정거장의 무역 화면. 원격 항구의 시세를 광지연만큼 지난 값으로 함께 보여 주고("Luna Base +2,700₡ more (live)") 계약으로 묶인 화물은 "held 10 (5 contracted)"처럼 팔 수 없는 수량이 갈라져 있으며 항구에 닿은 직후라 연료 100퍼센트·마모 0퍼센트입니다.

기술적으로 가장 공을 들인 것은 로버의 주행입니다. 주행은 클라이언트가 고프레임으로 즉시 반응하되 서버는 2초에 한 번 보내오는 위치를 물리 한계로만 검증하고 배터리 같은 값은 서버가 다시 계산해 클라이언트 신고를 신뢰하지 않습니다. 화면은 서버가 준 기준점에 클라이언트가 접평면에서 예측한 국소 이동을 더해 그리며 서버가 그 커밋을 받아들이면 다음 기준점이 예측 지점으로 따라와 자연스럽게 이어집니다. 어긋났을 때만 튕겨 돌아옵니다. 콘텐츠 쪽에서는 화물에 이야기를 붙였습니다. 수성 광부 파업 같은 비선형 이야기 줄기에서는 연대 물자와 회사 장비를 플레이어가 양쪽 다 나를 수 있고 어느 쪽을 먼저 나르느냐가 그다음 송장의 문면을 바꾸는 식으로 순서 자체가 메커닉이 됩니다. 플레이어의 과거는 동기와 전직과 흔적 세 축을 곱해 예순두 가지 구조로 펼치되 조합이 폭발적으로 증가하지 않도록 축마다 조각을 따로 써서 교집합의 톤으로 조립합니다. 시간 스케일을 절반으로 줄이는 작업에서는 배율로 상수를 제어하면 틀린다는 것을 배웠습니다. 구간마다 시간이 상수에 붙는 차수가 다르고 무엇보다 착륙 안전속도가 선체를 180도 뒤집는 15초 동안 미끄러지는 거리에서 유도되므로 가속을 네 배로 올리면 회전 속도도 두 배로 함께 올려 가속이 회전 속도의 제곱을 따라가게 해야 착륙이 깨지지 않았습니다.

그 게임에서 궤도를 수정할 때는 이전 결정을 되돌렸습니다. 도착했으면 도착한 것처럼 궤도를 천체에 바짝 붙이는 작업이었습니다. 골든 테스트를 한 번 다시 고정하는 것은 승인을 받았습니다. 그런데 밴드를 좁히자 작은 소천체들에서 착륙이 추락으로 이어졌습니다. 그래서 재고정을 쓰지 않고 코드를 통째로 되돌린 뒤 측정 도구와 막힌 이유만 남겼습니다. 골든을 다시 고정하는 것은 되돌리기가 가장 비싼 조작이고 고정한 뒤 소천체 추락을 고치려면 또 고정해야 하니 두 번의 승인이 필요하기 때문입니다. 다음 시도에서는 천체 크기로 밴드를 나눴고 경계로 삼은 반경 1000킬로미터는 임의로 고른 값이 아니라 신고 대상 천체는 전부 위에 있고 소천체는 전부 아래에 있는 명단이었습니다. 제약을 제거할 때는 그것이 부수적으로 무엇을 지키고 있었는지를 확인해야 합니다. 소천체에서 옛 하한이 지배적이었던 것이 곧 추락을 막던 여유였으므로 소천체에서는 그대로 두었습니다. 카메라가 지면을 뚫은 결함의 원인도 똑같았습니다. 카메라의 상하 회전을 일부러 제한하지 않기로 한 결정에 그렇게 하면 뷰가 뒤집히지 않는다는 옳은 이유가 적혀 있었는데 옛 제한이 뒤집힘만 막던 게 아니라 카메라가 지면 아래로 내려가는 것도 같이 막고 있었다는 사실이 아무 데도 적혀 있지 않았습니다. 불변식을 제거하는 변경은 그 불변식이 대신 지키던 것의 목록을 남겨야 하고 하나만 옮기고 나머지를 두면 남은 구멍은 한참 뒤에 전혀 다른 형태로 나타납니다.

게임을 실제 경로에 배포하면서 배운 것은 포크가 도구를 상속하지만 위협 모델은 상속하지 않는다는 것입니다. 이 게임은 기존 Space 프로젝트를 통째로 포크했으므로 보안 방어 하네스도 전부 물려받았는데 시작 지점에서 30초면 초기 보유금액을 두 배로 불릴 수 있었습니다. 착수 30분 만에 나온 한 줄이 전부를 설명했습니다. 레드팀 코드에서 경제 관련 단어를 세는 grep -c 한 줄의 결과가 0이었습니다. 상속한 하네스는 상속한 표면을 지키는데 원본에는 경제가 없었으니 경제 케이스도 없었습니다. 그런데 이 게임은 여럿이 공유하는 라이브 경제를 목표로 하고 있어 화폐 발행은 되돌릴 수 없으니 그 축을 위한 새 검증을 스스로 만들어야 했습니다. 심각한 결함 셋이 전부 그 새 테스트에서만 나왔습니다. 배포 자체도 사고가 될 뻔했습니다. 복사해 온 배포 설정이 같은 이미지 이름과 포트를 그대로 써서 그대로 올렸다면 같은 호스트에서 15시간째 돌던 게임의 운영중인 컨테이너를 덮어쓸 뻔했습니다. 공개하자마자 소개 페이지가 형제 게임을 설명하고 있던 것도 포크 잔재였으니 공개 배포가 그 자체로 검사였던 셈입니다. 지금 이 게임의 주소는 접속하면 접근 차단으로 403을 돌려주는데 그게 정상 동작입니다. 실제 서버에서 테스트하고 있어서 키가 없으면 막는 얇은 커튼을 두었고 그 커튼을 만든 세션에 곧바로 검증을 한 번 돌려 보지 않았다면 다음 세션이 커튼이 검증까지 막는다는 틀린 진단을 물려받았을 것입니다. 규약을 문서에 기록한 세션에는 그 규약을 최소 한 번 실행해 보아야 그것이 그럴듯한 문장으로만 남지 않습니다. 그리고 왕복 차익을 막는 조치가 정상 무역에도 영향을 주므로 레드팀에 공격이 아닌 케이스를 하나 넣어 지구에서 달로 가는 정상 운송은 여전히 이익이라는 것을 함께 확인했습니다. 보안 수정은 게임을 망가뜨리지 않았음을 증명해야 끝납니다.

같은 잘못된 보고가 게임 바깥에서도 반복됐습니다. 홈서버를 감시하는 모니터링 앱 쪽에서는 이번 주에 사고가 계속해서 이어졌습니다. 커널 패닉이 재발했고 이어서 외장 드라이브들이 스스로 마운트가 해제됐고 백업 도구가 성공한 척하며 다섯 디렉토리 중 넷을 건너뛰고 있었고 오늘 아침에는 백업이 파일 이름이 너무 길다는 오류를 냈습니다. 확인해보니 원인은 하나였습니다. 가상 머신의 파일 공유 계층이 게스트가 한 번 스캔한 파일마다 호스트 쪽 파일 핸들을 하나씩 영구히 생성하는데 사진 관리 앱 Immich를 3.x로 올렸더니 매일 새벽 3시에 40만 개 파일을 스캔하는 무결성 검사가 기본값으로 켜져 있었습니다. 그 검사가 밤마다 핸들을 계속해서 늘려 시스템 파일 테이블을 고갈시켰고 테이블이 차면 커널이 메모리에 올린 코드 페이지를 읽지 못해 열기 함수를 한 번도 부르지 않은 프로세스가 죽습니다. 그렇게 로그인 관리 프로세스가 죽자 시스템이 콘솔 로그아웃으로 오인하고 외장 볼륨을 전부 언마운트하려 든 것이 드라이브 사고였고 백업 도구의 볼륨 인식이 그때 실패해 회복되지 못한 것이 조용한 실패였습니다. 이 사고 과정에서 가장 순수한 형태로 앞서 소개한 잘못된 보고가 나왔습니다. 백업이 넷을 건너뛰던 사흘 동안 오류가 넷이라는 알림이 정확하게 떠 있었습니다. 개수도 맞고 분류도 맞고 임계치도 스케줄도 다 정상이었습니다. 그 알림이 실제로 전달한 것은 다섯 디렉토리 중 넷이 백업 대상에서 빠졌다는 것인데 사람이 읽은 문장은 파일 하나를 못 읽었을 때 나오는 문장과 똑같았습니다. 값은 맞고 범주가 없었습니다. 필요했던 것은 새 프로브가 아니라 이름이었습니다. 그래서 판별식을 백업 도구는 아니라고 하는데 호스트는 맞다고 한다는 논리곱으로 바꾸었습니다. 로그 한 줄만으로는 증거가 안 됩니다. 드라이브를 뽑아도 똑같이 나타나기 때문입니다.

docker-monitor 봇이 텔레그램으로 보낸 알림 두 건입니다. 위는 외장 드라이브가 마운트 해제되어 영향받는 스택을 내렸다는 "[CRIT] Drive 'vault' UNMOUNTED"이고 아래는 백업 도구가 디렉토리 넷을 건너뛰었다는 "[WARN] Arq plan 'B2': 4 backup folder(s) skipped"입니다. 아래 알림은 "Arq could not resolve these, but the host HAS them mounted" 아래에 건너뛴 경로와 그 볼륨이 호스트에 마운트되어 있다는 사실을 나란히 적고 있습니다.

이 문제를 규명하면서 정작 이 감시 앱 자신이 통과로 잘못 보고하고 있었다는 것도 드러났습니다. 파일 핸들 고갈에 대비한 4단 경보가 이미 있었는데 한 단도 울리지 않았습니다. 임계치가 잘못된 게 아니라 나누는 값이 틀렸습니다. 전체 한도인 2백만으로 나누고 있었는데 이 기계의 임계값은 그 3분의 1을 조금 넘는 값이라 두 번의 커널 패닉이 사실은 임계값의 100퍼센트에서 일어났는데 경보는 둘 다 35퍼센트라고 표시하고 있었습니다. 우선 재시작 없이 핸들을 반환하도록 했습니다. 컨테이너에서 게스트의 캐시를 비우면 붙잡힌 핸들이 5초 안에 풀리는데 컨테이너의 상태나 프로세스를 건드리지 않고 캐시를 다시 만드는 비용만 필요합니다. 그리고 문제의 새벽 검사를 무작정 끄는 대신 최악의 경우에도 두 배로 올린 임계값의 절반에 못 미친다는 것을 실측한 뒤 다시 켰습니다.

이 모니터링 쪽에서 배운 또 하나는 검증 계층에서 같은 실수가 반복된다는 자각이었습니다. 안전하게 종료할 수 있는 프로세스가 있는지 확인하는 세션에서 자식 셸이 없는지 확인하는 루프를 작성했는데 셸의 변수 분할 규칙 때문에 루프가 한 번만 돌고도 전부 이상 없다고 보고했습니다. 그 검사는 실패할 수 없었고 잡아낸 것은 주의가 아니라 종료 뒤에도 프로세스 수가 그대로라는 산술적 모순이었습니다. 지식으로 막을 수 없는 실수였습니다. 한 세션 전에 똑같은 종류의 실수를 기록해 두었는데도 재발했기 때문입니다. 방어는 지식이 아니라 절차, 곧 다른 각도의 두 번째 측정입니다. 그리고 명령이 가려진 인자를 돌려줄 때 즉 필요한 필드가 가려지거나 잘리거나 권한으로 막혀 있을 때 그 빈칸을 가장 그럴듯한 값으로 채우는 순간 관측이 가설로 바뀝니다. 가려진 관측은 약한 증거가 아니라 무증거입니다. 반대로 오늘 아침 파일 이름 오류를 규명할 때는 추론하지 않고 측정했습니다. 같은 파일들을 두 경로로 각각 상태 조회했더니 APFS 스냅샷 마운트를 거치는 쪽에서만 경로 길이가 한계를 넘는 정확히 그 파일들이 실패했습니다. 게다가 같은 소스 디렉토리가 같은 날 아침 다른 백업 계획에서 오류 없이 완료되어 있었으니 그 하나가 소스 손상과 볼륨과 서버를 한꺼번에 배제하는 대조군이 있었던 셈입니다. 대조군을 만드는 것보다 이미 있는 것을 찾는 것이 항상 더 쌉니다.

pytmux 쪽에서는 이번 주에 처음으로 인터넷에 노출되는 표면이 생겼습니다. 여러 컴퓨터에서 같은 클로드 계정을 쓸 때 기계 사용량 통계가 기기마다 따로 집계되어 랩탑에서 62퍼센트로 보이는 것이 데스크톱에서는 18퍼센트로 보이던 문제를 작은 동기화 서버로 해결했습니다. 여기서 처음으로 인증과 키 배포와 기밀이라는 축이 등장했고 웹 인증 표준을 직접 구현했습니다. 서버가 저장하지 않는 것을 설계의 중심에 두어서 계정 이메일 대신 익명화된 구분자만 두고 기록 내용은 각 기기가 암호화한 다음 올려 서버에는 복호화할 키가 없습니다. 서버가 통째로 유출되어도 남는 것은 암호문과 난수로 만든 익명화된 구분자와 데이터 수량 뿐입니다. 키를 기기에서 기기로 손으로 옮기던 첫 버전을 브라우저가 키를 만들어 패스키로 암호화해 서버에 두는 방식으로 고치면서 서버가 키에 접근할 수 없도록 하는 것을 핵심으로 삼았습니다. 코드를 서버가 만들면 그 순간 서버도 암호화한 키를 풀 수 있으니 코드는 브라우저가 만들고 서버에는 해시만 올립니다. 그런데 여기서도 같은 잘못된 보고가 반복됐습니다. 실제 브라우저로 로그인을 해 보니 화면에 내부 오류 한 줄만 나타났습니다. 요청마다 새 스레드를 만드는 서버가 데이터베이스 연결을 한 스레드에서 열어 둔 탓에 데이터를 건드리는 모든 경로가 실패했는데 헬스체크와 정적 파일은 데이터베이스를 확인하지 않아 정상이었고 테스트도 한 스레드에서만 불러 통과였습니다. 기기를 추가할 때도 화면에 알 수 없는 오류 한 줄만 나타나고 등록이 안 됐는데 확인해보니 서로 다른 네 가지 결함이 나왔습니다. 넷 다 테스트가 실제 실행 형태를 재현하지 않아 통과됐습니다. 원인만 있고 조치 방법이 없는 오류는 절반만 일한 것이고 그 조치는 한 줄 안에 들어가야 합니다. 상태줄 알림이 한 줄이라 인증서 오류 문구가 잘려서 정작 조치가 안 보인 것을 계기로 알림에 성공과 실패를 색과 기호로 구분하는 등급을 붙이고 지나간 알림을 다시 펼쳐 보는 이력을 넣었습니다. 그리고 새로 생긴 인터넷 표면을 그날 바로 보안 검수했는데 방어가 정당한 사용자를 가두면 안 된다는 것이 일관된 교훈이었습니다. 패스키를 지운 사용자가 로그인도 새 등록도 막혀 버리는 단일 실패점을 세션만 되찾아 주고 기록은 여전히 못 읽는 복구 코드로 해결했습니다.

pytmux에서 ESC 다음 콜론을 눌러 명령 프롬프트를 연 화면입니다. 동기화 서버에 이 기기를 등록하는 "claude-token-sync enroll"에 웹에서 복사한 페어링 코드를 인자로 넣는 중이고 오른쪽에는 그 명령의 설명이 붙습니다. 코드는 합성값이며 입력칸 폭에 맞춰 앞 묶음만 보입니다.

이 보안 검수를 따로 돌린 이유는 신뢰 경계가 바뀌었기 때문입니다. 그때까지 이 도구의 공격면은 제 머신과 ssh 연결뿐이어서 신뢰 경계가 셸 접근권과 같았는데 동기화 서버는 누구나 HTTP를 보낼 수 있는 종단이고 인증 전에 상태를 쓰는 경로가 있습니다. 서버가 데이터를 복호화하지 못하는 설계라 검수의 초점은 기밀이 아니라 무결성과 가용성과 자원이었습니다. 방법은 코드를 정독해 가설을 세우고 실제로 찔러 수치를 얻은 다음 처방하는 것이었고 통과하는 테스트는 증거로 인정하지 않았습니다. 가장 심각한 것은 인증 없이 새 저장소를 만들 수 있다는 것입니다. 50번 시도해 50번 성공했고 저장소마다 500만 행 쿼터가 붙어 있으니 디스크가 상한 없이 늘어납니다. 페어링 코드를 다섯 번 틀리면 남의 유효한 코드까지 지워지는 것도 나왔습니다. 유효한 코드 3개가 0개가 됐으니 아무나 남의 기기 등록을 막을 수 있다는 뜻입니다. 미인증 챌린지는 정리되지 않고 메모리에 2000개가 상주했습니다. 그리고 쿼터를 넘겼을 때가 가장 나빴습니다. 3행이 부분 저장되고 사용량이 0으로 남고 트랜잭션이 열린 채 방치됐는데 사용량이 0이면 쿼터가 다시는 걸리지 않습니다. 처방은 각각 첫 등록 뒤 신규 저장소 잠금과 코드별 오답 계수와 챌린지 상한과 사전 쿼터 판정을 넣은 배치 트랜잭션이었고 되돌리면 실패하는 회귀를 하나씩 붙였습니다. 페어링 코드 길이를 40비트에서 128비트로 올린 것은 취향이 아니라 필수였습니다. 서버에 암호화된 키 뭉치가 있으니 코드가 짧으면 오프라인에서 대입할 수 있기 때문입니다.

나머지 도구들도 이번 주에 조금씩 고쳤습니다. 형상관리 이력을 브라우저에서 보는 웹 클라이언트 p4-web은 리뷰 도구로 성격을 수정해 변경마다 승인이나 보완 필요 같은 상태를 붙이고 diff 안에서 줄에 직접 코멘트를 달고 멘션하면 배지가 뜨게 됐습니다. 이 도구는 지난 주에 바로 이 회고의 근거를 조회하려고 만든 것인데 이번 주 이 조사도 결국 같은 이력 열람에 기대고 있으니 도구가 도구를 설명하는 셈입니다. 모바일에서 diff가 화면을 넘치던 문제를 수정해 테이블 셀은 내용의 최대 폭으로 커지고 가로 스크롤 속성이 그걸 막지 못한다는 것을 배웠습니다. 이 도구에서는 서버 쪽 사고도 둘 잡았습니다. 이미지 미리보기가 가득한 문서에서 페이지를 떠나면 진행 중이던 내려받기가 한꺼번에 취소되는데 스트리밍하던 형상관리 명령이 아무도 비우지 않는 파이프에 계속 쓰다 막혀 버려 형상관리 서버 연결을 붙든 채 남았습니다. 한때 그렇게 놀고 있는 연결이 마흔한 개까지 쌓였고 취소되는 경로에서도 그 프로세스를 확실히 거둬들이게 고쳤습니다. 스크린샷이 백 장 넘게 포함된 문서에서 뒤쪽 그림이 조용히 안 뜨던 것도 서버가 파일 핸들이 부족해서였는데 터널이 브라우저의 이미지 요청을 한꺼번에 열게 만들고 요청 하나마다 형상관리 프로세스 둘이 붙는 탓이라 한도를 올리고 스트림 시작을 몇 개씩 나눠 여는 것으로 해결했습니다. 개인 도메인의 활동 타임라인을 갱신하다가 카드에 걸린 개미 그래픽이 구형이라는 제보를 받고 확인해보니 형상관리에 올라간 최신 스크린샷이 사실 최신 그래픽이 아니었습니다. 스크린샷이 그래픽 리모델보다 29분 앞서 찍혀 있었던 것이고 최신본이 곧 최신 그래픽이라는 가정이 그 컷에서 틀렸습니다. 최신이라고 믿은 것이 사실은 낡아 있던 이번 주 내내 반복된 이야기의 또 다른 사례였습니다. 실측 천체 우주 항해 시뮬 쪽에서는 2024년에서 2026년 사이의 실제 관측 넷을 게임에 옮겼습니다. 가장 가까운 태양형 별의 거주가능대에서 제임스웹 우주망원경이 코로나그래프로 직접 촬영한 가스행성 후보를 실측 궤도로 올리되 재관측에서 아직 다시 잡히지 않은 후보라는 사실을 정직하게 표기했고[^Beichman, Sanghi et al. 2025, ApJL 989 L22 (arXiv:2508.03814)] 우리은하 중심 블랙홀이 48시간 연속 관측에서 쉼 없이 깜빡인다는 것을 부착원반 렌더에 입혀 모든 방문자가 같은 실시각에 같은 플레어를 보게 했고[^Yusef-Zadeh et al. 2025, ApJL 980 L35 (arXiv:2501.04096)] 시프트-앤-스택 기법으로 한 번에 128기가 공인되며 총 274개가 된 토성의 역행 불규칙위성 무리를 실측 궤도로 실어 반경 1킬로미터짜리 게임 최소 천체까지 착륙할 수 있게 했고[^Ashton, Gladman, Alexandersen & Petit, RNAAS (DOI 10.3847/2515-5172/adbf87)] 두 볼프-레이에 별의 충돌하는 항성풍이 빚은 먼지 나선을 지상 관측이 세 겹 바람개비로 보던 것을 제임스웹이 네 겹 동심 껍질로 정정한 아펩 성운을 심우주 목적지로 넣었습니다[^Han et al. 2025 (arXiv:2507.14498, JWST/MIRI)]. 이 넷이 연구를 게임에 옮기는 방식을 잘 보여 줍니다. 실측 수치를 1차 출처로 재검증해 넣고 확정되지 않은 것은 후보로 표기하고 공개 데이터가 없는 부분은 근사임을 원장에 남기고 1차 출처가 2차 자료를 정정하면 그대로 따라 고칩니다. 그 밖에 터미널 앱의 에이전트 연동을 양방향으로 넓히고 윈도우 화면 돋보기의 켜기 지연이 런처와 무관하다는 것을 실측으로 확인하고 가계부 앱에 명세서 대조 절차를 넣은 자잘한 일들도 그 사이에 있었습니다.

마지막으로 이 모든 일이 벌어진 작업 환경을 정리합니다. 이 게임들과 도구들은 여러 개의 기계 세션이 같은 퍼포스 워크스페이스를 동시에 쓰면서 만들어집니다. 그리고 이번 주에 배운 것은 공유되는 것이 저장소만이 아니라는 것입니다. 체인지리스트를 지정하지 않고 파일을 연 사이에 병행 세션이 제출하면서 현재 프로젝트의 코드가 엉뚱한 체인지리스트 설명에 실려 나갔고 설명만 고치려다 열려 있던 파일 예순다섯 개가 통째로 디폴트 체인지리스트로 빠졌고 인자 없는 명령 하나가 남의 세션 파일을 현재 프로젝트의 체인지리스트로 끌어왔습니다. 한번은 골든 테스트의 새 해시만 제출되고 그 해시를 만드는 코드는 빠져 잠깐이지만 모두의 골든이 실패하는 상태가 저장소에 남기도 했습니다. 파일만 공유되는 게 아니라 프로세스 공간도 공유되어서 이름으로 프로세스를 죽이는 명령 한 줄이 병행 세션의 스크린샷 촬영 작업을 통째로 죽였습니다. 정착한 규칙은 소유 개념으로 수렴합니다. 파일을 여는 순간부터 별도 체인지리스트를 사용하고 남의 파일이 딸려 오면 원래대로 되돌리고 프로세스는 현재 작업중인 프로젝트에서 띄운 것만 그 식별자로 죽이되 명령줄까지 대조하고 되돌릴 때는 수정 시각으로 현재 프로젝트에서 만든 것만 가리고 어느 쪽이든 숨기지 않고 체인지리스트 설명에 적는 것입니다. 종전에는 현재 프로젝트의 체인지리스트에 남의 파일이 섞이는 것만 막았는데 반대로 현재 프로젝트의 편집이 다른 프로젝트의 체인지리스트로 새어 나가는 위험은 아무것도 막지 않고 있었다는 것을 이번 주에야 알았습니다.

이렇게 한 주를 정리해 보니 지난 주와 비슷한 것과 달라진 것이 뚜렷합니다. 변하지 않은 것부터 적으면 서버가 판정하고 클라이언트는 재생만 하는 구조도 대표 상태를 해시로 고정해 두는 골든 테스트도 배포마다 따라오는 보안 레드팀도 설계 문서 한 장에서 시작해 며칠 만에 완성하는 과정도 그대로였습니다. 실제 관측을 게임에 옮기는 사이클도 이어졌고 무엇보다 이렇게 무엇을 고쳤고 그래서 무엇을 배웠는지 글로 적는 일 자체가 배움을 정리한다는 것도 지난 글과 같습니다. 달라진 것 중 가장 큰 것은 폭에서 깊이로 옮겨 간 것입니다. 지난 주가 여러 모형을 동시에 만든 주였다면 이번 주는 한 모형을 끝까지 완성한 주였고 그 결과 작업의 무게가 만드는 문제에서 만든 것을 신뢰하는 문제로 옮겨 갔습니다. 지난 주의 가장 큰 작업이 화면을 사실적으로 만드는 날이었다면 이번 주의 큰 덩어리는 그 화면이 정말 맞는지 기계가 판정하게 하는 일이었습니다. 검증이 스크린샷 첨부와 육안 검사 수준에서 분리된 자율 체계로 조직됐고 처음으로 인터넷에 노출되는 표면이 생겨 인증과 기밀이라는 새 축이 등장했고 운영 사고가 개발 의제를 정했습니다.

새로 배운 것을 한자리에 모으면 결국 처음의 이야기로 돌아옵니다. 지난 주의 축이 측정은 직관을 이긴다는 것이었다면 이번 주는 그 측정 장치 자신을 의심하는 데까지 갔습니다. 판정식이 검사 대상의 계산을 그대로 빌려 항상 통과하거나 실행이 멈췄는데 통과로 읽거나 서버 값만 보고 화면은 안 보는 세 가지를 통과가 잘못 보고되는 세 방식으로 묶은 것이 이번 주의 가장 큰 배움입니다. 지난 글에 적었던 검증 조건은 통과하는데 화면에는 아무것도 안 보인다는 것이 이 셋 중 하나의 시작이었던 셈입니다. 자동 검증을 만드는 일의 태반이 자기 오라클을 고치는 일이라는 것 동시 접속 부하는 사람들 앞에 내놓기 전에 미리 세워 두어야 한다는 것 제약을 제거할 때는 그것이 대신 지키던 것을 적어야 한다는 것 정확한 값이 범주 없이 사흘을 감출 수 있다는 것 공유되는 것이 저장소만이 아니라는 것 기계가 만든 결함은 기계가 관리하게 해야 한다는 것 그리고 실수는 진단이 아니라 검증 계층에서 반복되므로 방어는 지식이 아니라 절차라는 것. 이렇게 늘어놓고 보면 하나같이 통과했다는 표시를 곧이곧대로 신뢰하지 않는 법 리허설이 매끄럽다고 무대가 버틴다고 미리 믿지 않는 법에 관한 이야기입니다.

지난 글에서 상업 게임이라는 실물 대신 작지만 온전히 작동하는 모형을 혼자 완성할 수 있게 됐다고 말했습니다. 이번 주에 배운 것은 완성하는 것과 신뢰하는 것이 다르다는 것입니다. 테스트 스위트가 전부 통과하고 골든이 고정돼 있어도 그것이 만든 것이 맞다는 뜻은 아니었고 관객이 몰리는 순간 혼자 완성했을 때는 없던 요구가 새로 생겨났습니다. 리허설은 여전히 필요합니다. 리허설이 없으면 막을 올리기 전에 무엇이 무너질지조차 알 수 없기 때문입니다. 다만 이번 주 내내 배운 것은 리허설이 매끄럽다고 무대가 버틴다고 미리 믿지 않는 습관입니다. 통과했다는 표시를 볼 때마다 그것이 무엇에 대한 통과인지 한 번 더 물어보는 것입니다. 무언가를 한 일의 통과인가 아무것도 안 한 일의 통과인가, 판정이 실제로 무언가를 재고 있는가 아니면 자기 대본을 되풀이하고 있을 뿐인가, 배우가 자리를 지켰다는 것과 객석에서 그것이 연극으로 보인다는 것은 다른 문제이니 서버가 지킨 약속이 화면에서도 지켜지고 있는가. 관객은 언제나 리허설이 못 본 것을 봅니다. 이 질문들을 기계가 대신 던지게 만드는 것이 결국 지난 글의 측정 이야기와 같은 이야기이고 다음 주에도 아마 같은 이야기를 다른 형태로 만나게 될 것입니다. 그럼 다음에도 기계의 손을 빌려 모형을 하나씩 만들어 보겠습니다. 이번에는 막을 올리기 전에 리허설을 제대로 할 작정입니다.