본문으로 건너뛰기
에이전트에게 테스트를 맡기기 전에, 판정층을 검수한다

에이전트에게 테스트를 맡기기 전에, 판정층을 검수한다

2026-07-23

검증 리포트의 한 줄이 앞뒤가 맞지 않았다. 같은 행에서 키 동기화는 실패인데, 그 옆 칸의 암호 테스트는 만점이었다. 키가 넘어가지 못했는데 그 키로 하는 암복호는 전부 통과했다는 그림이다. 나는 그 모순에서 출발해 “왜 이 플랫폼에서만 이런 조합이 나올까” 를 파기 시작했다.

그 질문은 처음부터 존재하지 않는 질문이었다. 러너는 동기화가 실패하면 암호 테스트를 건너뛰도록 되어 있었고, 그날은 실제로 건너뛰었다. 다만 리포트가 그 빈칸을 채우려고 결과 디렉터리의 파일을 읽었는데, 그 파일이 이틀 전 실행이 남긴 잔재였다. 이번 실행은 그 파일을 만들지도, 지우지도 않았다. 만점은 이틀 전의 만점이었다.

테스트 자동화를 에이전트에게 넘기는 일에서 대부분의 검수는 두 곳을 향한다. 에이전트가 짠 테스트 코드가 맞는지, 그리고 그 테스트가 제품을 사람과 같은 방식으로 조작하는지. 두 번째 문제는 앞선 글에서 한 번 정리했다. 그런데 그 위에 층이 하나 더 있다. 실행의 결과를 읽어 성공과 실패를 판정하는 층이다. 나는 이 층을 판정층이라 부르기 시작했다. 테스트가 아무리 정확해도, 판정층이 결과를 잘못 읽으면 에이전트는 그 잘못된 읽기를 근거로 다음 행동을 정한다. 이 글은 지난 한 달 동안 그 층에서 겪은 것들을 점검 항목으로 정리한 기록이다.

판정층은 에이전트의 센서다

에이전트가 도는 방식은 산출물 검수가 상정하는 것보다 한 바퀴 크다. 행동하고, 결과를 관찰하고, 관찰에 근거해 다음 행동을 정한다. 제어 루프의 언어로 옮기면 산출물 검수는 액추에이터를 점검하는 일이고, 판정층은 센서에 해당한다. 그리고 센서를 점검하는 사람은 드물다.

센서 고장이 위험한 이유는 고장 자체가 아니라 고장의 형태에 있다. 액추에이터가 고장 나면 결과물이 깨지고, 깨진 결과물은 눈에 띈다. 센서가 조용히 틀리면 루프는 멀쩡히 돌면서 엉뚱한 곳으로 수렴한다. 발산의 형태가 크래시가 아니라 그럴듯한 오답이다.

사람과 에이전트가 갈라지는 지점도 여기다. 사람은 앞뒤가 맞지 않는 표를 보면 대개 멈춘다. ‘이거 좀 이상한데’ 라는 감각이 작동해서, 데이터를 더 파기 전에 데이터를 만든 장치를 의심한다. 에이전트는 그 자리에서 멈추지 않는다. 이상한 관측을 설명하는 가설을 만드는 데 능하고, 그 능력은 전제가 틀렸을 때도 똑같이 잘 작동한다. 그래서 판정층의 결함은 에이전트에게 넘기는 순간 비용이 커진다. 사람 혼자였다면 리포트 한 장을 갸웃하고 넘어갔을 일이, 에이전트가 붙으면 잘못된 전제 위에 정합적인 조사 계획으로 자라난다.

아래 다섯 절은 그 층에서 실제로 무엇을 놓쳤고, 지금은 무엇을 다시 보는지에 대한 것이다.

종료상태가 어디서 유실되는지 본다

가장 흔하고 가장 알아보기 쉬운 형태는 하니스가 실패를 무해한 잡음으로 뭉개는 것이다. 그리고 그 대부분은 종료상태가 중간에서 사라지는 데서 온다.

원격에서 도구를 돌리는 러너가 이런 모양이었다.

# the pipeline's exit status is sed's, not the tool's
run_tool "$input" | grep 'RESULT:' | sed 's/^RESULT: //'

파이프라인의 종료상태는 마지막 명령의 것이다. 앞단이 무슨 일을 당했든 sed 가 정상 종료하면 0 이다. 실제로 그 도구는 특정 플랫폼에서 간헐적으로 세그멘테이션 폴트로 죽고 있었다. 죽었으니 결과 줄이 나올 리 없고, 러너는 빈 출력을 받아 ‘결과 파싱 실패, 종료코드 0’ 으로 보고했다. 리포트만 보면 “실동작은 성공인데 파서만 실패” 라는 그림이 된다. 파서를 고치러 가면 하루를 버린다.

같은 유실이 다른 옷을 입고 나오기도 한다. 어떤 러너는 테스트 실행 파일의 표준 에러를 2>/dev/null 로 버리고 있었다. 실행 파일이 의존 라이브러리를 찾지 못해 로드 단계에서 죽으면서 그 사실을 표준 에러로 내고 있었는데, 표준 출력에는 아무것도 없으니 빈 출력이 전부 ‘불일치’ 로 집계됐다. 160개 테스트 벡터가 전건 실패한 것처럼 보였고, 겉보기에는 암호 구현이 통째로 잘못된 그림이었다.

고친 방법은 실행과 판정을 분리하는 것이었다. 러너가 원출력을 파일로 받아 실행체 자신의 종료코드를 잡고, 신호로 죽었으면 파싱을 시도하는 대신 별개의 상태를 내보내게 했다.

# after — run first, judge later; a signal death gets its own state
run_tool "$input" > "$raw" 2> "$err"
rc=$?
if [ "$rc" -ge 128 ]; then
    echo "CRASHED signal=$((rc - 128))"
    tail -40 "$raw" "$err"
    exit 1
fi
grep 'RESULT:' "$raw" | sed 's/^RESULT: //'

상류의 리포트도 이 마커를 알아보고 CRASHED (signal N) 으로 표시하게 했다. 그러자 몇 주째 ‘결과 파싱 실패’ 로 흘려보내던 칸이 다음 실행에서 곧바로 크래시로 떴다. 그 신호를 근거로 코어를 확보해 분석했더니 병렬 처리 종료 경로에서 자원을 두 번 닫는 결함이 나왔다. 마스킹을 걷어내는 작업이 곧 결함 하나를 발굴하는 작업이었던 셈이다.

표준 에러 쪽도 같은 원리로 고쳤다. 표준 에러를 버리지 않고 보존하되, 결과가 0건이면 불일치로 집계하는 대신 로드 오류로 분기해 원격의 표준 에러를 그대로 노출하게 했다. 그 즉시 화면에 의존 라이브러리 이름이 찍혔고, 그 파일을 배치하자 같은 대상이 160개 벡터 전건 통과로 돌아섰다. 알고리즘은 처음부터 멀쩡했다.

이 부류를 잡는 검토 항목은 이렇다.

  • 파이프·서브셸·래퍼를 건너 실행체 자신의 종료코드가 살아남는가. 셸이라면 set -o pipefail 이나 PIPESTATUS 가 최소선이고, 더 나은 방법은 원출력을 파일로 받아 실행과 파싱을 분리하는 것이다.
  • 신호로 죽은 경우가 별개 상태로 보고되는가. 종료코드 139 를 그냥 ‘실패’ 로 뭉치면 크래시와 단순 오류가 같은 칸에 들어간다. 크래시는 다른 라벨을 받아야 한다.
  • 표준 에러가 어디로 가는가. 결과가 0건일 때 표준 에러를 그대로 노출하는 분기가 있는지 확인한다.

그리고 판별 규칙 두 개를 챙겨 두면 조사 시간이 크게 줄어든다. 결과가 ‘파싱 실패’ 나 ‘빈 결과’ 로 보고되면 파서를 의심하기 전에 실행체가 살아서 끝났는지부터 본다. 그것은 크래시의 가면일 때가 많다. 그리고 테스트가 균일하게 전건 실패하면 로직이 아니라 로드나 접속을 의심한다. 진짜 로직 버그는 그렇게 가지런히 전부 틀리지 않는다.

이 값이 이번 실행에서 나온 값인지 본다

글 첫머리의 사고가 여기 속한다. 이번 실행이 만들지 않은 파일을 리포트가 읽는 순간, ‘실행하지 않음’ 과 ‘성공’ 은 구분 불가능해진다. 누적되는 결과 디렉터리는 그 자체가 마스킹 장치다.

이 함정이 특히 고약한 것은 실패한 단계와 짝을 이룬다는 점이다. 러너가 단계를 건너뛰면 그 칸은 비어야 하는데, 리포트는 빈칸을 싫어한다. 그래서 어딘가에서 값을 찾아 오고, 가장 가까운 곳에 지난번 값이 놓여 있다. 실패한 단계일수록 잔재로 덮일 확률이 높다는 뜻이다.

교정 자체는 한 줄이었다. 러너가 시작할 때 자기가 이번 실행에서 쓸 산출물을 먼저 지우게 했다. 그런데 그 한 줄의 효과가 컸다. 다음 실행에서 그 칸은 만점 대신 빈칸으로 나왔고, “동기화는 실패인데 암복호는 만점” 이라는 모순이 사라지자 남은 것은 순수한 동기화 실패 하나였다. 그때부터 조사가 직진했다.

그 뒤가 이 절에서 가장 하고 싶은 이야기다. 마스킹을 걷어내고 보니 그 아래에 진짜 제품 결함이 있었다. 동기화 스윕이 대상을 훑기 시작한 시각이 아니라 끝낸 시각을 워터마크로 기록하고 있었고, 그래서 스윕이 도는 중에 발생한 변경은 이미 지나간 대상에 속하면 다음 후보 조회에서 영구히 빠졌다. 확증은 단순한 대조 실험으로 했다. 13분째 멈춰 있던 수렴이, 원본 쪽에 아무것도 바꾸지 않는 갱신을 한 번 날리자 4초 만에 완료됐다. 데이터는 내내 그 자리에 있었고 워터마크 산술만이 막고 있었다는 뜻이다. 제품 수정은 워터마크를 스윕 시작 시각으로 옮기는 것이었다.

이 결함은 몇 주 동안 리포트에 떠 있었지만, 옆 칸의 만점이 그림을 흐려 아무도 그 방향을 파지 않았다. 잔재 파일 한 줄을 지운 것이 결국 제품 수정 하나로 이어졌다.

  • 러너가 시작할 때 자기가 쓸 산출물을 지우는가.
  • 리포트가 읽는 파일에 실행 식별자나 타임스탬프가 박혀 있는가. 이번 실행의 것이 아니면 값을 채우는 대신 비워야 한다.
  • 건너뛴 단계가 ‘성공’ 이 아니라 ‘건너뜀’ 으로 표시되는가. 이 둘을 같은 칸에 넣으면 정보가 사라진다.

일반적인 CI 로 옮기면 대응물이 바로 보인다. 이전 실행의 캐시나 아티팩트를 참조하는 리포트 단계, 커버리지 파일을 누적 병합하는 설정, 테스트가 실패해도 살아남는 이전 빌드 산출물이 전부 같은 구조다. 지금 초록불이 이번 커밋의 초록불인지 물어보면 답이 갈리는 경우가 생각보다 많다.

이진 판정이 무엇을 삼키는지 본다

지금까지는 판정 로직이 틀린 경우였다. 더 알아보기 어려운 것은 판정 로직이 설계대로 정확히 동작하는데, 그 설계 자체가 마스킹 장치인 경우다.

혼합 버전 상태를 진단 목적으로 한 번 돌리는 단계가 있었다. 구버전 노드가 섞여 있으니 일부 실패는 예상된 현상이고, 그래서 런북은 이 단계의 실패를 ‘알려진 일시 현상’ 으로 자동 분류하고 총판정에서 제외한다. 합리적인 설계다.

그날의 실측은 테스트 벡터 0/160 이었고, 런북은 예정대로 통과시켰다. 문제는 직전 두 번의 실측이 155/160, 157/160 이었다는 데 있다. 몇 개가 아직 전파되지 않은 상태와 한 개도 풀지 못하는 상태는 질적으로 다른 현상이다. 그런데 판정에는 성공과 실패 두 값밖에 없었고, 실패에는 고정된 라벨이 하나 붙었다. 그 순간 이탈의 크기가 사라진다. 과거 리포트를 꺼내 숫자를 손으로 대조하지 않았다면 회귀 신호는 초록불 안에 묻혔을 것이다.

여기서 정리해 둘 표현이 있다. ‘예상된 실패’ 는 판정이 아니라 판정 유예다. 그리고 유예 구간은 시간이 지나면 아무도 들여다보지 않는다.

사람에게 이런 라벨은 맥락이다. “이건 원래 그래” 라고 말해도 사람은 그 ‘원래’ 의 범위를 어렴풋이 기억하고, 어느 날 숫자가 크게 벗어나면 눈치챈다. 에이전트에게 같은 라벨은 영구 면죄부다. 분류 규칙에 걸리는 한 무한히 통과시키고, 이번 값이 지난번과 얼마나 다른지는 애초에 비교 대상이 아니다.

그래서 판정 규칙에서 값을 하나 늘렸다. 성공과 실패 둘이 아니라, 허용 범위 안의 실패와 범위를 벗어난 실패를 갈랐다.

before   sync test failed            → RED → classified as known transient → pass
after    sync test failed, 155/160   → RED (in-band, 150~160)  → pass
         sync test failed, 0/160     → RED (out-of-band)       → block

핵심은 라벨을 예쁘게 나눈 데 있지 않고, 분류의 근거가 되는 범위를 문서의 표에서 러너 안으로 옮긴 데 있다. 예상 범위가 런북 문서에만 적혀 있으면 아무도 대조하지 않는다. 러너가 매 실행 대조하면 범위를 벗어나는 순간 다른 색이 뜬다. 함께 넣은 것이 하나 더 있다. 허용해서 통과시킨 실패도 값 자체는 기록하게 했다. 이번에 몇 개가 통과했는지가 남지 않으면 추세를 볼 방법이 없고, 추세가 없으면 ‘알려진 실패’ 는 영원히 알려진 채로 남는다.

  • 예외에 수치 범위가 붙어 있는가. ‘이 단계의 실패는 허용’ 이 아니라 ‘150 이상 160 미만이면 허용, 벗어나면 다른 라벨’ 이어야 한다.
  • 그 범위를 사람이 아니라 러너가 대조하는가. 기대값이 문서의 표에만 있으면 아무 일도 일어나지 않는다.
  • 허용된 실패의 추세가 남는가. 매번 통과시키더라도 값 자체는 기록해서, 변화가 보이게 해야 한다.

CI 에서 같은 자리에 서 있는 것들은 익숙하다. 재시도로 감싼 불안정한 테스트, continue-on-error: true, 명령 끝의 || true, 그리고 ‘알려진 실패’ 목록이다. 전부 실패를 계속 진행시키는 장치이지 실패를 없애는 장치가 아닌데, 시간이 지나면 후자처럼 취급된다. 목록에 언제 들어갔고 지금도 같은 이유로 실패하는지 대조하지 않는 순간, 그 목록은 판정 유예의 저장소가 된다.

요약과 원본 중 무엇이 1차 권위인지 정한다

앞의 셋은 실패가 성공이나 무해한 잡음으로 뭉개진 경우다. 반대 방향도 있다. 성공이 실패로 뒤집히는 경우다. 나는 이쪽이 훨씬 비싸다는 것을 세 시간을 태우고 배웠다.

원격 노드에 패치를 적용하는 자동화가 어느 날 실패를 뱉었다. 그런데 패치는 실제로 성공한 상태였다. 패처 자신의 로그에는 완료 메시지와 새 릴리즈 번호가 또렷이 찍혀 있었다. 판정을 뒤집은 것은 패치 후 재검증 루틴이었다. 기동 확인에 쓰던 옵션이 그 플랫폼의 명령어에 존재하지 않아 조건문이 조용히 헛통과했고, 서비스가 뜨지 않은 상태에서 버전을 조회하니 캐시된 구버전이 돌아왔다. 도구는 그 값을 근거로 실패를 선언했다.

사고는 그다음에 났다. 나는 그 판정을 사실로 받아들이고 아래 사슬을 따라 내려갔다.

patch failed  →  old binary still live
              →  new schema missing
              →  verification step fails
              →  replication looks incomplete
              →  suspect a timing defect in the product

한 칸씩은 전부 합리적이다. 각 단계는 앞 단계의 결론에서 자연스럽게 도출된다. 그리고 사슬 전체가 첫 칸의 거짓 위에 서 있다. 마지막 칸에 도달했을 때 나는 존재하지 않는 제품 결함을 상정하고 있었고, 그 가설은 다른 팀의 코드 분석으로 반박됐다. 패처 로그를 끝까지 읽었으면 30초에 끝날 일이었다.

이 비대칭이 실무에서 중요하다. 실패가 성공으로 묻히는 마스킹은 비용이 이연된다. 언젠가 다른 경로로 드러나고, 그때 대가를 치른다. 반대로 성공이 실패로 뒤집히는 마스킹은 비용이 즉시 청구된다. 없는 버그를 파는 데 사람이 곧바로 투입되기 때문이다. 그리고 이 방향은 에이전트와 특히 나쁘게 결합한다. 잘못된 관측 하나를 주면 정합적인 서사를 끝없이 지어낸다.

교정의 방향은 값을 고치는 것이 아니라 권위를 옮기는 것이었다. 판정의 1차 근거를 패처 자신의 출력 — 완료 문구와 마지막으로 찍힌 릴리즈 번호 — 으로 바꾸고, 사후 버전 조회는 보조 교차확인으로 강등했다. 조회 결과가 1차 근거와 어긋나면 실패를 선언하는 대신 두 값을 나란히 찍고 사람에게 넘긴다. 곁가지 두 개도 같이 손봤다. 서비스 기동을 세션에서 분리해 원격 접속이 매달리지 않게 했고, 기동 확인 폴링 시간이 짧아 정상 수렴을 시간 초과로 오판하던 것을 넉넉히 늘렸다.

효과는 다음 전수검증에서 확인됐다. 여섯 대를 32분에 걸쳐 설치·패치·동기화까지 돌렸고, 전 호스트가 테스트 벡터 전건 통과에 노드 버전도 전부 일치했다. 오판은 한 건도 나오지 않았다. 이전 회차에서 세 시간을 태운 것과 대비하면, 고친 것은 코드 몇 줄인데 회수한 것은 하루치 작업이었다.

  • 판정의 1차 권위를 실행 주체 자신의 출력에 둔다. 사후 조회로 상태를 재구성하는 방식은 보조 교차확인으로 강등한다. 조회는 실행 시점이 아니라 조회 시점의 상태를 보여주며, 그 둘은 자주 다르다.
  • 도구의 요약과 원본 로그가 어긋나면 원본을 믿는다. 요약은 원본을 해석한 결과물이고, 해석은 틀릴 수 있다.
  • 하위 증상을 해석하기 전에 상위 사실을 실측으로 못박는다. “작업이 진짜 됐는가” 를 먼저 확정하고 나서 “그래서 무엇이 이상한가” 로 내려간다.

덧붙이면, 이 사고의 뿌리에는 이식성 문제가 있었다. 나는 자동화 스크립트의 상당 부분을 에이전트와 함께 쓰는데, 에이전트가 내놓는 셸은 압도적으로 GNU 관용구다. 학습 데이터가 그렇게 생겼으니 당연하다. 그런데 그 관용구들이 오래된 유닉스에는 존재하지 않고, 없는 옵션을 만난 명령어들은 대체로 조용히 틀린다. 조건문이 헛통과하고, 멀쩡한 파일이 없는 것으로 보고되고, 대기 루프가 자기 명령줄을 매칭해 영원히 끝나지 않는다. 에이전트가 짠 검증 스크립트를 이질적인 환경에 배치할 때 나는 이제 로직보다 이식성을 먼저 본다. 로직이 틀리면 결과가 깨지지만, 이식성이 틀리면 결과가 조용히 예뻐진다.

하니스가 실환경에 무엇을 할 수 있는지 본다

마지막은 판정층이 신호를 왜곡하는 정도를 넘어, 신호 자체를 만들어낸 경우다.

다른 팀이 쓰던 검증 환경에서 키 테이블 일부가 사라졌다는 제보가 올라왔다. 암호 제품에서 키 유실은 가장 무거운 축의 사고다. 나는 동일 구성을 로컬에 재현해 통제 실험을 돌렸다. 동기화 대상 테이블 전체의 행 수를 주기적으로 스냅샷하면서 부하를 1000회 반복했고, 유실은 0건이었다. 제품 결함이라는 근거는 나오지 않았다.

진범은 우리 쪽 E2E 스위트였다. 그 스위트는 시작할 때 데이터를 초기화하는 서버 측 경로를 호출하는데, 실행 대상이 실제 쓰이던 인스턴스로 향해 있었다. 확정은 목록의 일치로 이루어졌다. 사라진 테이블이 초기화 코드가 지우는 목록과 정확히 일치했고, 그 코드가 명시적으로 제외하는 것들은 살아남았다.

여기서 더 중요한 문제가 드러났다. 키 테이블이 통째로 삭제됐는데 그 사실을 알려주는 감사 기록이 어느 쪽에도 없었다. 그래서 무슨 일이 있었는지 증명하려고 부수적인 로그의 타임스탬프를 짜맞추는, 사실상 법의학에 가까운 작업을 해야 했다. 가장 민감한 자산이 사라졌는데 그 사건을 기록하는 장치가 없었다는 것은 이번 사고와 별개로 제기해야 할 사각지대였다.

조치는 두 갈래였다. 감사 기록의 공백은 별건으로 올렸고, 하니스 쪽은 서버가 노출하던 데이터 초기화 경로를 제거했다. 초기화는 이제 호스트 허용목록을 거치는 외부 스크립트로만 가능하다.

이 형태가 중요한 이유는 방어의 위치가 달라졌기 때문이다. 전에는 테스트가 대상을 올바르게 지정하기만 하면 안전했고, 그 전제가 깨지는 순간 막을 것이 없었다. 지금은 대상을 잘못 지정해도 그 경로 자체가 제품에 없다. 테스트 설정에 의존하던 안전을 배치 구조로 옮긴 것이다. 에이전트에게 실행을 맡길수록 이 차이가 커진다. 설정으로만 막힌 파괴적 경로는 언젠가 잘못된 설정을 만나는데, 에이전트는 설정을 사람보다 훨씬 자주 새로 쓴다.

  • 테스트 편의를 위해 제품 안에 심어 둔 파괴적 경로가 있는가. 그 경로가 존재하는 한 언젠가 잘못된 대상을 향한다. 초기화·시드·리셋은 제품이 아니라 외부 도구가 갖는다.
  • 하니스의 대상 지정이 안전한 쪽으로 실패하는가. 대상이 명시되지 않았을 때 기본값이 무엇인지, 그 기본값이 운영 인스턴스를 가리킬 여지가 있는지 확인한다.
  • 파괴적 조작이 감사 기록에 남는가. 남지 않으면 사고 후에 원인을 확정할 방법이 없고, 조사 자체가 추론이 된다.

이 사례를 마지막에 둔 이유는 규모 때문이 아니다. 조사하는 며칠 동안 우리는 검증 장치를 의심 대상에 넣지 않았다. 검증 도구는 관찰자로 전제되지 관찰 대상으로 전제되지 않기 때문이다. 에이전트에게 실행 권한을 넓게 주는 방향으로 갈수록, 이 전제를 한 번씩 깨 볼 필요가 있다.

고친 것을 규칙으로 남긴다

다섯 건을 고치고 나서 남은 문제는 여섯 번째였다. 개별 수정은 그 자리만 막는다. 다음 스크립트, 다음 러너, 다음 리포트에서 같은 종류가 다시 나온다는 데 걸 수 있었다. 마스킹은 하니스가 자라면서 자연히 생기기 때문이다. 실행 시간을 줄이려고 산출물을 재사용하고, 노이즈를 줄이려고 예외를 분류하고, 편의를 위해 결과를 요약한다. 각 결정은 저마다 합리적인데, 합쳐지면 신호가 깎인다.

그래서 두 가지를 남겼다.

첫째, 사고에서 뽑은 철칙을 문서로 쓰고 그 문서를 에이전트의 시작 컨텍스트로 승격했다. 저장소의 에이전트 규칙 파일에 두 줄을 넣어, 세션이 열릴 때마다 자동으로 적용되게 했다. 하나는 도구가 낸 요약 판정을 원본 로그로 교차확인하라는 것이고, 다른 하나는 이질적인 환경으로 나가는 명령에는 확장 옵션을 쓰지 말라는 것이다. 사람의 기억에 맡긴 교훈은 두 달이면 사라진다. 에이전트에게 넘길 때는 사정이 다르다. 시작 컨텍스트에 있는 규칙은 잊지 않고 매번 적용되고, 잊지 않는 것이 에이전트의 몇 안 되는 확실한 장점이다. 회고의 결과물을 어디에 두느냐가 그 회고의 수명을 정한다.

둘째, 판정층을 거치지 않고 원시 상태를 볼 수 있는 창을 따로 만들었다. 판정도 요약도 하지 않고, 키의 시그니처와 동기화 워터마크, 노드 상태, 로그 꼬리, 프로세스 목록을 한 번에 찍기만 하는 읽기 전용 스크립트다. 리포트가 이상할 때 이것부터 돌린다. 이런 도구가 없으면 판정층을 의심할 때마다 명령을 손으로 짜맞춰야 하고, 그 과정이 번거로우면 결국 요약을 믿게 된다. 의심하는 비용이 믿는 비용보다 비싸면 사람은 믿는 쪽을 고른다.

정리하면 접근이 바뀌었다. 마스킹을 없애겠다는 목표는 현실적이지 않다. 대신 마스킹이 생겼을 때 발견하는 비용을 낮추는 쪽으로 붙었다. 실행과 판정을 분리하고, 산출물의 수명을 실행 하나로 묶고, 예외에 수치를 붙이고, 1차 권위를 명시하고, 원시 상태를 보는 창을 열어 둔다. 다섯 가지 다 같은 성격의 조치다. 판정이 틀렸을 때 그것이 티가 나게 만든다.

마무리

앞선 글에서 도구층 충실도를 정리하며 던진 질문은 이것이었다. 에이전트가 사람과 같은 경로를 지나는가. 지난 한 달은 거기에 질문 하나를 덧붙였다. 에이전트가 사람과 같은 것을 보는가.

두 질문은 짝을 이룬다. 도구층이 어긋나면 에이전트의 행동이 사람의 행동과 달라지고, 판정층이 어긋나면 에이전트의 관찰이 사람의 관찰과 달라진다. 앞쪽이 무너지면 엉뚱한 일을 하고, 뒤쪽이 무너지면 제대로 한 일을 잘못했다고 믿거나 잘못한 일을 제대로 했다고 믿는다. 검증을 에이전트에게 위임한다는 것은 결국 판정층을 신뢰한다는 뜻인데, 나는 그 층을 검수 대상으로 삼은 적이 없었다.

한 달치 작업의 결과를 숫자로 적으면 이렇다. 고친 것은 러너와 스크립트 몇 곳이고, 그 부산물로 제품 결함 두 건이 드러났다. 병렬 처리 종료 경로의 자원 이중 해제와 동기화 워터마크의 유실 창이다. 둘 다 새로 생긴 결함이 아니라 리포트에 몇 주째 떠 있던 것이었고, 다만 마스킹에 가려 아무도 그 방향을 파지 않았을 뿐이다. 판정층을 손보는 일은 검증의 위생 관리처럼 보이지만, 실제로는 그동안 못 보던 결함을 한꺼번에 꺼내는 작업이었다.

그래서 요즘 검증 결과를 볼 때 첫 질문이 바뀌었다. 예전에는 이 결과가 무엇을 의미하는지부터 물었다. 지금은 이 값이 이번 실행에서 생산된 값이 맞는지부터 확인한다. 앞뒤가 맞지 않는 리포트를 만나면 데이터를 파고들기 전에 데이터를 만든 장치를 먼저 의심한다. 순서를 바꾼 것뿐인데, 그 사이에 있던 세 시간짜리 유령 사냥이 사라졌다.

마지막 수정 일자

댓글 0