기록을 다루는 시스템을 맡았는데 첫 요구가 이랬다.
기존 기록은 수정하지 않는다. 정정이 필요하면 정정 기록을 남긴다.
처음엔 불편해 보였다. 오타 하나 고치려고 새 행을 만든다니.
Table of contents
Open Table of contents
배경 — 고치지 말라는 요구
이유가 있었는데 언제 무엇이 기록됐는지가 그 자체로 사실이기 때문이다.
3월 10일에 A 로 기록했다가 3월 15일에 B 로 정정했다면 그 사이 5일 동안은 A 로 알고 있었던 것이다. 그 기간의 판단은 A 를 근거로 했다.
UPDATE 로 행을 고쳐 버리면 처음부터 B 였던 것처럼 된다. 5일간의 사실이 사라지고 그 값을 보고 내려진 판단의 근거도 없어진다.
구조가 달라졌다
일반적인 방식은 이렇다.
UPDATE records SET value = 'B' WHERE id = 1;
여기서는 이렇게 했다.
INSERT INTO records (ref_id, value, revision, reason, created_at)
VALUES (1, 'B', 2, '입력 오류 정정', NOW());
원본은 그대로 있고 revision 이 올라간 정정 기록이 추가된다. 조회할 때는 최신 판을 본다.
SELECT * FROM records
WHERE ref_id = ? ORDER BY revision DESC LIMIT 1;
조회가 복잡해졌다
이 구조의 대가는 조회다. 목록을 뽑을 때 각 ref_id 의 최신 판만 가져와야 하는데 그냥 조회하면 모든 판이 다 나온다.
SELECT r.* FROM records r
INNER JOIN (
SELECT ref_id, MAX(revision) AS rev
FROM records GROUP BY ref_id
) m ON m.ref_id = r.ref_id AND m.rev = r.revision;
매번 이것을 쓸 수는 없어서 VIEW 로 만들었고 화면은 뷰를 본다. 이력이 필요한 곳만 원본 테이블을 직접 본다.
다만 뷰라고 다 같은 것은 아니다. 매뉴얼은 병합을 막는 구성이 있으면 임시 테이블을 쓴다고 적고 DISTINCT 나 LIMIT 을 그 예로 든다.
최신 판을 고르는 뷰는 그런 구성이 들어가기 쉬워서 ALGORITHM = MERGE 를 줘도 그대로 안 먹고 경고를 내며 UNDEFINED 로 바뀐다. 감싸는 것으로 화면은 단순해지지만 그 자리에서 TEMPTABLE 이 만들어지는 것은 알고 있어야 했다.
삭제도 기록이었다
삭제 요구가 왔을 때도 같아서 DELETE 로 행을 지우지 않는다.
INSERT INTO records (ref_id, revision, deleted, reason)
VALUES (1, 3, 1, '중복 입력');
deleted 가 선 판을 추가하면 조회 뷰에서 빠지고 이력에는 언제 왜 지웠는지가 남는다. 실제로 지워 버리면 그 자료가 있었다는 사실 자체가 없어진다.
행이 계속 늘어나는 것도 걱정됐다. 정정이 많으면 몇 배가 되니 COUNT(*) 로 실제로 재 봤다.
전체 행 대비 정정 비율: 약 3%
대부분은 한 번 쓰고 안 고쳐서 걱정보다 훨씬 적었다. 그래도 조회 뷰의 인덱스는 신경 써서 (ref_id, revision) 복합 인덱스로 최신 판 조회가 빠르게 되도록 했다.
판단 기준 — 어디에 적용할지
이 방식을 배우고 나서 다른 곳에도 적용했다. 설정 변경은 누가 언제 무엇을 어떻게 바꿨는지가 남고 가격 변경은 그 시점에 얼마였는지를 답할 수 있으며 상태 전이는 어떤 경로로 지금 상태가 됐는지 보인다.
전부 그때 무엇이었나를 물을 수 있는 자료들이라 revision 을 쌓을 값어치가 있다.
다만 모든 테이블을 이렇게 만들면 복잡도가 감당이 안 되므로 기준을 정했다. 과거 값이 필요한지와 누가 왜 바꿨는지가 중요한지와 법적이나 회계적 근거가 되는지 셋이다.
셋 중 하나라도 해당하면 revision 을 쌓고 아니면 UPDATE 로 덮어썼다. 세션 정보와 캐시와 임시 상태 같은 것은 그냥 덮어썼는데 과거를 물을 일이 없는 것들이다.
정리
- 기록을 고치지 않고 판을 쌓으면 그때 무엇이었나에 답할 수 있다
- 덮어쓰면 처음부터 그랬던 것처럼 되고 그 사이가 사라진다
- 그 값을 보고 내려진 판단의 근거가 없어진다
- 삭제도 판을 추가해 언제 왜 지웠는지를 남긴다
- 대가는 조회 복잡도이고
VIEW로 감싸면 화면 쪽은 단순하다 - 병합이 안 되는 뷰는
TEMPTABLE로 도니MERGE를 강제할 수 없다 - 행이 느는 걱정은 실제 정정 비율을 재 보고 판단한다
(ref_id, revision)복합 인덱스로 최신 판 조회를 받친다- 과거 값과 변경 주체와 법적 근거로 대상을 가른다
- 지금 것만 필요한 자료까지 쌓으면 복잡해지기만 한다