녹색 신호
지난주에 만든 이슈트래커를 이번 주에 계속해서 사용하며 수정하고 있습니다. 데이터베이스를 교체했고 그 과정에서 이전까지는 통과하고 있던 테스트들이 실은 아무것도 측정하고 있지 않았다는 것을 여러 번 발견했습니다. 녹색 신호는 길이 비었다는 보고가 아니라 신호등이 녹색이라는 사실입니다.
지난 '관측'에서 개인용 이슈트래커를 직접 만든 과정을 정리했습니다. 개인적으로 지라를 6년 동안 쓰면서 이슈 2만 3천여 건을 만들었고 그동안 도구를 다시 검토한 적이 없었습니다. 에이전트에게 일을 맡기기 시작하자 처음으로 아쉬운 점이 생겨 그 후 6일에 걸쳐 만들었습니다. 그 뒤로 지라에서 관리하던 프로젝트를 조금씩 이 도구로 옮겨 왔습니다. 옮긴 다음 사용하면서 부족한 점을 수정하고 필요한 기능을 더하기를 반복하고 있습니다. 목표는 지라로 관리하던 프로젝트를 전부 이 도구로 가져오는 것입니다. 아래에서 '이 도구'라고 부르는 것이 이 이슈트래커입니다.
이번 주에는 이 도구가 남긴 개발 기록을 조사했습니다. 배포 작업이 5분 넘게 동작하고 있던 에이전트 실행을 강제 종료했다는 기록이 3일째 원인 미상으로 남아 있었기 때문입니다. 그런데 그 실행 로그를 확인해 보니 강제 종료되기 5분 42초 전에 이미 스스로 끝나 있었습니다. 기록은 종료 이유도 틀렸습니다. 로그에 남은 이유는 인증 만료이고 기록에 남은 이유는 강제 종료였습니다. 종료 시각은 5분 42초 뒤로 밀려 있었고 비용은 0에서 빈 값으로 바뀌어 있었습니다. 같은 형태로 종료된 실행이 얼마나 더 있는지 일주일치를 전수로 세어 보니 42회 가운데 38회였습니다. 감사 기록에 없던 사건이 생기고 있던 사건이 사라져 있었습니다. 이 조사를 시작한 이슈 자체가 그 잘못된 기록에서 나온 것입니다.
지난주 글을 발행한 다음 날부터 이번 주까지 일주일 동안 이 도구에 퍼포스 체인지리스트 492건이 들어갔습니다. 지난주까지 누적 496건이었으니 일주일 만에 그만큼이 한 번 더 쌓였습니다. 소스는 34,708줄에서 54,003줄이 됐고 테스트는 1,662건에서 3,009건이 됐습니다. 지난주가 만드는 작업이었다면 이번 주는 사용하면서 수정하는 작업이었습니다. 그렇게 사용하는 동안 이 도구가 남긴 기록과 이 도구가 가진 테스트를 계속 확인해야 했습니다. 위의 38회도 그 확인에서 나온 결과입니다. 녹색 신호 즉 테스트가 전부 통과했다는 것은 길이 비었다는 보고가 아니라 신호등이 녹색이라는 사실입니다. 이번 주에는 신호등의 상태와 길의 상태를 여러 번 함께 확인해야 했습니다.
가장 큰 작업은 데이터베이스 교체였습니다. 이 도구는 단일 SQLite 파일로 동작하고 있었고 그 파일이 컨테이너와 호스트의 파일시스템 경계를 넘나들면서 손상되는 일이 있었습니다. 그래서 호스트에 마운트한 PostgreSQL로 바꾸는 절차를 문서로 작성했고 3일 뒤인 8월 15일 오전 11시 1분에 컷오버를 마쳤습니다. 이 저장소가 설계 문서에 '런타임 의존성 0, npm install이 없다'로 명시해 두고 있던 원칙이 깨졌습니다. PostgreSQL 클라이언트는 표준 라이브러리에 없습니다. 의존성이 0에서 1이 됐고 그 의존성 때문에 컷오버 당일 컨테이너가 재시작 루프에 들어갔습니다.

이 데이터베이스 교체에서 가장 중요한 부분은 같은 코드의 의미가 달라진다는 것입니다. SQLite의 API는 동기이고 PostgreSQL 클라이언트는 예외 없이 비동기입니다. 그래서 데이터베이스를 부르는 곳마다 await를 더해야 합니다. 처음 수정할 곳을 세어 보니 1,932곳이었고 그 가운데 60%가 테스트 코드에 있었습니다. await를 빠뜨려도 오류가 나지 않습니다. 자바스크립트의 프로미스 객체는 언제나 true로 판정되고 배열처럼 펼치면 아무것도 나오지 않고 값과 비교하면 언제나 같지 않습니다. 즉 빠뜨린 곳은 오류 없이 잘못된 값을 반환합니다. 그래서 소스를 검사해 이런 형태를 상시로 확인하는 테스트를 따로 만들었고 결함 10건이 거기서 나왔습니다.
의미가 달라지는 부분은 문법에도 있었습니다. SQLite의 LIKE는 기본이 대소문자를 구분하지 않지만 PostgreSQL은 구분합니다. 이슈 제목 800줄로 측정해 보니 같은 쿼리가 한쪽에서는 24행 다른 쪽에서는 0행이었습니다. 오류가 아니라 '0건'이 나오는 것이 이 부류의 특징입니다. 소스에 그런 코드가 41군데 있었습니다. 정렬도 마찬가지여서 공식 이미지의 로컬 정렬은 SQLite와 551줄의 순서가 달라집니다. 알파인 리눅스 기반 이미지로 측정하면 우연히 0줄이 됩니다. 그 libc가 로컬 정렬을 하지 않기 때문입니다. 우연에 의존하는 것이므로 데이터베이스를 초기화할 때 정렬 규칙을 명시적으로 지정했습니다. 개수를 세는 쿼리는 데이터를 읽지 않았습니다. 535메가바이트짜리 테이블을 세는 데 0.00초가 나왔습니다. 빠른 것이 아니라 읽지 않은 것입니다. 무결성 검사를 그 방식으로 작성하면 아무것도 검사하지 않으면서 통과합니다.
이관 도중 '컷오프 조건'을 정의했습니다. 그전까지 인수인계 문서마다 다음에 할 일은 있어도 완료 조건이 없었습니다. 조건은 실패하는 테스트를 모두 수정해 테스트가 모두 성공해야 하고 컷오프로 도커 컨테이너를 중단한 다음 데이터베이스 파일을 퍼포스에 서브밋하는 것입니다. 그런데 이 조건이 두 가지 의미로 읽혔습니다. 이 저장소는 데이터베이스 파일을 버전 관리에서 예외 없이 제외하도록 설정돼 있고 그 제외에는 이유가 있습니다. 그렇다면 파일을 서브밋하라는 것이 그 설정까지 고치라는 뜻인가. 한쪽으로 읽으면 되돌리기 어려운 결정이 됩니다. 결론은 '둘 다 한다'였습니다. 평상시 백업은 지금 방식대로 두고 컷오버 직전에 파일을 한 번만 기록하되 그 예외 설정을 다시 제거하는 것까지 한 번의 변경으로 묶는 것입니다. 그래서 되돌리기 어려운 결정이 아니게 됐습니다. 한 문장이 두 가지 의미로 읽히고 그중 한쪽이 되돌릴 수 없는 쪽이면 어떻게 읽을지 먼저 확정한다는 규칙을 실제로 적용해본 사례입니다.
컷오버 당일에는 전체 테스트 2,795건이 모두 통과한 상태에서 세 가지 문제가 남아 있었습니다. 스키마 검사 도구가 이관에서 빠졌고 데이터베이스 클라이언트 패키지가 컨테이너 이미지에 들어 있지 않았고 마이그레이션 명령이 배포 과정에서 권한이 없어 실패했습니다. 세 가지 모두 테스트 프로세스 밖에서만 나타납니다. 테스트는 호스트에서 실행되고 이미지는 빌드할 때만 만들어지므로 이미지에서 무엇이 빠졌는지는 테스트가 아무리 많아도 미리 확인할 수 없습니다. 그래서 새로 만든 테스트는 동작 대신 이미지를 만드는 파일을 직접 확인합니다. 그날의 결론은 컷오버 뒤에는 테스트가 통과한 것으로 만족하지 말고 이미지를 만들었는지 응답하는지 복구되는지를 따로 확인해야 한다는 것입니다.
데이터베이스를 교체한 이유는 3일 전인 8월 12일 밤에 라이브 데이터베이스 손상을 겪었기 때문입니다. 증상은 테스트가 실행되고도 결과를 저장하지 못하는 한 가지였습니다. 손상된 것은 파생 데이터가 쌓이는 테이블 한 개였고 나머지 응답은 정상이었습니다. 손상은 1분 안에 일어났습니다. 마지막으로 성공한 적재가 밤 9시 51분 7초였고 4시간마다 실행되는 백업 작업이 9시 52분 6초에 실패했습니다. 여기서는 설계가 의도대로 동작했습니다. 백업을 만들기 전에 원본의 무결성을 먼저 확인하고 손상됐으면 사본을 만들지 않습니다. 그래서 백업이 실패했고 6시간 전의 온전한 사본을 새 사본으로 덮어쓰지 않았습니다. 다만 실패는 로그에만 남았고 확인한 것은 2시간 뒤였습니다. 그 뒤로 저널 방식을 바꿨고 데이터베이스 파일을 파일시스템 경계를 지나지 않는 곳으로 옮겼고 백업 보관 정책을 최신 몇 개만 남기던 것에서 하루와 한 주와 한 달 단위의 변경사항만 저장하는 방식으로 바꿔 되돌릴 수 있는 범위를 약 20시간에서 약 두 달로 늘렸습니다. 그리고 백업이 온전한지와 그것으로 복구할 수 있는지가 다른 질문이므로 매일 스냅샷으로 데이터베이스를 다시 복구해 보는 리허설 작업을 만들었습니다. 지난 '리허설'에서 완성한 것을 어떻게 검증할 것인가를 다뤘고 이번에는 백업에 같은 절차를 적용했습니다.
이번 주에 가장 오래 멈춰 있던 것은 이슈를 자동으로 가져가는 프로세스인 러너입니다. 56시간 동안 아무 일도 하지 못했습니다. 마지막으로 성공한 실행이 8월 12일 저녁 7시 57분 43초였고 첫 인증 실패가 1분 24초 뒤였습니다. 그 뒤로 인증 실패로 끝난 실행이 19건이고 그 때문에 수정되지 않고 러너를 그냥 통과해버린 이슈가 17건입니다. 진단이 어려웠던 이유는 터미널에서 같은 명령을 직접 실행하면 성공하기 때문입니다. 그래서 러너 쪽 오류를 오탐으로 판단하기 쉽습니다. 자격증명은 두 곳에 있었습니다. 파일에는 유효한 토큰이 있었고 운영체제의 키체인에도 항목은 온전히 남아 있었지만 토큰 두 개가 빈 문자열이었습니다. 터미널은 대화형 세션이 아니어서 키체인을 읽지 못하고 파일로 폴백해 성공하고 백그라운드에서 실행되는 러너는 키체인을 읽을 수 있어서 빈 값을 읽어 277밀리초 만에 실패했습니다. 키체인을 읽을 수 있는 쪽이 실패했습니다. 실행 시간이 수백 밀리초라는 것은 네트워크 요청을 한 번도 보내지 않았다는 뜻이고 이것이 단서가 되었습니다.
원인을 기록한 변경은 조치 방법을 재로그인으로 기록했습니다. 그런데 다음 날 작성한 문서가 이 관측이 틀렸다는 것을 확인했습니다. 재로그인을 해 보니 파일 쪽 토큰은 갱신됐지만 키체인 쪽은 빈 문자열 그대로였고 백그라운드 프로브의 증상은 변하지 않았습니다. 문제를 해결한 방법은 그 빈 키체인 항목을 삭제한 것입니다. 삭제한 직후에 프로브가 성공했습니다. 이 프로그램은 키체인 항목이 있으면 그것을 먼저 쓰고 없어야 파일로 폴백하기 때문입니다. 그리고 순서가 중요해서 파일에 유효한 토큰이 있는 상태에서 삭제해야 합니다. 러너 코드 자체는 문제가 없었습니다. 인증이 연속으로 실패하면 큐 수행을 멈추고 10분마다 다시 확인해 표시하는 테스트가 이미 있었고 설계대로 정확히 동작했습니다. 다만 이 테스트가 막는 것은 실패를 알리지 못한 채 실행하는 것까지입니다. 사람이 개입하지 않으면 이슈를 처리하지 못합니다.
기록과 테스트를 계속 확인하게 된 이유는 잘못된 기록과 멈춘 러너만이 아닙니다. 일주일치 변경을 전부 살펴보면 같은 상황이 반복됩니다. 통과하고 있던 테스트가 아무것도 측정하지 않고 있는 곳이 계속 나타납니다. 웹의 위조 요청 방어에 쓰는 비밀키가 컨테이너의 쓰기 레이어에 저장돼서 배포할 때마다 새로 생기고 있었고 그 값을 테스트 파일 27편은 자기 임시 디렉토리에 기록해 두고 있어서 테스트에 언제나 통과했습니다. 비동기 함수가 예외를 던지는지 확인하는 검증 조건이 31군데 있었고 그 형태는 언제나 통과합니다. 즉 막아야 할 것이 막히지 않아도 통과합니다. 검색 결과에서 원시 HTML이 나오지 않는지 확인하던 테스트는 PostgreSQL의 발췌 함수가 닫힌 태그를 지운다는 사실 때문에 아무것도 확인하지 못하고 있었습니다. 화면 문구를 검사하던 테스트 여럿은 스타일시트 주석에 그 문구가 그대로 들어 있어서 코드가 아니라 주석을 보고 판단하고 있었습니다. 어떤 테스트는 윈도우에서 가정이 영원히 성립하지 않아 검사를 한 번도 실행하지 않고 실패로 끝났고 그 실패가 오히려 아무것도 측정하지 않고 있다는 사실을 덮고 있었습니다. 이 가정을 고치자마자 위반을 발견했고 그것은 같은 세션이 방금 만든 변경의 결과였습니다.
이런 일을 계속 겪다 보니 테스트를 새로 만들 때마다 이전 코드로 되돌려 확인하는 절차를 추가했습니다. 새 테스트를 만들고 나서 고치기 전 코드로 되돌려 그 테스트가 실패하는지 먼저 확인합니다. 실패하지 않으면 이 테스트는 성공했다고 판단할 수 없습니다. 그렇게 확인해 보면 예상과 다르게 나오는 경우가 생깁니다. 어떤 회차는 되돌렸을 때 네 건 가운데 세 건만 실패했고 두 축이 서로를 가리지 않는다는 뜻이었습니다. 어떤 회차는 일곱 번째 테스트가 양쪽에서 모두 통과했고 과잉 차단을 막는 반대편 검증이라 설계대로였습니다. 넓히는 변경에는 반대편을 확인하는 테스트가 같이 있어야 한다는 규칙이 거기서 나왔습니다.
같은 확인을 테스트 대신 작업 트리에 적용했더니 더 큰 문제를 발견할 수 있었습니다. 큐에 밀려 있던 이슈 148건을 전부 판정하는 작업을 하다가 한 저장소의 작업 트리가 버전 관리 서버의 내용과 다르다는 것을 발견했습니다. 84개 파일이 오래된 버전이었습니다. 그런데 동기화 도구는 최신 상태라고 답했습니다. 서버가 가진 목록만 최신이었고 파일은 최신이 아니었기 때문입니다. 되돌아간 파일을 열어 고쳐 제출한 변경이 이전 상태를 그대로 서버에 제출해 3일 전에 제출한 변경을 지웠습니다. 그리고 아무도 이를 경고하지 않았습니다. 그 변경을 검사하는 테스트가 그 트리에 있었고 거기서는 테스트 파일도 함께 되돌아가 있었기 때문입니다. 서버 내용만으로 트리를 새로 구성해서 실행하면 4건이 실패하고 작업 트리에서 실행하면 실패하지 않습니다. 그 테스트가 아예 없기 때문입니다. 5일 동안 서버 쪽은 실패했고 트래커에는 통과로 기록돼 있었습니다. 다른 다섯 저장소는 정상이었고 이 한 저장소만 퍼포스 워크스페이스와 같은 위치에 깃 저장소를 함께 두고 있어서 한 디렉토리를 두 버전 관리 시스템이 동시에 관리하고 있었습니다. 여기서 남긴 규칙은 작업 트리가 서버 내용과 같다고 가정하지 않는 것이고 그것을 확인하는 명령을 지금 아무도 부르고 있지 않다는 사실도 함께 기록했습니다.
이번 주에는 이 도구가 쓴 시간도 처음으로 측정했습니다. 끝난 실행 480건과 로그 1.18기가바이트를 파싱해서 도구 호출 31,868건을 분류했습니다. 결과는 예상과 달라서 도구 시간의 64%인 42.2시간이 검증 루프였습니다. 전체 테스트를 한 번 실행하는 데 125초입니다. 실행 한 건이 그것을 중앙값 5회, 상위 10%는 17회, 최대 53회 실행하고 있었습니다. 그리고 이슈를 전혀 닫지 못한 실행이 232건이고 그것이 러너 시간의 47%인 103.2시간이었습니다. 코드를 찾아 읽는 시간은 호출 수로는 1위이지만 전체의 5.6%로 가장 작았습니다. 같은 검토에서 이슈를 잘게 쪼개면 빨라지는지와 난이도에 따라 모델을 골라 쓰면 비용이 줄어드는지를 함께 조사했고 둘 다 지금은 권하지 않는다는 결론에 도달했습니다. 쪼개는 도구는 이미 만들었지만 사용 건수가 0이었고 착수 전에 알 수 있는 신호로는 난이도를 구분할 수 없습니다. 조건을 전부 고정한 묶음에서도 하위 10%가 0.1분이고 상위 10%가 53분이었습니다. 그리고 이 러너가 쓰는 것은 돈이 아니라 사용량 한도라서 조건 자체가 달랐습니다. 검토가 끝난 그날 부분 실행 기능이 코드로 들어갔고 러너에게 건네는 프롬프트에 이전 전체 테스트 결과를 기준으로 삼도록 했습니다.
웹 화면도 여러 부분 수정했습니다. 지난주 글을 쓸 때는 웹에서 메모를 남길 방법이 없었습니다. 애초에 이 도구의 설계 철학이 이슈를 쓰고 수정하고 테스트하는 모든 절차를 사람이 하지 않는다는 것이었기 때문에 딱히 이상한 일은 아니었습니다. 문서 화면의 안내에 남기는 길은 MCP 도구라고 명시돼 있었고 화면이 읽는 쪽을 터미널로 보내고 있었습니다. 그것이 5일 만에 판 비교, 퍼머링크, 초안 자동 저장, 이미지 붙여넣기, 작성자에 따른 배경색 구분으로 확장되었습니다. 브라우저 없이 동작하는 저장소였지만 이번 주에는 다른 프로젝트의 헤드리스 브라우저를 빌려서 화면 좌표를 측정하는 여러 가지 변경을 했습니다. 테이블이 화면을 넘치는 문제를 측정해 보니 넓은 모니터일수록 더 넘쳤습니다. 제목 칸의 최대 폭은 화면 너비를 기준으로 정했고 본문은 정해진 폭에서 멈추기 때문입니다.
이슈 본문이 지목한 원인이 자주 틀렸습니다. 좁은 화면에서 한국어 라벨이 글자마다 세로로 쪼개지는 문제는 이슈가 지목한 설정 때문이 아니었고 그 설정을 되돌려도 증상이 그대로였습니다. 스크롤이 튀는 문제는 이슈에 스크롤 위치 값이 변하지 않아야 한다고 기록돼 있었지만 직접 측정해 보니 반대였습니다. 그 값은 한 번도 틀린 적이 없었고 오히려 고친 뒤에 움직였습니다. 브라우저가 스스로 보정한 결과였기 때문입니다. 움직인 것은 스크롤 위치가 아니라 그 위치에서 보이는 화면입니다. 그래서 그 테스트는 일부러 스크롤 위치를 측정하지 않습니다. 그것을 정답으로 삼으면 옳은 수정을 실패로 판단하게 됩니다. 수정을 맡은 실행이 이슈의 진단이 틀렸다는 것을 직접 측정으로 확인하고 그 사실을 체인지리스트 설명에 남기는 일이 이번 주에만 여러 번 있었습니다.
주 끝에 이 도구를 혼자 쓰지 않게 될 가능성이 생겼습니다. 8월 14일에 두 가지 작업을 시작했습니다. 첫 번째는 이 도구를 팀에 배포하는 것이고 조건이 있었습니다. 라이선스 문제로 도커 데스크탑을 쓸 수 없습니다. PostgreSQL 없이는 지금 코드가 성립하지 않으므로 해결해야 하는 문제는 도커 없이 PostgreSQL을 갖는 방법입니다. 그리고 아직 비어 있는 부분이 있습니다. 빈 데이터베이스에 테이블을 만드는 명령이 없습니다. 지금 라이브의 테이블은 이관 도구가 만들었고 그 도구는 컨테이너가 있어야 합니다. 도커가 없는 곳에는 첫 테이블을 만들 방법이 없습니다. 그리고 이 절차는 작성한 환경에 도커도 PostgreSQL도 없어서 한 줄도 실행해 보지 않았습니다.
같은 날 시작한 다른 작업은 매뉴얼이었습니다. 이 도구는 사람이 직접 이슈를 만드는 도구가 아니라는 점을 단서로 달았고 정리해 놓고 보니 지난주 글의 논지가 그대로 확인됐습니다. 이 트래커를 쓰는 것은 사람이 아니라 LLM 세션입니다. 사람은 읽고 정하는 쪽이고 쓰는 쪽은 세션입니다. 쓰는 길도 한 가지뿐입니다. 지라에서는 사람이 폼을 채우고 여기서는 세션이 도구를 부릅니다. 지라에서 본문은 무엇을 해 달라는 짧은 요청이고 여기서 본문은 측정한 값과 근거가 들어간 조사 기록입니다. 그래서 이 도구를 지라처럼 쓰면 효과가 없습니다. 지라에서 효과는 일감이 흐르는 데서 나오고 여기서 효과는 다음 세션이 같은 것을 다시 조사하지 않는 데서 나옵니다. 좋은 이슈에는 무엇을 만족하면 이 이슈를 닫을 수 있는지 기록하는 '완료 조건'과 '여기서 측정하지 못한 것'이 들어갑니다. 완료 조건이 없으면 다음 세션은 언제 끝났는지 모르고 측정하지 못한 것이 없으면 보지 않은 것을 정상으로 판단합니다.
이번 주에 이 도구에 새 프로젝트를 등록했습니다. 그것이 이 블로그입니다. 8월 9일에 등록했고 지금 프로젝트 목록에 'Blog'가 추가되었습니다. 지난주 글을 쓸 때는 스무 개 남짓한 그 목록에 블로그가 없었습니다. 다만 등록하고 나서 며칠 동안 그 프로젝트에 들어 있는 것은 문서 두 편뿐이었고 이슈는 없었습니다. 목록에 등록됐다는 것과 작업이 여기서 진행된다는 것은 다른 사건입니다. 지난주에는 당분간 지라와 이 도구를 나란히 쓰겠다고 기록했고 이번 주에 그 당분간은 어느 프로젝트를 언제 옮기느냐를 정하는 일이 됐습니다.
그리고 이 글을 쓰는 오늘 그 순서가 정해졌습니다. 8월 16일에 지라의 블로그 프로젝트 전체가 넘어왔습니다. 이슈 2,368건, 코멘트 2,815건, 하위 이슈 803건, 첨부 220건, 마감일 680건이 지라에 기록된 값 그대로 따라왔습니다. 완료 시각을 오늘로 덮지 않는 것이 이 이관의 규칙입니다. 먼저 열린 상태로 만들고 그다음에 지라에 기록된 날짜로 닫습니다. 그런데 이슈만 넘어오고 글이 없었습니다. 이슈 본문이 가리키는 것은 주소뿐이고 초안은 컨플루언스에 발행된 결과는 블로그에 있었기 때문에 트래커에서 이슈를 열면 무엇을 쓰려던 일인지가 링크 한 줄로만 남아 있었습니다. 그래서 같은 날 컨플루언스 960쪽과 발행된 글 70편을 받아 문서로 등록했고 문서 1,030편을 이슈 1,063군데에 연결했습니다. 글자로 세면 약 1,058만 자입니다. 그리고 지금 읽고 계신 이 원고의 티켓도 그 2,368건에 들어 있습니다. 몇 시간 전까지 지라에 있던 '이번주의 개발'이 지금은 이 도구에 들어와 있습니다.

지금 이 도구의 큐에는 일감 142건이 밀려 있고 실행 중인 것은 한 건입니다. 절반 넘게는 트래커 자신과 pytmux가 차지하고 있고 나머지는 도커 모니터링 도구와 다른 프로젝트입니다. 가장 오래 기다린 것은 27시간째 기다리고 있습니다. 그중 열한 건이 사람이 답한 것을 들고 이어서 간다는 메모를 달고 있고 그것이 이 도구가 막혔을 때 하는 일입니다. 러너가 판단이 나뉘는 대목을 만나면 직접 정하지 않고 질문 이슈를 만들어 사람에게 선택지를 제시합니다. 사람이 고르면 그 답을 포함해 다시 큐에 들어갑니다. 어제와 오늘 그 왕복이 열한 번 있었습니다. 그중 한 건은 산문에 기록된 이슈 번호를 문서와 이슈의 연결로 삼을 것인가였습니다. 지금 규칙으로는 34건이지만 전부 세면 2,263건으로 66배가 되고 그 2,263건의 71%는 인덱스와 보고서 다섯 편에서 나옵니다. 한 번 만든 연결은 지우기 전까지 남으므로 되돌리는 비용이 큽니다. 그래서 그 실행은 코드를 한 줄도 고치지 않고 상한을 어디에 두면 몇 건이 남는지 표를 만들어 사람에게 제시했습니다.
그 큐에 이 글을 시작한 건도 올라 있습니다. 42회 가운데 38회가 사실과 다르던 기록입니다. 확인해 보니 배포 쪽 검사에는 문제가 없었습니다. 배포가 그 요청을 처리했을 때 강제 종료했다는 실행은 이미 5분 20초 전에 끝나 있었고 그 기록이 어긋난 원인은 러너가 종료되면서 이미 끝난 실행에 종료 보고를 한 번 더 보낸 것입니다. 그 보고가 종료 이유와 종료 시각과 비용을 덮었습니다. 트래커 쪽은 오늘 새벽에 고쳐 제출했습니다. 종료 보고를 결과와 중단으로 나누고 중단은 아직 끝나지 않은 실행에만 기록되게 했습니다. 그런데 그것이 라이브에 적용되려면 러너 쪽에 한 줄이 더 필요하고 그 파일은 다른 세션의 제출되지 않은 변경에 열려 있습니다. 퍼포스에서 한 파일은 한 변경에만 열립니다. 지금 고치면 그 한 줄이 다른 세션의 관심사와 함께 다른 세션의 설명으로 제출되고 그 변경을 대신 정리하는 것은 다른 세션의 미완성 작업을 처분하는 일입니다. 그래서 어느 쪽도 하지 않고 고칠 한 줄을 패치로 이슈에 첨부한 채 큐에 올려 뒀습니다. 그때까지 그 잘못된 기록은 하루 몇 건씩 계속 쌓입니다.
큐에 들어가지 못한 열린 이슈가 이 도구의 방향을 더 잘 보여 줍니다. 지금 이슈를 만들려면 사람이 클로드 앱이나 명령줄을 실행해야 합니다. 브라우저로 트래커를 보다가 이슈를 남겨야겠다고 판단하는 시점이 그때이지만 거기서 할 수 있는 일이 없고 아이폰에서는 방법이 없습니다. 그래서 웹에 작성 상자를 만드는 이슈가 올라 있고 그 설계에서 중요한 것은 제목 칸을 만들지 않기로 한 부분입니다. 제목을 붙이는 것이 이 기능이 기계에게 시키는 일의 절반이고 칸을 두면 사람은 거기에 요약을 쓰고 그러면 짧게 쓰고 마는 쪽으로 돌아가기 때문입니다. 사람이 쓴 글 그대로 이슈가 먼저 등록되고 그다음에 기계가 그것을 읽어 트래커 규약대로 다시 씁니다. 둘을 한 사건으로 묶지 않는 것이 그 이슈가 정한 규칙입니다. 다시 쓰기가 실패해도 사람이 쓴 글은 이미 이슈로 등록돼 있어야 하고 등록이 됐는지와 정리가 됐는지는 다른 질문이기 때문입니다.
나머지도 방향이 같아서 이슈 상태에 '진행 중'을 더해 러너가 처리 중인 것과 아무도 가져가지 않은 것을 구분하는 일이 있고 이슈와 문서에 세줄요약 버튼을 추가하는 일이 있습니다. 그런데 웹은 요약을 만들 수 없습니다. 바깥 의존성을 두지 않는 규칙 때문에 이 도구에서 언어 모델을 부르는 곳은 러너뿐이고 그래서 그 버튼은 요약을 만드는 것이 아니라 일감을 큐에 넣는 것이 됩니다. 그리고 아직 각 저장소에 남아 있는 산문을 트래커가 전부 흡수하고 저장소에는 진입점 한 편만 남기는 일이 있습니다. 지금 스무 곳 중 열 곳은 그 진입점조차 없습니다. 마지막으로 오늘 추가한 일감은 화면과 명령줄의 모든 문구를 높임말로 고치는 것입니다. 위에서 매뉴얼이 읽는 쪽과 쓰는 쪽을 나눈 구분은 그대로입니다. 바뀌는 것은 읽는 쪽에게 보여 주는 말투이고 그런 문구가 늘고 있습니다. 그리고 주 끝의 그 팀 배포 작업이 성립하면 읽는 쪽이 저 혼자가 아니게 됩니다. 그 절차 문서는 아직 한 줄도 실행해 보지 않은 채로 남아 있습니다.
일주일을 되돌아 보면 고친 것보다 측정하고 있는 줄 알았지만 측정하지 않고 있던 것을 찾은 쪽이 많았습니다. 데이터베이스를 세는 쿼리는 데이터를 읽지 않고 있었고 예외를 확인하는 검증 조건은 언제나 통과하고 있었고 화면 문구를 확인하는 검사는 주석을 보고 판단하고 있었고 작업 트리는 서버 내용이 아니었고 감사 기록은 없던 사건을 기록하고 있었습니다. 지난주 글의 제목이 '관측'이었고 이번 주는 그 관측 장치 자체가 여러 곳에서 아무것도 보고 있지 않았다는 이야기입니다. 3천 건이 넘는 테스트가 전부 통과하는 저장소에서 그런 곳을 일주일 동안 열 번 넘게 찾았다는 것이 이 주의 요약입니다. 녹색 신호는 길이 비었다는 뜻이 아닙니다. 신호등이 녹색이라는 뜻이고 고장 난 신호등도 녹색일 수 있습니다. 녹색은 옳다는 뜻이 아니라 물어본 것에 대해 아니라고 답하지 않았다는 뜻입니다. 무엇을 물었는지 확인하지 않으면 그 녹색은 침묵과 구별되지 않습니다. 다음에도 녹색 신호가 무엇을 확인한 결과인지 살펴보겠습니다.