안전하게 멈춘 GC는 일을 끝낸 것일까
과거 상태를 증명하려면 그때의 자료를 보관해야 한다. 그렇다고 모든 검증 자료를 계속 쌓아두면, 원장이 처리하는 거래가 늘어날수록 저장소를 유지하는 부담도 커진다. 보존할 범위를 정했다면, 이제 필요 없어진 자료를 정리하는 일도 필요하다.
과거 상태와 증명에서는 자료를 제공할 수 없다는 답과 그때 기록이 없었다는 답을 구분했다. 이번에는 그 자료를 지우는 쪽에서 묻는다. 지우면 안 되는 자료를 보호했다면, 정리 기능은 제 역할을 다한 것일까?
nigo-protocol의 2026년 9월 18일 로컬 실험에서는 이 질문에 두 가지 답이 나왔다. 거래가 계속 들어오는 동안 삭제 기준이 바뀌자 GC는 안전하게 멈췄다. 하지만 그 부하 구간에서 끝까지 완료한 정리 주기는 없었다. 무엇을 지켜냈고, 무엇을 아직 해내지 못했는지 함께 읽어야 하는 결과다.
오래된 자료가 모두 불필요한 것은 아니다
GC(garbage collection)는 더 이상 필요하지 않은 자료를 찾아 회수하는 작업이다. 여기서 다루는 nigo-protocol의 GC는 상태를 검증하는 머클 트리의 노드를 정리한다. 이때 노드는 네트워크에 참여하는 컴퓨터가 아니라, 트리를 구성하는 저장 자료 한 조각을 뜻한다.
머클 트리는 상태의 내용을 해시로 묶는 구조다. 그 꼭대기의 상태 루트는 특정 시점의 상태를 요약한 값이고, 아래의 자료들은 개별 기록이 그 상태에 속하는지 확인하는 경로를 만든다. 상태가 바뀌어 새 루트가 만들어져도, 바뀌지 않은 부분은 여러 루트가 공유할 수 있다.
높이 10과 높이 20의 두 상태를 보관하고 있다고 하자. 높이는 블록의 순번이며,
각 상태는 그 블록을 처리한 뒤의 결과다. 설명을 위해 두 루트가 가리키는 일부만 그려보자.
X는 높이 10만 사용하는 가지이고, S는 두 상태가 함께 사용하는 가지다.
다른 보존 대상에서는 X를 사용하지 않는다고 가정한다.
이제 높이 10을 보존 대상에서 제외한다고 하자.
보존 관계를 설명하는 축약 그림이다. X와 S는 자산·상태의 원문이 아니라 트리의 가지를 뜻하며, 다른 가지는 생략했다. 보존에서 제외된 높이 10의 자료도 아직 삭제하지 않은 시점이다.
이 경우 X는 다른 보존 루트에서 닿을 수 없으므로 삭제 후보가 된다.
하지만 S는 높이 20을 확인하는 데 여전히 필요하다. 높이 10을 만들 때 저장됐다는 이유로
함께 지우면, 계속 보관하기로 한 높이 20의 증명까지 망가진다.
그래서 삭제 기준은 “언제 만들어졌는가?”가 아니라 지금 보호하는 어느 루트에서도 도달할 수 없는가다. 보존할 루트에서 시작해 필요한 노드들을 표시하고, 어디에서도 쓰지 않는 노드를 후보로 남긴다. 오래된 루트를 보존 대상에서 빼는 판단과, 그 루트에 연결됐던 자료를 실제로 지우는 판단 사이에 이 검사가 들어간다.
범위도 구분해야 한다. 이 GC는 자산과 상태를 담는 기록인 StateCell의 원문이나 블록·거래·처리 결과의 본문을 지우지 않는다. 과거 Cell이 현재 상태에서 소비됐다는 사실만으로 그 원문을 삭제하는 기능도 아니다. 이번 글에서 말하는 회수량은 검증 트리의 노드 수이며, 디스크에서 반환된 바이트 수가 아니다.
삭제 후보를 찾은 뒤에도 기준은 바뀐다
앞의 예제에서 GC가 X를 삭제 후보로 기록했다고 하자. 아직 실제 삭제 전이다.
그사이 높이 21의 블록이 확정돼 새 상태가 저장되면 어떻게 될까?
진단할 때와 비교해 보존할 루트와 저장된 노드의 구성이 달라질 수 있다.
높이 20까지 살펴본 결론을 높이 21에서도 그대로 써도 되는지 다시 확인해야 한다.
보호 조건을 바꾸는 것은 새 블록만이 아니다. nigo-protocol에는 현재 보존하는 특정 상태를 계속 붙잡아 두는 pin이 있고, 진행 중인 조회가 자료를 읽는 동안의 보호도 있다. 이런 보호가 추가되거나 유효기간이 바뀌는 일도 삭제 판단에 반영해야 한다. pin은 이미 보존 대상에서 빠진 상태를 되살리는 기능으로 쓰지 않는다.
새 블록이 생겼다고 X를 비롯한 모든 후보가 실제로 필요해지는 것은 아니다.
다만 이전 진단이 지금의 저장소에 그대로 적용된다는 근거가 달라졌다는 뜻이다.
nigo-protocol은 진단 당시의 보존 기준과 저장소 변경 세대를 함께 기록한다.
삭제 전에 그 기준을 다시 대조하고, 더 이상 유효하지 않으면 작업을 STALE로 판단한다.
오래된 판단이라는 뜻이지, 후보를 그대로 지워도 된다는 뜻은 아니다.
따라서 진단을 완료했다는 결과는 그 자체로 삭제 허가서가 아니다. 잠시 기다리는 유예 시간도 이를 대신할 수 없다. 기다리는 사이 보호 조건이 바뀌었다면, 시간이 지났다는 이유로 삭제를 진행해서는 안 된다.
자동 실행도 같은 삭제 검사를 거친다
nigo-protocol의 자동 GC는 H2와 RocksDB 영속 저장소에서 동작하며 기본 설정은 꺼져 있다. 켜면 별도의 느슨한 삭제 경로를 사용하는 대신, 기존 GC 작업을 주기적으로 만들고 진행한다. 진단부터 삭제, 작업용 임시 자료 정리까지 한 차례를 수행한 뒤 다음 주기를 기다린다.
안쪽에서는 보존할 루트의 자료를 검사하고, 삭제 직전에도 보호 조건을 확인한다. 삭제는 한 번에 전부 처리하지 않고 작은 묶음으로 나누며, 각 묶음의 삭제와 진행 위치 갱신을 원자적으로 반영한다. 원자적이라는 말은 일부만 저장된 중간 결과를 남기지 않는다는 뜻이다. 삭제 전후에는 보존 대상의 트리 연결도 검사한다.
이 과정에서 기준이 바뀌면 오래된 작업을 정리하고, 대기 후 새 기준으로 진단을 시도한다. 무한히 시도하지는 않는다. 연속해서 기준이 어긋나는 횟수에 한도를 두고, 그 한도에 도달하면 자동 정책을 중단해 운영자에게 넘긴다. 합의를 중단하거나 보호 검사를 생략해서 완료 횟수를 올리지는 않는다.
이 설계에는 중요한 구분이 하나 더 있다. 주기를 끝내지 못했어도 일부 삭제는 이미 끝났을 수 있다. 앞선 묶음은 당시 유효한 조건에서 삭제됐지만, 다음 묶음을 처리하기 전에 기준이 바뀔 수 있기 때문이다. 안전하게 커밋된 앞선 삭제를 이후의 중단 때문에 되돌리는 구조는 아니다.
따라서 지표도 같은 뜻으로 읽어서는 안 된다. 삭제한 노드 수는 이미 반영된 개별 삭제를 세고, 완료한 주기 수는 삭제와 해당 작업의 임시 자료 정리까지 끝낸 횟수를 센다. 한쪽은 늘어도 다른 쪽은 늘지 않을 수 있다.
거래가 계속 들어오는 동안에는 끝내지 못했다
이 차이를 실제 부하에서 확인하기 위해, 9월 18일 실험은 H2와 RocksDB를 각각 세 번 실행했다. 매번 별도의 데이터베이스를 만들고 실제 서명 거래를 제출해 처리 결과와 블록 포함을 확인했다. 자동 GC가 켜진 구간에는 조회 요청도 함께 보냈다.
쓰기 방식은 계속 제출하는 경우와, 최대 여덟 건씩 제출·확인한 뒤 2초씩 쉬는 경우로 나눴다. 두 저장소에 세 번씩, 두 가지 쓰기 방식을 적용했으므로 부하가 있는 관측 구간은 총 12개다. 각 구간은 최소 30초와 실제 처리 결과 128개라는 조건을 충족했다. 단일 노드의 로컬 합의 모드에서 진행한 검사이며, 여러 노드가 참여하는 운영 환경의 결과는 아니다.
| 쓰기 방식 | 완료 증가 | 논리 노드 삭제 |
|---|---|---|
| 연속 제출 · 6구간 | 모두 0 | 없음 |
| 간격 제출 · 6구간 | 모두 0 | H2에서만 일부 진행 |
완료는 정리 주기를 끝낸 횟수다. 완료와 삭제의 증가는 첫 확정 거래 묶음부터 마지막 확정 묶음까지의 관측 범위다. 모든 부하 구간의 마지막 상태는 재시도 한도에 도달한 자동 정책 중단이었다.
H2의 간격 제출에서는 각각 7,000개, 17,000개, 8,000개의 논리 노드 삭제가 관측됐다. 그렇더라도 완료 주기 증가는 0이었다. 일부 삭제가 가능했던 순간과 한 차례의 정리를 끝냈다는 결과를 같은 것으로 세지 않은 것이다.
12개 구간 모두 보존 기준 변화로 진단이 낡는 일이 반복됐고,
최종적으로 STALE_RETRY_LIMIT에 도달해 자동 정책이 중단됐다.
다시 계산하는 동안 원장은 다음 상태로 나아갔다.
현재의 보수적인 검사가 안전하게 작업을 거절하는 것과,
지속되는 쓰기 사이에서 정리를 끝내는 것은 별도의 문제로 드러났다.
보고서의 전체 판정은 통과였다. 여기서 통과한 것은 정해진 로컬 부하에서의 안전한 제한 중단, 거래·조회 지연 예산, 그리고 뒤에서 볼 명시적 인계 후의 정리와 보존 검사다. 부하 중 자동 완료를 관측했다는 뜻은 아니다. 안전 조건을 지켜 멈춘 결과는 필요한 증거지만, 계속 쌓이는 자료를 자동으로 처리한다는 증거까지 주지는 않는다.
실험 조건과 비교 범위
실행 장비는 Apple M4, 논리 CPU 10개, RAM 32 GiB의 darwin arm64 환경이었다. Java 21.0.7에서 노드 heap은 2,048 MiB, active processors와 서명 검증 worker는 각각 4, DB 연결은 8이었다. 단일 INSTANT 합의, 병렬 실행 OFF, 수수료 금액 0 조건을 사용했다.
각 DB에는 8개 소유자의 코인을 나누는 확정 거래 1,032개로 초기 자료를 만들었다. 최근 루트 8개를 보존하고, 별도의 보존 pin과 확인 가능한 미사용 트리 노드를 넣었다. GC는 묶음 1,000개, 작업 단계 수 상한 100,000, 실행 예산 120초, 유예 1초, 연속 STALE 한도 8회로 설정했다. 자동 실행 간격은 부하 중 1초, 쓰기 없는 구간에서 10초였다. 이는 실험 설정이며 제품 기본값을 변경한 것이 아니다.
각 DB에서 자동 GC OFF 기준선을 먼저 실행하고, 같은 DB의 누적 이력 위에서 ON을 실행했다. 동시 조회 부하도 ON에만 있었다. 동일한 초기 상태에서 GC만 켜고 끈 비교가 아니므로, 이 결과로 GC 단독 부하 비용이나 두 저장소의 일반적인 성능 우열을 계산하지 않는다. 본문은 당시 결과 기록의 회고이며 새로 수행한 측정이 아니다.
쓰기가 조용해졌다는 것만으로 다시 시작하지는 않았다
부하가 줄어들면 중단됐던 GC가 알아서 이어서 처리했을까? 이번 실험의 뒤쪽 결과를 그렇게 읽으면 중요한 절차가 빠진다.
자동 정책은 재시도 한도에 도달하면 운영자의 확인이 필요한 상태로 남는다. 부하가 줄었다는 이유만으로 이 중단을 해제하지 않는다. 이전에 남은 작업도 재기동한 정책이 임의로 인수하지 않는다.
실험에서는 운영자가 명시적으로 남은 작업을 정리한 뒤 노드를 정상 종료하고 다시 시작했다. 그다음 새 거래를 제출하지 않는 구간에서 새 자동 주기가 완료되는지 확인했다. 중단된 작업이 저절로 이어진 것도, 부하 감소만으로 회복된 것도 아니다.
9월 18일 최종 실험의 순서다. 중간의 운영자 절차를 생략한 무인 회복 경로를 뜻하지 않는다.
여섯 데이터베이스 각각에 미리 넣은, 어느 보존 루트에서도 쓰지 않는 트리 노드 4,096개는 최종 검사에서 모두 사라졌다. 일부는 앞선 단계에서, 나머지는 쓰기 없는 마지막 구간에서 회수됐다. 이 미사용 표본은 앞서 관측한 모든 논리 노드 삭제량과는 다른 집합이다.
정리 뒤에는 현재 Cell 전체와 대응하는 이력 원문, 선택한 과거 상태와 현재 상태의 증명, pin과 보존 범위를 검사했다. 이 검사들은 통과했다. 모든 과거 Cell 버전을 전수 검사한 것은 아니지만, 보호할 자료를 남기면서 정리를 마치는 로컬 경로가 있다는 근거는 얻었다.
결과를 이어 읽으면 분명하다. 이번 조건에서는 안전하게 중단했고, 명시적 인계 뒤 조용한 상태에서는 회수를 끝냈다. 지속 쓰기 중 완료와 무인 회복은 아직 확인하지 못했다.
다음에 확인해야 할 것은 삭제량만이 아니다
남은 문제를 단순히 재시도 한도를 늘리는 것으로 닫을 수는 없다. 계속 바뀌는 원장에서 다시 만든 진단도 같은 이유로 낡는다면, 한도 증가는 정리 완료 없이 기다리는 시간을 늘릴 수도 있다. 반대로 보호 조건 검사를 줄여 완료를 만들면, 처음 지키려던 과거 증명이 위험해진다.
다음 검증에서는 거래가 계속 확정되는 동안에도 운영자의 작업 정리나 재시작 없이 완료 주기와 실제 회수가 반복되는지 봐야 한다. 한 번 성공하는 것에 더해, 새로 쌓이는 정리 대상에 비해 회수가 뒤처지지 않는지도 관측해야 한다. 부하가 내려갔을 때 스스로 다시 진행하는지, 그동안 거래 확정과 조회가 정한 지연 예산을 지키는지도 필요하다.
완료 주기가 늘더라도 물리 디스크 크기가 곧바로 줄어든다고 단정할 수는 없다. 데이터베이스 안에서 논리적으로 지운 공간을 재사용하거나 파일을 재정리하는 과정은 또 다른 층의 일이다. 트리 노드 GC의 진전, Cell·블록 본문의 보관 범위, 실제 디스크 사용량을 각각 확인해야 저장소 전체가 계속 감당 가능한 크기로 유지되는지 판단할 수 있다.
앞의 작은 예제로 돌아가면 첫 번째 책임은 높이 20이 쓰는 S를 지키는 것이다.
두 번째 책임은 더 이상 어느 보존 상태에서도 쓰지 않는 X를 원장이 움직이는 동안에도 회수하는 것이다.
nigo-protocol의 현재 결과는 첫 번째 책임을 위한 중단과, 명시적 인계 뒤의 완료를 보여줬다.
두 번째 책임을 지속되는 부하 속에서 수행하는 일은 다음 설계와 검증에 남아 있다.
그래서 자동 GC가 켜져 있다는 표시만으로는 충분하지 않다. 마지막으로 언제 정리를 끝냈는지, 얼마나 회수했는지, 멈췄다면 무엇을 기다리는지가 함께 보여야 한다. 자료를 지키는 판단과 일을 끝내는 능력을 따로 관측할 때, 안전하게 멈춘 결과를 다음 개선의 출발점으로 쓸 수 있다.