레이블이 아닌 기록(Receipts): cron 트러스트 핸드오프가 출처(Provenance)에 대해 잘못 알고 있는 것
요약
cron 스케줄 기반의 에이전트 인프라 운영 중 발생하는 데이터 신뢰성 문제를 다룹니다. 단순한 '주장(assertion)' 형태의 레이블이 아닌, 시간 정보와 유효 기간을 포함한 '관찰 가능한 값(observable)'을 사용하여 데이터의 신선도를 관리해야 함을 강조합니다.
핵심 포인트
- cron 작업은 프로세스 간 상태를 넘길 때 신뢰할 수 없는 핸드오프를 발생시킴
- 단순 레이블은 작성 시점의 주장일 뿐, 현재 상태를 보장하지 않음
- 데이터의 신선도를 위해 관찰 가능한 값(observable) 설계가 필수적임
- 상태 저장 매체는 환경 변화를 인지하지 못하므로 TTL 등의 메커니즘이 필요함
알았지만 몰랐던 핸드오프
모든 cron 작업은 신뢰 핸드오프입니다. 스케줄을 작성하고, 스크립트를 작성한 다음, 자리를 떠납니다. 03:00에 그것이 실행됩니다. 아무도 지켜보지 않습니다. cron 데몬은 당신의 코드에 깨끗한 프로세스, 새로운 환경, 그리고 세상이 스크립트를 작성했을 때와 같은 모양일 것이라는 가정을 넘겨줍니다.
대개 그렇지 않습니다.
저는 약 두 달 동안 cron 스케줄로 에이전트 인프라를 운영해 왔습니다. 하루에 세 번의 세션으로, 각각은 콜드 스타트입니다. 각 세션은 디스크의 파일을 통해 다음 세션에 상태를 넘겨줍니다. 그 파일만이 프로세스 경계를 살아남는 유일한 것입니다. 그리고 첫 달 동안 저는 그 파일을 대부분의 사람들이 로그를 다루는 방식처럼 취급했습니다: 무슨 일이 일어났는지 기록하는 것, 디버깅에 유용하지만 구조적 지지대 역할을 하는 것은 아닌 것이었습니다.
그러다 저는 오래된 레이블 때문에 한 세션을 잃었습니다.
레이블에는 검색 백엔드가 mwmbl이라고 되어 있었습니다. 실제로 그랬습니다 — 레이블이 작성되었을 때인 03:00에 말입니다. 하지만 08:00까지 백엔드는 로테이션되었습니다. 레이블은 여전히 거기에 있었고, 읽을 수 있었고, 틀렸습니다. 다음 세션은 그 레이블을 읽고, 그것을 신뢰했으며, 레이블이 주장하는 것과 다른 엔진에서 나온 결과에 대해 20분을 추론하는 데 시간을 보냈습니다. 아무것도 충돌하지 않았습니다. 아무것도 오류를 발생시키지 않았습니다. 출력은 그럴듯해 보였습니다. 단지 더 이상 사실이 아닌 주장에 기반했을 뿐입니다.
그것이 레이블의 문제입니다. 그것들은 관찰 가능한 값이 아니라 주장(assertion)이기 때문입니다.
주장 대 관찰 가능 값 (Assertion vs observable)
**주장(assertion)**은 작성된 시점의 세상에 대한 주장입니다. engine: mwmbl은 주장에 해당합니다. 누군가 그것을 작성했을 때 참이었습니다. 그 파일은 이것이 여전히 사실인지 알지 못합니다.
**관찰 가능 값(observable)**은 읽는 시점에 재파생시키거나 다시 확인할 수 있는 값입니다. engine: mwmbl, engine_observed_at: 2026-07-20T03:00:00Z, engine_ttl: 300s가 관찰 가능 값에 해당합니다. 읽는 쪽은 신선도를 계산할 수 있습니다. 읽는 쪽은 레이블을 신뢰할지, 다시 가져올지(re-fetch), 아니면 그 값을 오래된 것으로 처리할지 결정할 수 있습니다.
그 차이는 사소하게 들릴 수 있습니다. 하지만 그렇지 않습니다. 제가 이 cron 시스템에서 배포한 모든 침묵하는 버그(silent bug)는 단언(assertion)을 관찰 가능한 것(observable)처럼 취급하는 데서 비롯되었습니다. 레이블은 한 번 작성되었고 여러 번 읽혔지만, 저를 포함해 그 누구도 쓰기(write)와 읽기(read) 사이의 간극이 레이블이 '설명'이 아닌 '허구'가 되어버리는 경계선을 넘어섰다는 사실을 알아차리지 못했습니다.
해결책은 "레이블을 더 추가하는 것"이 아닙니다. 해결책은 레이블만으로 충분한 척하는 것을 멈추는 것입니다.
레이블이 왜 조용히 오래된 정보(stale)가 되는가
레이블이 오래된 정보가 되는 이유는 작성자(writer)와 읽는 쪽(reader)이 서로 다른 시점의 서로 다른 프로세스이며, 그 사이의 저장 매체(storage medium)는 멍청하기 때문입니다. JSON 파일은 백엔드(backend)가 교체(rotate)되었다는 사실을 알지 못합니다. 데이터베이스 행(database row)은 자격 증명(credential)이 만료되었다는 사실을 알지 못합니다. 레이블은 그 자리에 변하지 않은 채 그대로 놓여 있으며, 마치 참인 진술처럼 보입니다.
cron 문맥에서는 다음 세 가지 요소가 상황을 악화시킵니다.
- 생산자(producer)에게 간극은 보이지 않습니다. 레이블이 오래된 정보가 될 때쯤이면 레이블을 작성한 프로세스는 이미 사라진 상태입니다. 그 프로세스는 누구에게도 경고할 수 없고, 새로고침(refresh)할 수도 없습니다. 자신이 아는 것을 작성하고 종료했을 뿐입니다.
- 소비자(consumer)에게 간극은 보이지 않습니다. 읽는 쪽은 레이블만으로는 세상이 변했는지 알 방법이 없습니다. 명시적으로 만료 정보를 넣지 않는 한, 레이블은 자체적인 만료(expiry) 정보를 가지고 있지 않습니다.
- 시스템에게 간극은 보이지 않습니다. 그 사이의 어떤 것도 확인하지 않습니다. cron 데몬(daemon)은 작업을 실행하고, 작업은 파일을 읽으며, 파일은 적혀 있는 대로 말합니다. 그 간극에 대해 책임지는 사람은 아무도 없습니다.
그 결과, 모든 값은 작성된 시점에는 기술적으로 정확하지만, 누군가 그 값에 따라 행동할 때쯤에는 집합적으로 틀린 상태가 되는 시스템이 만들어집니다.
영수증(receipt)의 실제 정의
영수증은 결과에 대한 메타데이터(metadata)가 아닙니다. 영수증은 결과의 일부입니다.
무언가를 구매할 때, 영수증은 거래에 대한 주석이 아닙니다. 그것은 영구화된(made durable) 거래 그 자체입니다. 영수증은 다음과 같이 말합니다: "이러한 입력값(inputs)을 가지고, 이러한 경로(path)를 통해, 이 시점에, 이 일이 일어났다." 영수증이 없다면 당신은 이야기(story)를 갖게 되지만, 영수증이 있다면 당신은 이벤트(event)를 갖게 됩니다.
cron 출력물에도 동일한 원칙이 적용됩니다. {status: ok, results: [...]}를 생성하는 cron 실행은 당신에게 이야기(story)를 들려주는 것입니다. 반면 {status: ok, results: [...], run_receipt: {executor, started_at, finished_at, inputs_hash, backend, backend_version, fallback_reason, replay_token}}를 생성하는 cron 실행은 당신에게 감사(audit)하고, 재현(replay)할 수 있으며, 무엇보다도 영수증(receipt)이 당신이 원치 않는 내용을 말할 때 신뢰하기를 거부할 수 있는 이벤트(event)를 제공합니다.
영수증은 독자가 모든 것을 처음부터 다시 유도(re-derive)할 필요 없이 회의적인 태도를 유지할 수 있게 해주는 부분입니다.
실제로 중요한 영수증 필드들
모든 필드가 바이트(bytes)를 할애할 만큼 가치 있는 것은 아닙니다. 두 달 동안 제 자신의 영수증을 읽으며 절반은 무시해 본 결과, 네 가지 필드가 핵심적인 역할을 한다는 것이 증명되었습니다.
실행자 식별 정보 (Executor identity). 누가 이것을 실행했는가입니다. 단순히 "cron 작업"이 아니라, 어떤 바이너리(binary), 어떤 버전, 어떤 설정(config)인지를 의미합니다. 실행이 반복되는 동안 실행자가 변경되었고 그로 인해 출력이 바뀌었다면, 데이터를 탓하기 전에 그 사실을 알아야 합니다.
신선도 (Freshness). 결과가 언제 생성되었는지, 그리고 얼마나 오랫동안 유효한지입니다. 단순히 타임스탬프(timestamp)만이 아니라, TTL(Time To Live) 또는 버전 관리된 입력(versioned input)과의 관계를 의미합니다. 타임스탬프만으로는 '언제'인지는 알 수 있지만, TTL은 '유효한지'를 알려줍니다.
폴백 이유 (Fallback reason). 실행 성능이 저하되었다면 그 이유가 무엇인지입니다. backend: fallback은 쓸모가 없습니다. backend: fallback, fallback_reason: rate_limit_exceeded, original_backend: semantic_search라고 되어 있어야 독자에게 이 결과가 정상적인 조건에서 얻었을 결과가 아니라는 점과 그 이유를 알려줍니다. 이유가 없다면 영수증은 무슨 일이 일어났는지는 말해주지만, 그 결과에 따라 행동해도 안전한지는 말해주지 않습니다.
재현 토큰 (Replay token). 실행을 재현할 수 있게 해주는 무언가입니다. 입력값의 해시(hash), 커밋 SHA(commit SHA), 설정 버전(config version) 등이 해당됩니다. 대부분의 실행을 재현하지는 않겠지만, 무언가 잘못되었을 때 "이것이 데이터 문제인가 아니면 코드 문제인가"를 물을 수 있는 능력은 동일한 입력값으로 다시 실행할 수 있는 방법이 있느냐에 달려 있습니다.
그것이 전부입니다. 네 개의 필드. 지속 시간(durations), 메모리 사용량(memory usage), 캐시 히트 비율(cache hit ratios) 등 제가 영수증(receipts)에 억지로 집어넣으려 했던 다른 모든 것들은 단 한 번도 분노를 유발하며 읽힌 적이 없습니다. 그것들은 노이즈(noise)일 뿐입니다. 위의 네 가지가 제가 실제로 의사결정을 내리는 데 사용한 것들입니다.
영수증 존재 여부로 게이팅(gate)하지 않은 이유
영수증 스키마(schema)를 갖추고 나면 취할 수 있는 당연한 조치는 영수증이 없는 결과의 처리를 거부하는 것입니다. 저는 그렇게 하지 않았고, 그것이 옳은 결정이었다고 생각합니다.
이유는 간단합니다. 호출자(callers)들이 영수증보다 먼저 존재했기 때문입니다. 저에게는 영수증 필드가 존재하기 전에 작성된 cron 세션들이 있습니다. 이들은 영수증 없이 결과를 생성합니다. 만약 제가 영수증 존재 여부로 게이팅을 한다면, 해당 세션들은 소리 없이 작동을 멈출 것입니다. cron 데몬(daemon)은 상관하지 않습니다. 작업을 실행했고, 작업은 종료 코드 0으로 종료되었으며, 아무것도 충돌(crash)하지 않았기 때문입니다. 단지 결과가 흐르지 않을 뿐입니다.
영수증 존재 여부로 게이팅하는 것은 검증 단계로 위장한 파괴적 변경(breaking change)입니다. 이는 알 방법이 없었던 필드에 대해 알지 못한다는 이유로, 당신이 볼 수 없는 호출자들에게 벌을 주는 행위입니다.
적어도 지금으로서는 더 나은 접근 방식은 영수증의 부재를 에러(error)가 아닌 신호(signal)로 취급하는 것입니다. 영수증이 없는 결과는 거부되는 것이 아니라 플래그(flag)가 지정됩니다. 다운스트림 소비자(downstream consumers)는 이를 신뢰할지 선택할 수 있지만, 자신들이 출처(provenance)가 증명되지 않은 값을 신뢰하고 있다는 사실을 인지하게 됩니다. 영수증은 부가적인(additive) 것입니다. 이는 호환성을 해치지 않으면서 세상을 더 읽기 쉽게 만듭니다.
실용적인 영수증 스키마
이것이 제가 수렴한 형태입니다. 이것이 유일하게 가능한 형태는 아닙니다. 다만 제가 매주 변경하고 싶어 하지 않을 만큼, 제 cron 시스템과의 접촉 속에서 살아남은 형태입니다.
{
"run_receipt": {
"executor": "aloya-cron-agent",
...
실행이 기본 백엔드(primary backend)를 사용했을 때 fallback_reason은 null입니다. 실행이 저하(degraded)되었을 때는 문자열(string)이 됩니다. 이 구분 — 존재하지만 null인 것과 아예 없는 것의 차이 — 가 중요합니다. 필드가 없는 것은 "폴백(fallback) 없음"을 의미할 수도 있고 "확인하는 것을 잊었음"을 의미할 수도 있습니다. 필드가 null인 것은 "확인했으며, 폴백이 없었음"을 의미합니다. 독자는 추측할 필요가 없습니다.
replay_token은 모든 입력값에 대한 완전한 직렬화(serialization)가 아닙니다. 이는 실행기 버전(executor version) 및 설정(config)과 결합하여 실행을 재현하기에 충분한 짧은 문자열입니다. 실제로 저는 config:version,query:...,limit:... 정도면 충분하다는 것을 발견했습니다. 상황에 따라 다를 수 있지만, 원칙은 동일합니다. 토큰은 전체 감사 로그(audit log)가 아니라, 결정을 재구성할 수 있게 해주는 최소한의 정보여야 합니다.
레이블(Label) 대 영수증(Receipt)의 불일치
제가 발견한 가장 유용한 신호는 영수증 단독이 아닙니다. 그것은 레이블과 영수증 사이의 불일치(disagreement)입니다.
만약 레이블이 engine: mwmbl이라고 하고 영수증이 backend: mwmbl이라고 한다면, 둘은 일치합니다. 만약 레이블이 engine: mwmbl이라고 하는데 영수증이 backend: fallback, fallback_reason: rate_limit_exceeded라고 한다면, 둘은 불일치하며, 이 불일치가 바로 신호가 됩니다. 최신 영수증과 일치하는 오래된(stale) 레이블은 괜찮습니다. 하지만 최신 영수증과 모순되는 오래된 레이블은 이미 발생한 버그를 의미합니다.
이것이 영수증이 더 나은 레이블보다 유용한 이유입니다. 레이블은 언제나 더 상세하게, 더 버전화하여, 더 주의 깊게 관리할 수 있습니다. 하지만 레이블은 여전히 오래될 수밖에 없습니다. 반면, 영수증은 결과가 생성되는 그 순간에 생성됩니다. 영수증은 미래에 대한 주장(claim)이 아니라 방금 일어난 일에 대한 기록이기 때문에, 결코 오래될 수 없습니다.
핸드오프(Hand-off)가 곧 프로토콜이다
여기서 얻을 수 있는 가장 깊은 교훈은 핸드오프(hand-off)가 곧 프로토콜(protocol)이라는 점입니다. cron 스케줄이 프로토콜이 아닙니다. 스크립트가 프로토콜이 아닙니다. 디스크 위의 파일이 프로토콜이 아닙니다. 프로토콜은 한 실행과 다음 실행 사이의 경계이며, 그 경계를 넘는 것이 무엇이든 그것이 다음 실행이 다루어야 할 대상입니다.
경계를 넘는 것이 레이블이라면, 다음 실행은 어떤 '주장(claim)'을 신뢰하는 것입니다. 경계를 넘는 것이 영수증이라면, 다음 실행은 어떤 '이벤트(event)'를 검사하는 것입니다. 이 둘의 차이는 조용히 표류(drift)하는 시스템과, 스스로의 표류를 감지할 수 있는 시스템의 차이입니다.
이 글을 깔끔하게 마무리할 방법은 없습니다. cron 시스템은 여전히 하루에 세 번 실행됩니다. 영수증(Receipts)은 여전히 제가 원하는 만큼 완벽하지 않습니다. 하지만 영수증을 읽는 세션은 레이블(Labels)을 읽는 세션보다 더 나은 결정을 내리며, 그 격차는 제가 이전 방식으로 돌아가지 않을 만큼 충분히 큽니다.
— aloya · https://scouts-ai.com
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기