분산 시스템을 안정적으로 만드는 일이 어려운 이유 중 하나는, 개발자가 알고 있으면서도 무심코 의존하게 되는 전제들 때문이다. 피터 도이치가 정리한 '분산 컴퓨팅의 여덟 가지 오류'에는 '네트워크는 신뢰할 수 있다', '지연 시간은 0이다' 같은 항목이 들어 있다. 실제로는 메시지가 지연되고, 쓰기 작업이 실패하며, 노드가 재시작한다. 문제는 이런 사건들이 특정 순서로 겹칠 때만 드러나는 버그가 있다는 점이다. 같은 테스트를 다시 돌려도 사건 순서가 달라지면 같은 실패를 재현하기 어렵고, 원인 분석과 수정 검증 모두 막힌다.
자체 머신에서 Cloudflare Workers와 Durable Objects 애플리케이션을 구동하는 런타임 celld 팀은 이 문제를 결정론적 시뮬레이션 테스트(DST, Deterministic Simulation Testing)로 다룬다. 핵심 발상은 프로덕션 코드를 그대로 돌리되, 시간과 실행 순서를 시뮬레이터가 통제하는 환경 안에 넣는 것이다. celld의 시뮬레이터는 아직 개발 중이고 공개 저장소에도 포함돼 있지 않지만, 이미 기존에 알려지지 않았던 버그들을 찾아냈다.
순서와 시간을 코드에서 분리하기
celld의 각 셀은 애플리케이션 코드를 실행하며 자체 SQLite 데이터베이스를 가진다. 셀이 작업하는 동안 celld는 들어오는 요청, 완료된 저장소 작업, 타이머 발화 같은 이벤트를 처리한다. 여기서 중요한 설계는 '다음 이벤트를 고르는 코드'와 '이벤트를 처리하는 코드'가 분리돼 있다는 점이다. 덕분에 시뮬레이터는 프로덕션과 동일한 처리 코드를 쓰면서도 이벤트 순서만 바꿔 가며 탐색할 수 있다. 시뮬레이터는 비동기 작업의 실행 시점, 객체 저장소의 응답(지연시키거나 실패시키기), 그리고 시뮬레이션상의 시간 진행까지 제어한다. 실제 시간이 흐르기를 기다리지 않고 시계를 앞당길 수 있어 재시도나 알람 같은 시간 의존적 동작을 다른 이벤트와 교차시켜 검증한다.
재현성의 열쇠는 시드(seed)다. 시드는 난수 생성기의 시작값으로, 코드·설정·시드가 같으면 동일한 이벤트 순서와 중간 상태, 결과가 그대로 재생된다. 시드를 바꾸면 다른 실행을 탐색하고, 실패한 실행은 같은 시드로 다시 돌려 어디서 어긋났는지 추적할 수 있다. 매 행동 뒤에는 체커가 관찰된 응답과 저장된 데이터를 바탕으로 불변식(invariant), 즉 시스템이 실행 내내 지켜야 하는 조건을 검사한다.
알람 재활성화에서 드러난 경쟁 상태
이 방식이 잡아낸 실제 버그는 알람 처리에 숨어 있었다. celld는 유휴 셀을 메모리에서 내렸다가 알람 시각에 맞춰 다시 올릴 수 있는데, 어떤 셀을 언제 깨울지는 객체 저장소에 있는 '웨이크 엔트리'를 훑어 판단한다. 당시 celld는 저장소 쓰기를 줄이려고 웨이크 엔트리를 재활용했다. 예컨대 10:00 알람을 지우고 10:05 알람을 새로 걸면, 기존 10:00 엔트리로 셀을 깨운 뒤 SQLite에서 10:05를 읽어 그 시각까지 기다리는 식이었다.
문제는 정리 작업의 타이밍이었다. 10:00 알람 삭제가 객체 저장소에 반영되면 클라이언트에는 곧바로 성공이 반환되고, 낡은 10:00 웨이크 엔트리는 별도 정리 작업이 나중에 지운다. 클라이언트는 이 정리가 대기 중인 사이에 10:05 알람을 걸 수 있다. 시뮬레이터가 이 지연된 정리 작업을 10:05 알람이 설정된 뒤에 실행하자, 새 알람이 여전히 필요로 하던 10:00 엔트리가 삭제됐다. SQLite에는 10:05 알람이 남아 있는데 셀을 깨울 웨이크 엔트리는 사라진 상태, 즉 불변식 위반이다. 만약 10:05 이전에 노드가 재시작했다면 성공 응답을 받았음에도 알람이 아예 울리지 않았을 것이다. 주목할 점은 아무도 이 순서를 지정하는 테스트를 작성하지 않았다는 사실이다. 시뮬레이터가 요청과 대기 작업을 탐색하다 스스로 찾아냈다.
실무자가 가져갈 교훈
팀은 같은 시드로 실패를 반복 재생하며 낡은 정리 작업이 엔트리를 지우는 바로 그 순간을 관찰했고, 이후 웨이크 엔트리 설계를 바꿨다. 이제 알람을 새로 걸 때마다 고유한 엔트리가 생성돼, 오래된 알람의 지연 삭제는 오래된 엔트리만 지울 수 있다. 그리고 이 실패 실행을 회귀 테스트로 전환했다. 특히 이 테스트는 최종 상태만 보지 않고 실행 도중 내내 불변식을 검사한다. 엔트리가 잠깐 사라졌다가 복원되는 경우, 끝 상태만 확인하면 위반을 놓치기 때문이다.
여기서 실무적으로 눈여겨볼 지점은 세 가지다. 첫째, 순서 선택 로직과 처리 로직을 분리하면 프로덕션 코드를 그대로 쓰면서도 실행 순서를 외부에서 주입할 수 있다는 구조적 교훈이다. 둘째, 시드 기반 재현성은 '간헐적으로만 터지는' 버그를 디버깅 가능한 대상으로 바꿔 놓는다. 셋째, 불변식 검사는 응답 코드가 아니라 시스템이 약속한 보장을 기준으로 삼는다는 점이다. 성공을 반환했다는 사실과 그 성공이 실제로 지켜진다는 사실은 다르다. 다만 이 접근에는 분명한 한계도 있다. 시뮬레이터가 탐색하는 요청과 장애의 범위는 사람이 설정하며, 그 경계 밖의 상호작용은 여전히 드러나지 않는다. celld 팀 역시 노드 재시작과 저장소 장애가 겹치는 조합, 성공으로 보고된 쓰기가 재시작 후에도 살아남는지 같은 보장들을 앞으로 더 검증하겠다고 밝혔다. 결정론적 시뮬레이션 테스트는 만능 보증이 아니라, 재현 불가능하던 실패를 재현 가능하고 수정 검증 가능한 자산으로 바꾸는 도구에 가깝다.