대표가 현황을 볼 때 완료라는 표시가 많으면 안심하기 쉽다. 그런데 그 완료가 저장을 뜻하는지, 업체에 보냈다는 뜻인지, 현장에서 받았다는 뜻인지 다르면 다음 판단도 달라진다. 나는 ERP 제작 기록에서 반복해서 나타난 이 경계를 작은 독립 예제로 다시 만들었다. 원래 프로그램을 복원하거나 실제 업무에 접속하는 도구는 아니다. 합성 기록에 어떤 근거가 제공됐는지 읽고, 서로 다른 사건을 하나의 성공으로 합치지 않는 방법을 보여 준다.
문서의 현재 버전과 사건의 버전을 함께 본다
예제의 입력에는 문서 식별자와 현재 버전, 근거 목록이 있다. 근거 한 개는 저장·확정·전달·수령 중 하나의 종류와 해당 문서 버전, 중복되지 않는 참조를 가진다. 문서가 두 번째 버전인데 전달 근거가 첫 번째 버전에만 있다면 이전 버전 전달로 표시한다. 최신 내용을 보냈다고 자동으로 해석하지 않는다. 수정한 문서가 있다는 사실과 수정 내용을 전달했다는 사실을 구별하기 위한 구조다.
새 근거가 없으면 미확인 상태가 남는다. 저장한 근거 하나로 나머지 단계의 근거를 만들어 내지 않으며, 수령 근거가 있다고 앞의 모든 단계를 자동으로 완료시키지도 않는다. 이 예제는 업무 순서를 강제하는 상태 기계가 아니라 명시된 사실을 읽는 도구다. 실제 서비스에서 가능한 전이와 권한은 별도로 정해야 한다. 보드는 그 계약을 대신하는 것이 아니라 서로 다른 의미가 섞이지 않았는지 살펴보는 출발점이다.
같은 근거를 두 번 받아도 두 사건으로 늘리지 않는다
네트워크 재시도나 파일 합치기를 거치면 같은 참조가 반복해서 들어올 수 있다. 종류와 버전까지 같은 참조는 한 번만 읽는다. 반대로 같은 참조가 다른 버전이나 다른 사건을 가리키면 모순으로 보고 거부한다. 조용히 마지막 값을 채택하면 처음 기록과 나중 기록 중 어느 쪽을 믿었는지 알 수 없다. 작은 예제에서도 이런 충돌을 보이는 편이 실제 연결을 설계할 때 도움이 된다.
현재 문서보다 미래 버전을 가리키는 근거나 알 수 없는 종류도 거부한다. 버전은 안전한 양의 정수여야 하고 빈 참조는 근거로 인정하지 않는다. 이 검사는 제공된 값의 형식을 살필 뿐, 참조가 진짜 서버 로그나 실제 수령 서명인지 확인하지는 않는다. 원래 업무의 신뢰할 수 있는 사건 저장소에서 확인된 값만 가져오도록 연결하는 책임은 사용하는 프로그램에 있다.
코드와 구현 살펴보기
독립 예제 핵심 코드
/** Read-only interpretation of explicitly supplied evidence. No network or persistence. */
export function inspect(record) {
if (!record || typeof record !== 'object' || Array.isArray(record)) throw Error('record');
const {id, version, evidence} = record;
if (typeof id !== 'string' || !id.trim() || !Number.isSafeInteger(version) || version < 1 || !Array.isArray(evidence)) throw Error('identity/version/evidence');
const kinds = ['saved', 'confirmed', 'sent', 'received'];
const seen = new Map();
const unique = [];
for (const item of evidence) {
if (!item || !kinds.includes(item.kind) || typeof item.ref !== 'string' || !item.ref.trim() || !Number.isSafeInteger(item.version) || item.version < 1 || item.version > version) throw Error('invalid evidence');
const previous = seen.get(item.ref);
if (previous && (previous.kind !== item.kind || previous.version !== item.version)) throw Error('conflicting evidence reference');
if (!previous) { seen.set(item.ref, item); unique.push(item); }
}
const facts = kinds.map(kind => {
const refs = unique.filter(e => e.kind === kind);
const current = refs.filter(e => e.version === version);
return {kind, state: current.length ? 'current' : refs.length ? 'earlier-version' : 'no-evidence', references: refs.map(e => e.ref)};
});
return {id, version, facts, scope: 'Provided evidence only; no business event, payment or external delivery verified.'};
}
화면과 CLI는 같은 읽기 함수를 사용한다
아래 예제는 합성 입력을 읽고 각 종류에 현재 버전 근거, 이전 버전 근거, 근거 없음 중 하나를 표시한다. 입력을 바꿔 읽어도 서버에 저장하거나 문서를 확정·발송하지 않는다. 다운로드에 들어 있는 CLI 역시 같은 함수를 불러 JSON 결과를 출력한다. 새로운 모양의 화면을 만든다고 업무 판단까지 다른 코드로 복제하지 않으려는 의도를 작은 형태로 담았다.
파일을 내려받은 뒤 예제 폴더에서 node cli.mjs example.json으로 결과를 확인할 수 있다. 함께 제공한 검사 파일은 같은 참조의 중복, 모순된 참조, 이전 버전 전달, 미래 버전, 입력 불변과 잘못된 버전을 다룬다. 합성 사례를 사용한 Node 검사 열두 개가 통과했다. HTML에 코드가 들어갔다는 사실과 실제 브라우저에서 모든 조작을 시험했다는 사실은 다르며, 이번 발행에서는 후자를 주장하지 않는다.
실제 업무에 붙일 때 바꿔야 할 경계
대표용 화면에서는 내부 참조 전체를 그대로 보여 줄 필요가 없다. 권한에 맞는 설명과 확인 시각을 보여 주고, 필요한 사람만 원래 근거로 이동하게 할 수 있다. 직원에게는 어떤 버전의 무엇을 확인해야 하는지 다음 행동을 연결할 수 있다. 다만 이 예제 자체가 실제 배송, 결제, 문서 전송을 확인하는 것은 아니다. 화면에서 현재 버전이라고 표시됐다고 외부 업무까지 완료됐다고 해석하면 안 된다.
프로젝트에 적용한다면 근거 생성 주체, 버전 증가 규칙, 정정 방법과 읽기 권한부터 정한다. 저장 중 끊긴 요청을 전달 완료로 바꾸지 않고, 이전 버전의 발송 기록을 최신 버전의 기록으로 고치지 않는 사례를 준비하면 좋다. 제공 파일에는 원래 회사의 코드·장부·계정·주소가 없다. 공개한 것은 완성된 업무 엔진이 아니라 기록의 의미를 분리하고 검토할 수 있는 작은 구현이며, 사용하는 서비스의 검증과 보존 규칙을 추가해 확장해야 한다.