버저닝 (Versioning) 패턴 - NoSQL 및 RDBMS 이력 관리 데이터 모델 설계

핵심 인사이트 (3줄 요약)

  1. 본질: 버저닝(Versioning) 패턴은 데이터베이스 설계 시 레코드가 수정(Update)될 때 원본 데이터를 덮어쓰지(Overwrite) 않고, 수정될 때마다 새로운 버전 번호(v1, v2, v3...)를 매겨 데이터의 전체 변경 이력(History)을 영속적으로 보존하는 스키마 디자인 패턴이다.
  2. 가치: 법적 규제(Compliance), 금융 감사(Audit), 위키피디아 같은 문서 이력 추적이 필수적인 도메인에서 "누가 언제 데이터를 어떻게 바꿨는가?"에 대한 완벽한 단일 진실의 원천(SSOT)을 제공하며, 실수로 지운 데이터를 100% 복구할 수 있는 안정성을 보장한다.
  3. 융합: 과거 RDBMS에서는 히스토리 테이블(History Table)을 따로 파거나 트리거(Trigger)를 거는 무거운 방식으로 구현했으나, 최근에는 NoSQL의 유연한 문서 구조를 활용한 단일 컬렉션 버저닝, 이벤트 소싱(Event Sourcing), 그리고 SCD(Slowly Changing Dimensions) Type 2 빅데이터 분석 기법과 완벽하게 융합되어 거버넌스를 책임진다.

Ⅰ. 개요 및 필요성 (Context & Necessity)

  • 개념: 일반적인 데이터베이스는 CRUD 중 'U(Update)'가 발생하면 과거의 값을 메모리 속으로 날려버리고 새 값으로 디스크를 덮어쓴다. 버저닝 패턴은 이 'U' 연산을 '버전 번호를 올린 새로운 I(Insert)' 연산으로 둔갑시키는 철학이다. 사용자가 보는 화면에는 가장 최신 버전(is_latest: true)만 보여주고, 뒷단에는 과거의 스냅샷들이 무수히 쌓여있다.

  • 필요성: 병원의 환자 진료 기록이나 은행의 계좌 송금 내역을 생각해 보자. 의사가 3개월 전 진료 차트를 나중에 몰래 수정했는데 과거 기록이 덮어씌워져 버리면 큰 의료 분쟁 시 증거가 사라진다. EU의 GDPR 법률이나 금융권 ISMS 규제에서는 중요 데이터의 변경 이력 보존을 법으로 강제한다. 따라서 데이터를 단순 보관하는 것을 넘어 "시간의 흐름(Time-series)"에 따른 상태 변화를 모두 캡처해 두는 아키텍처는 선택이 아닌 필수다.

  • 💡 비유: 우리가 쓰는 '한글(Hwp)' 문서나 '구글 독스(Google Docs)'를 떠올려 봅시다.

    • 버저닝 미적용 (일반 DB): 글을 쓰다가 Ctrl+S (저장)를 누르면 과거 썼던 내용은 영원히 사라집니다. 실수로 다 지우고 저장해 버리면 복구할 길이 없습니다.
    • 버저닝 적용 (구글 독스): 내가 글을 10번 고쳤을 때, 메뉴에서 '버전 기록 보기'를 누르면 1시간 전, 어제, 1주일 전 문서의 모습이 완벽하게 다 남아있어서 원할 때 언제든지 '과거 버전으로 복구(Rollback)'할 수 있는 마법의 저장 방식입니다.
  • 등장 배경 및 발전 과정:

    1. RDBMS의 트리거(Trigger) 시대: RDBMS 시절에는 메인 테이블(User)에 Update가 일어나면 트리거가 발동해 옛날 데이터를 이력 테이블(User_History)로 복사하는 방식이 주류였다 (DBA의 고통 가중).
    2. NoSQL 및 Event Sourcing의 부상: NoSQL 시대가 열리며 굳이 테이블을 쪼갤 필요 없이, JSON 문서 뱃속에 이력 배열(Array)을 임베딩하거나, 아예 모든 변경을 로그로 쌓는 이벤트 소싱 사상이 득세했다.
    3. 데이터 레이크와 SCD Type 2: 분석가들이 과거 10년간의 데이터 변화 트렌드를 분석하기 위해, 웨어하우스(DW) 환경에서 시작/종료 시간(Effective Date)을 박아 넣는 SCD(천천히 변하는 차원) 모델링이 정석으로 자리 잡았다.
  • 📢 섹션 요약 비유: 지우개가 없는 연필(버저닝)입니다. 글씨를 틀리면 지우개로 싹 지워버리고(Update) 흔적을 없애는 게 아니라, 틀린 글씨 위에 빨간 줄(구 버전 표시)을 찍 긋고 그 옆에 새 글씨(새 버전)를 써서, 내가 어떤 실수를 하며 글을 고쳐왔는지 일기장 전체를 남기는 정직한 기록법입니다.


Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)

버저닝 패턴의 3대 아키텍처 구현 전략 (NoSQL & RDBMS 공통)

버전 이력을 어디에 어떻게 쌓을 것인가에 따라 시스템 성능과 용량이 완전히 달라진다.

  ┌───────────────────────────────────────────────────────────────┐
  │         데이터베이스 버저닝(Versioning) 모델링의 3가지 핵심 전략       │
  ├───────────────────────────────────────────────────────────────┤
  │                                                               │
  │  [ 1. Document Versioning (내장 배열 패턴 - NoSQL 전용) ]        │
  │   ▶ 하나의 문서(JSON) 안에 `revisions` 배열을 만들고 과거 이력을 쌓음. │
  │   {  _id: 101,  current_price: 2000,                          │
  │      revisions: [ { price: 1000, date: "23-01" },             │
  │                   { price: 1500, date: "24-01" } ] }          │
  │   - 장점: 단 1번의 쿼리로 현재값과 전체 과거 이력을 몽땅 가져옴 (O(1)).    │
  │   - 단점: 이력이 10만 개로 늘어나면 16MB 한도(OOM) 뚫고 문서 폭발! 💥   │
  │                                                               │
  │                                                               │
  │  [ 2. History Collection (이력 테이블 분리 패턴 - RDBMS/NoSQL) ] │
  │   ▶ 메인 테이블에는 '최신값'만 남기고, 수정 시 과거값을 이력 테이블에 Insert.│
  │   [Product Table] (최신) ID: 101 | Price: 2000                │
  │   [Product_History]     ID: 101 | Price: 1000 | V: 1          │
  │                         ID: 101 | Price: 1500 | V: 2          │
  │   - 장점: 메인 테이블이 가벼워 메인 로직의 조회 속도가 극대화됨.           │
  │   - 단점: 과거 이력을 보려면 별도의 테이블을 뒤져서 조인/조회해야 함.         │
  │                                                               │
  │                                                               │
  │  [ 3. SCD Type 2 (단일 테이블 라인 분리 패턴 - DW/분석계) ]         │
  │   ▶ 한 테이블에 최신값과 과거값을 섞어 넣고 `is_latest` 플래그로 구분.     │
  │   [Product] ID: 101 | V: 1 | Price: 1000 | is_latest: False │
  │   [Product] ID: 101 | V: 2 | Price: 1500 | is_latest: False │
  │   [Product] ID: 101 | V: 3 | Price: 2000 | is_latest: True  │
  │   - 장점: "특정 날짜 기준 데이터 줘!" 등 시계열 분석(Analytics)에 최강.    │
  │   - 단점: 최신값을 쿼리할 때 무조건 `WHERE is_latest=True`를 붙여야 함. │
  └───────────────────────────────────────────────────────────────┘

[다이어그램 해설] 만약 고객이 이력(History) 화면을 수시로 띄워 본다면 1번 내장 배열 패턴이 코딩하기 제일 편하다. 하지만 이력이 수만 건 쌓이는 위키피디아(Wiki) 문서라면 2번 이력 테이블 분리 패턴을 써야 부모 문서를 살릴 수 있다. 반면, 데이터 분석가들이 지난 10년간의 가격 변동 추이를 SQL로 편하게 돌려보고 싶어 한다면 3번 단일 테이블 방식(SCD)이 압도적이다. 이처럼 버저닝은 시스템이 이력을 '어떻게 소비할 것인가(Query Pattern)'에 따라 스키마가 완전히 뒤바뀌는 고도의 아키텍처다.


Ⅲ. 실무 적용 및 기술사적 판단

실무 시나리오

  1. 시나리오 — 배송 시스템의 과거 이력 소실 사고: 이커머스에서 고객이 3일 전 100,000원에 신발을 샀다. 그런데 오늘 관리자가 백오피스에서 신발 가격을 120,000원으로 올렸다(Update). 고객이 마이페이지에서 '내 주문 내역'을 눌렀더니, 3일 전에 산 신발 가격이 120,000원으로 출력되어 2만 원 바가지 씌웠다며 클레임이 폭주한 상황.

    • 판단: 주문 테이블이 상품 테이블의 가격(Price)을 외래키(FK)로 직접 조인하여 실시간으로 읽어오게 만든 최악의 RDBMS 정규화 안티패턴이다.
    • 해결책: 주문 도메인은 필연적으로 **스냅샷(Snapshot)**이자 **버저닝(Versioning)**이 결합되어야 한다. 상품 가격이 오를 때마다 상품 테이블에 버전을 매겨 저장(Product_V1, Product_V2)하거나, 아니면 애초에 주문을 생성(Insert)할 때 그 당시의 상품 이름과 가격 텍스트를 고스란히 복사(Denormalization)해서 주문 테이블 뱃속에 박제해버려야 한다. 이를 통해 원본(상품)이 어떻게 변하든 내 과거 주문의 무결성을 지켜낸다.
  2. 시나리오 — MongoDB 낙관적 락(Optimistic Locking) 붕괴: 문서 편집기 SaaS를 MongoDB로 구축했다(문서 번호 V1). A사용자와 B사용자가 동시에 V1 문서를 열어놓고 글을 수정하다가, A가 먼저 '저장'을 눌러 DB에 넣고 1초 뒤 B도 '저장'을 눌렀다. B의 내용이 A의 내용을 덮어써 버려(Lost Update), A가 쓴 글이 허공으로 날아간 끔찍한 동시성 장애 상황.

    • 판단: 버전 관리가 단순히 이력을 저장하는 것을 넘어, 동시성 통제(Concurrency Control) 역할까지 수행해야 함을 간과한 것이다.
    • 해결책: 데이터 모델에 __v (버전 해시) 필드를 도입하여 **낙관적 락(Optimistic Lock)**을 융합한다. A가 덮어쓸 때 __v: 1__v: 2로 바뀐다. 1초 뒤 B가 자기가 쥐고 있던 __v: 1 문서 상태로 업데이트를 날린다. DB 쿼리문에 WHERE _id = 101 AND __v = 1 조건을 건다. DB에 있는 문서는 이미 __v: 2가 되었으므로 쿼리는 매칭 실패(Update Count 0)로 튕긴다. 백엔드는 B사용자에게 "다른 사람이 방금 문서를 고쳤으니 새로고침 하세요!"라고 에러를 띄워줘 데이터 증발을 100% 막아낸다.

도입 체크리스트

  • 인프라/비용적: 버저닝을 도입한다는 것은 데이터 쓰레기(구 버전)를 영원히 끌어안고 가겠다는 뜻이다. RDBMS나 NoSQL 스토리지가 기하급수적으로 비대해질 준비가 되었는가? 만약 용량이 감당 안 된다면, 최신 버전 3개만 Hot DB(RDS/Mongo)에 남겨두고, 나머지 과거 수천 개의 버전 이력은 밤마다 배치(Batch)로 퍼내어 싼 S3(Cold Storage)로 이관(Archiving)하는 데이터 파이프라인(Data Lifecycle Management) 구축이 선행되어야 한다.

Ⅳ. 기대효과 및 결론

정량/정성 기대효과

구분단순 CRUD (덮어쓰기 모델)버저닝 패턴 (History & Snapshot)기대 효과 및 개선 지표
정량 (감사 대응)해킹/조작 발생 시 원본 추적 절대 불가변경자, 변경 시간, 전후 데이터 100% 보존ISMS 및 GDPR 보안 감사 요건 100% 패스
정량 (데이터 복원)장애 시 새벽 전일자 백업 파일 수동 복구(수 시간)로직을 통해 이전 버전 번호로 즉각 롤백휴먼 에러로 인한 데이터 손실 복구 시간 수 초 컷(100배 단축)
정성 (분석 가치)분석가는 항상 '현재 스냅샷' 데이터만 봄과거 수년간의 상태 전이 트렌드(Trend) 분석**빅데이터 시계열 분석(Time-Series)**을 위한 황금 같은 데이터 마이닝 가능

데이터베이스 설계에서 버저닝(Versioning)은 귀찮은 추가 작업이 아니라, 비즈니스의 '시간의 축(Time Axis)'을 데이터베이스 모델링에 삽입하는 철학적 차원 이동이다. 기술사는 단순히 공간을 아끼려고 옛날 데이터를 덮어쓰는 DBA의 본능을 누르고, 저장 용량(Storage Cost)을 희생하는 대가로 규제 준수(Compliance), 동시성 락 제어(Locking), 시간 여행(Time-Travel Analytics)이라는 3마리 황금 거위를 모두 잡아내는 거시적인 데이터 거버넌스 아키텍트로 활약해야 한다.


📌 관련 개념 맵 (Knowledge Graph)

개념 명칭관계 및 시너지 설명
낙관적 락 (Optimistic Locking)1번 사용자와 2번 사용자가 동시에 수정(충돌)할 때, 데이터의 version 번호표를 비교하여 뒤늦게 저장하려는 사람의 업데이트를 안전하게 튕겨버리는 동시성 방어 기술.
이벤트 소싱 (Event Sourcing)아예 상태 덩어리를 저장하지 않고 "A가 B로 변경했다"는 액션(이벤트)만 영구히 기록해, 이벤트들을 처음부터 쫙 재생(Replay)하면 현재 버전이 튀어나오는 버저닝의 끝판왕 모델.
SCD (천천히 변하는 차원)데이터 웨어하우스(DW) 모델링 기법. 고객 주소가 이사 갔을 때 덮어쓰지 않고 새로운 행을 만들어 Start_Date, End_Date를 박아 이력을 추적하는 빅데이터 표준(Type 2) 설계다.
타임 트래블 쿼리 (Time-travel Query)Snowflake나 BigQuery 같은 최신 클라우드 DB가 제공하는 기능. 쿼리문에 AT (TIMESTAMP => '어제')만 붙이면 어제 시점의 데이터 테이블을 그대로 볼 수 있는 마법.
TTL (Time To Live)버전 이력을 다른 컬렉션에 무한히 쌓아두면 DB가 터지므로, 이력 문서에 expireAfterSeconds 인덱스를 걸어 1년 지난 과거 이력은 알아서 폭파되도록 만드는 생명주기 관리 기법.

👶 어린이를 위한 3줄 비유 설명

  1. 내가 일기장에 글을 쓰다가 지우개로 빡빡 지우고 다른 내용을 쓰면, 나중에 "어? 나 옛날에 뭐라고 썼었지?" 하고 후회해도 다시는 볼 수가 없죠 (덮어쓰기 모델).
  2. 그래서 똑똑한 친구는 지우개를 쓰지 않아요! 틀리면 그 페이지를 그대로 둔 채, 다음 장을 넘겨서 '버전 2'라고 적고 새로 글을 이어서 쓰는 거예요.
  3. 종이(저장 공간)는 엄청나게 많이 쓰겠지만, 나중에 1장부터 끝 장까지 쭉 훑어보면 내가 어떤 실수들을 하면서 생각을 바꿔왔는지 100% 완벽하게 기억해 낼 수 있는 마법의 일기장 쓰기법이 바로 '버저닝'이랍니다!