CRUD 중에서 시스템을 가장 복잡하게 만드는 것은 UPDATE다. 왜냐하면 상태 전이를 관리해야 하기 때문이다. INSERT는 새로운 사실을 추가하고 DELETE는 사실을 제거할 뿐이지만, UPDATE는 이미 있는 사실을 덮어쓴다. 덮어쓰기에는 반드시 규칙이 따라붙는다. 어떤 상태에서 어떤 상태로 바뀔 수 있는지, 어느 업무가 어느 컬럼을 갱신해도 되는지, 갱신 전의 값은 남겨야 하는지 같은 것들이다. 문제는 이 규칙들이 데이터 모델에는 드러나지 않고 개별 화면 설계서나 배치 설계서에 흩어져 기록된다는 점이다. 업무 자체에 복잡한 규칙이 있는 것은 어쩔 수 없더라도, 시스템과 설계가 그 위에 복잡성을 더 얹는 일은 피하고 싶다.
왜 이런 개념이 생겼는가
현실 세계의 사건 대부분은 되돌릴 수 없다. 돈을 송금했다. 주문을 했다. 계약을 체결했다. 병원 진료를 받았다. 일단 일어난 사실은 나중에 수정되는 것이 아니다. 계약을 파기해도 “체결했다”는 사실이 사라지는 것이 아니라 “파기했다”는 새로운 사실이 뒤에 추가될 뿐이고, 지난 진료 기록을 고쳐 쓰는 병원이 있다면 그것은 정정이 아니라 위조다. 현실은 상태가 아니라 사실의 연속이며, 상태는 그 사실들이 쌓인 결과일 뿐이다.
그런데 우리는 데이터베이스에서는 UPDATE를 한다. 현실 어디에도 없는 “사실의 덮어쓰기”가 데이터베이스 안에서만 일어나는 것이다. 이 관행에는 역사적인 이유가 있다. 저장 장치가 비싸던 시절에는 지나간 상태까지 남겨 둘 여유가 없었으므로, 현재 상태 하나만 저장하고 변화가 생기면 그 자리를 덮어쓰는 설계가 표준이 되었다. 저장 비용이 무시할 수 있는 수준으로 떨어진 지금은 덮어써야 할 기술적 이유가 대부분 사라졌는데도, 설계 관행만 그대로 남았다.
문제는 이 관행이 현실과 모델 사이에 어긋남을 만든다는 점이다. 현실에서 주문, 주문 확인, 발송은 각각 다른 시점에 일어난 별개의 사실인데, 이를 주문 한 행의 컬럼 갱신으로 표현하는 순간 사실들의 독립성이 사라지고 서론에서 말한 갱신 규칙들이 그 자리를 채운다. 덮어쓰는 순간 잃는 것도 분명하다. 갱신 전의 값(이력), 누가 왜 바꿨는지(감사), 지난 시점의 상태를 다시 볼 가능성(시간)이 함께 사라진다.
왜 현실을 시간과 사실(Fact)의 관점으로 모델링해야 하는가
그렇다면 모델을 무엇에 맞춰 다시 세워야 하는가. 기준은 시간과 사실이다. 사실(Fact) 은 특정 시점에 일어난 일의 기록이다. “3번 주문이 7월 10일에 들어왔다”, “그 주문이 7월 12일에 발송되었다”처럼 사실에는 반드시 시점이 붙 고, 한번 일어난 사실은 시간이 지나도 참이다. 주문이 나중에 취소되더라도 “주문이 들어왔다”가 거짓이 되는 것이 아니라, “취소되었다”는 새로운 사실이 시간 축 위에 하나 더 놓일 뿐이다. 현실이 사실을 추가하기만 하듯 모델도 사실을 추가하기만 하게 만들면, 덮어쓰기를 관리하기 위한 복잡성은 처음부터 생기지 않는다.
이 관점에서 상태는 시간 축을 특정 시점에서 자른 단면이다. “이 회원의 주소는 무엇인가”는 불완전한 질문이고, “지금 이 회원의 주소는 무엇인가”가 완전한 질문이다. 상태만 저장하는 모델은 “지금”에 대한 답만 갖고 있어서, 시간이 개입하는 질문(지난달 말 기준 재고는 얼마였나, 이 가격은 언제부터 적용됐나)에는 답하지 못한다. 시점이 붙은 사실을 그대로 보존하는 모델은 어느 시점의 단면이든 다시 만들어 답할 수 있다.
분산 시스템 연구자 Pat Helland는 이 관점을 “진실은 로그다. 데이터베이스는 로그의 부분집합에 대한 캐시다”라는 문장으로 요약했다. 시점이 찍힌 불변의 사실들이 쌓인 역사(History)가 원본이고, 테이블에서 조회하는 현재 상태는 그 역사에서 파생된 캐시라는 것이다. 그는 원자적 트랜잭션으로 묶을 수 없는 엔티티 사이의 협업도 상대와 주고받은 사실들을 파트너별로 기억하는 상태(Activity)로 관리된다고 정리했는데, 이 구조는 Step 3의 롱텀 이벤트 패턴에서 다시 만나게 된다.
덮어쓰지 않고 변화를 기록하는 구조는 이미 일상에 있다. 통장이 그렇다. 입금과 출금은 한 줄씩 추가될 뿐 이미 적힌 줄은 고쳐지지 않고, 잘못 보낸 돈도 지난 줄을 고치는 대신 정정 거래를 한 줄 더 적어 바로잡으며, 현재 잔액은 쌓인 거래의 결과를 요약한 숫자일 뿐이다. Immutable Data Model 은 데이터 모델을 이 통장처럼 설계해서 UPDATE의 발생 자체를 설계 단계에서 줄이는 모델링 기법이다. 일어난 일은 새 행으로 추가하고(INSERT), 덮어쓰기가 필요한 자리는 잔액처럼 요약 값 한 곳으로 좁혀 격리한다.
이 기법은 다섯 단계의 절차로 구성되어 있어서, 절차를 따라가면 설계자 간 스킬 편차도 어느 정도 흡수할 수 있다. 이 글은 테이블 설계와 ER 다이어그램을 그리는 개발자를 대상으로, 엔티티 추출부터 교차 엔티티 도입까지 각 단계를 온라인 쇼핑몰 예제로 설명한다.
Step 1. 5W1H로 엔티티를 추출한다
절차의 출발점은 요구사항 문서에서 엔티티 후보를 골라내는 일이다. 기준은 5W1H다.
| 구분 | 쇼핑몰에서의 엔티티 후보 예 |
| Who | 회원, 판매자, 상담원, 배송 기사 |
| What | 상품, 재고, 쿠폰, 리뷰 |
| When | 날짜, 월, 연도, 판촉 기간 |
| Where | 배송지, 창고, URL |
| Why | 주문, 반품, 결제, 환불 |
| How | 주문서, 청구서, 영수증 |
예를 들어 다음 요구사항 문서가 있다고 하자.
주문 담당자가 주문 리스트를 바탕으로 상품의 재고를 확인하고, 재고가 있으면 상품을 주문자가 주문 시 지정한 배송지 주소로 발송한다.
여기서 5W1H에 해당하는 단어에 밑줄을 긋는다. 주문 담당자와 주문자(Who), 상품과 재고(What), 배송지 주소(Where), 주문과 발송(Why)이 각각 엔티티와 그 속성의 후보가 된다.

