데이터베이스 GUI는 절대 직접 쓰면 안 되는 이유
모든 데이터베이스 GUI에는 클릭 한 번이 프로덕션에 대한 쓰기로 바뀌는 순간이 있습니다. 우리는 그 순간이 존재해서는 안 된다고 결정했습니다. DynoTable에서는 여러분이 하는 어떤 일도 — 항목 편집도, 대량 삭제도, 의 변경도 — DynamoDB에 직접 손대지 않습니다. 모든 쓰기는 검토 가능한 속성별 diff로 에 들어가며, 사람이 커밋할 때에만 전송됩니다.
우리가 지향한 멘탈 모델은 git입니다. 검토를 위한 스테이징 영역, 그리고
git push --force-with-lease처럼 동작하는 커밋 — 원격이 여러분이 마지막으로
본 그대로일 때에만 성공합니다. 이 글은 그 아래에 깔린 엔지니어링입니다.
전체 설계를 새로 빚어낸 탭 간(cross-tab) 버그, 주된 산출물이 삭제된 코드
였던 리팩터링, 그리고 AI 에이전트의 등장이 어떻게 UX상의 사소한 배려를
하중을 지탱하는 안전벽으로 바꿔 놓았는가 하는 이야기입니다.
쓰기는 diff다
편집은 로컬 SQLite 기반 스테이지에 쌓이고, 사이드 패널에 diff 카드 — 이전 값, 새 값, 속성별 — 로 표시됩니다. 그리드의 행은 색조로 물들어, 스테이징 상태가 패널뿐 아니라 데이터에서도 보입니다. 커밋은 스테이징된 집합을 DynamoDB 으로, 서비스 한도(트랜잭션당 100개 항목, 작업별·요청별 바이트 예산)에 맞춰 청크로 나누어, 순차적으로, 청크당 30초 타임아웃과 함께 전송합니다.

스테이징 문서는 일상적인 워크플로를 다룹니다. 거기서 다루지 않는 것은 스테이지가 무엇을 키로 삼는가 입니다 — 그리고 이것이 시스템에서 가장 중대한 결정으로 드러났습니다.
탭이 아니라 테이블을 키로
첫 버전은 각 스테이지를 그것을 만든 탭으로 범위를 한정했습니다. 그 설계는 실제 버그를 낳았습니다. AI의 스테이징 도구는 대상을 활성 탭 으로부터 이끌어냈고, 그 가드는 테이블 뷰가 아닌 탭을 건너뛰었습니다. 그래서 SQL Workbench 탭이 포커스된 상태에서 어시스턴트에게 어떤 행을 고쳐 달라고 요청하면, 그 편집은 Workbench의 스테이지에 스테이징되었고 — "스테이징된 변경 보기" 칩은 엉뚱한 탭을 열었습니다.
더 나쁘게도, 커밋 잠금 역시 탭별이었습니다. 같은 테이블 을 보는 두 탭이 서로 독립적인 잠금 두 개를 쥐었고 — 이는 둘 다 같은 스테이징된 행을 실제 DynamoDB에 동시에 커밋할 수 있음을 뜻했습니다. 프로덕션을 상대로 한 이중 커밋 경합이 데이터 모델에 그대로 박혀 있었던 셈입니다.
해결책은 모든 것을 탭이 아니라 테이블 식별자 —
{profile, region, tableName} — 로 다시 키를 매기는 것이었습니다:
- 테이블당 하나의 스테이지. 그 테이블의 모든 뷰에서 보이고, 탭을 닫았다 다시 열어도 유지됩니다. AI는 테이블 탭이 전혀 열려 있지 않아도 스테이징할 수 있습니다.
- 테이블당 하나의 커밋 잠금. 이중 커밋 경합은 "처리"되는 것이 아니라 — 표현조차 불가능합니다.
- 엉뚱한 탭 버그 부류는 구조적으로 사라졌습니다. 키에 탭이 없으니까요.
그리고 다시 키를 매기는 작업은 추가한 것보다 더 많은 코드를 삭제했습니다. 닫힌 탭의 스테이지를 가비지 컬렉션하던 시작 시점의 정리 작업 말입니다. 한 리뷰어가 그것을 새 모델에서는 기능을 파괴하는 것이라고 지적했고 — 스테이지는 더 이상 탭과 함께 사라지지 않으므로 — 통째로 제거되었습니다. 탭 닫기 시 폐기와 그 확인 대화 상자도 제거되었습니다. 남은 유일한 진짜 고아는 삭제된 연결 프로필이며, 이는 명시적으로 연쇄 삭제됩니다.
마이그레이션 자체는 이 저장소 최초의 행-변경형 마이그레이션이었습니다.
손으로 작성한 SQLite 테이블 재구축으로, 레거시 탭별 행을
ROW_NUMBER() OVER (PARTITION BY …) 윈도(최신 편집이 이김)로 중복 제거하고,
런타임의 키 빌더와 바이트 단위로 동일하게 char(0) 구분자로 새 키를
백필했습니다. 행-변경형 마이그레이션은 그날 자체적인 seed-then-migrate
테스트 하니스를 갖게 되었습니다.
--force-with-lease, DynamoDB를 위한
검토한 것이 실제로 쓰이는 것과 다르다면 검토 UI는 무의미합니다. 스테이징과 커밋 사이에, 다른 누군가가 그 행을 바꿨을 수 있습니다. 그래서 모든 커밋 작업은 쓰기를 여러분이 검토한 바로 그 스냅샷에 고정하는 을 지닙니다 — 속성 하나하나에 대한 입니다:
- 생성 은 항목이 아직 존재하지 않음을 단언합니다.
- 업데이트 는 여러분이 바꾸는 모든 속성이 스테이징할 때 본 값을 여전히 지니고 있음을 단언합니다.
- 제거 조차도 그 속성이 여전히 이전 값과 같음을 단언합니다 — 여러분의
note제거가 스테이징된 채로 있는 동안 팀원이note를"old"에서"new"로 바꿨다면, 커밋이 그들의 편집을 조용히 삭제해서는 안 됩니다.
조건이 실패하면 DynamoDB는 트랜잭션 전체를 취소하고 어떤 항목이 어긋났는지 알려줍니다. 그 행은 어느 방향으로든 조용한 덮어쓰기 대신 충돌 배너를 받습니다 — 실시간 값에 대해 리베이스하거나 중단하거나.
그리고 어떤 청크라도 실패하면 커미터는 중단 합니다. 커밋된 청크는 커밋된 채로 남고, 실패한 청크는 원자적으로 롤백되며, 이후로는 아무것도 시도하지 않습니다. 우리는 최선 노력(best-effort) 방식의 계속 진행을 의도적으로 거부했습니다. 충돌을 지나쳐 계속 쓰는 검토 도구는 아무도 검토하지 않은 변경 집합을 적용하는 것이기 때문입니다.
서브시스템을 삭제한 리팩터링
커밋은 오래 걸리는 작업입니다. 앱은 각 커밋을 리플레이 레지스트리에 등록해서,
렌더러 리로드 — 또는 두 번째 창 — 가 다시 붙어 완료를 지켜볼 수 있게
합니다. 시간이 지나면서 서로 다른 세 코드 경로가 각각 커밋당 두 개 의
옵저버를 붙였고, 하나의 질문을 중심으로 온전한 중재 메커니즘이
자라났습니다. 어떤 옵저버가 커밋 잠금을 해제할 수 있는가? 거기에는 그 나름의
무시무시한 이름(ownsLockOnBeforeRegistration), IPC 순서 경합을 설명하는
주석 블록, 그리고 230줄짜리 토스트 추적기가 있었습니다.
해결책은 단일 소유자였습니다. 커밋당 정확히 한 번만 붙고, 잠금을 배타적으로 소유하고, 진행 상황을 스토어에 투영하고, 그 start 호출이 모든 종료 경로에서 매듭지어짐을 보장하는 — 결코 멈추지 않고, 결코 거부하지 않는 — Commit Session 입니다. 중재 기구와 추적기는 옮겨진 것이 아니라 삭제되었습니다. 스테이징 패널을 몰아붙이는 메모리 벤치마크는 힙의 약 3분의 1을 덜어냈습니다. 그달에 우리가 받은 최고의 코드 리뷰 코멘트: "이 PR은 대부분 빨간색이네요."
그 소유권 모델에서 두 가지 동작이 파생되는데, 우리는 이를 프로덕션에 쓰는 어떤 도구든 갖춰야 할 기본기로 여깁니다. 진행 중인 커밋은 렌더러 리로드를 견디고(세션이 다시 붙어 완료됩니다), 커밋 도중 여러분의 라이선스가 읽기 전용으로 바뀌어도 견딥니다 — 새 커밋은 차단되지만, 이미 진행 중인 쓰기는 끝까지 지켜보며, 절반만 적용된 채로 결코 버려지지 않습니다.
와이어 형식이 일부러 못생긴 이유
스테이징된 값은 친절하게 언마샬링된 JSON이 아니라 원본 DynamoDB 타입 봉투 —
{N: "42"}, {S: "42"} — 로 저장됩니다. 이유는 두 가지입니다. diff는
타입을 인식 해야 합니다. {N: "1"}과 {S: "1"}은 서로 다른 값이며,
언마샬링된 값 위에 만든 기본 키 해시는 둘을 충돌시킬 것입니다. 그리고 왕복은
무손실이어야 합니다. SDK의 언마샬링된 숫자 래퍼는 SQLite로의 JSON 직렬화와
복원을 견디지 못하며, 사용자 입력에 대한 깊은 동등성 비교가 깨집니다. 커밋
경로도 같은 이유로 원본 클라이언트를 사용합니다 — 여러분이 검토한 것이
바이트 단위 그대로 조건이 걸리고 쓰이는 것입니다.
그리고 에이전트가 도착했다
스테이징은 우리의 AI 기능보다 앞서 존재했지만, 그 AI 기능이 출시될 수 있었던 이유이기도 합니다. 어시스턴트의 쓰기 도구는 스테이징합니다. 그 도구 자체의 설명이 모델에게 다음과 같이 — 그대로 — 알려줍니다:
The user reviews + commits from the staging panel —
this tool never writes to DynamoDB directly.커밋 도구는 없습니다. 권한으로 통제되는 것도, 감사되는 것도 아니라 — 부재 합니다. MCP 서버는 같은 경계를 구조적으로 노출합니다. 그 가운데 동의 범위의 이름은 문자 그대로 "read + stage"입니다. 그 범위의 에이전트는 쓰레기를 제안할 수 있지만, 그 폭발 반경은 사람이 읽는 diff 카드 하나입니다. 그리고 커밋은 속성별 낙관적 잠금을 지니므로, 오래된 에이전트 편집조차 동시적인 사람의 편집을 조용히 뭉갤 수 없습니다 — 다른 모든 것과 마찬가지로 충돌로 표면화됩니다.
우리는 에이전트의 쿼리를 신뢰할 수 있게 만드는 일에 대해 쓴 바 있습니다. 스테이징은 그 나머지 절반입니다 — 에이전트의 쓰기 를 지루하게 만드는 일이죠.
여러분의 데이터베이스에 쓰는 어떤 도구에든 요구해야 할 것들
- 의도와 쓰기 사이의 검토 단계 — 확인 대화 상자가 아니라 diff.
- 삭제와 제거를 포함해, 검토된 스냅샷 에 대한 낙관적 동시성 제어 — 결코 마지막 작성자 우선(last-writer-wins)이 아니어야 합니다.
- 충돌을 지나쳐 최선 노력으로 계속하는 대신, 실패 시 중단하는 원자적 배치.
- 절반만 적용된 집합을 남기지 않고 크래시나 리로드를 견디는 쓰기 경로.
- AI의 경우: 에이전트가 가진 유일한 쓰기 프리미티브로서의 스테이징 — 부재하는 능력이 통제되는 능력보다 낫습니다.
스테이징 문서는 워크플로를 보여주고, DynamoDB 데이터 편집하기는 그것이 보호하는 기본기를 다룹니다. 아니면 DynoTable을 다운로드해서, ⌘S로 편집 몇 개를 스테이징하고, 대량 변경이 일어나기 전에 실제로 읽어 볼 수 있는 무언가로 바뀌는 것을 지켜보세요.


