본문 바로가기

프로그래밍 기술 노트/기타 정보

[DB] 현재는 모든 과거의 필연적인 산물이다 - Bitemporal 을 곁들인 타임머신

현재는 모든 과거의 필연적인 산물이다 

 
Block Chain은 일종의 불변 + 분산 데이터베이스로 간주될 수 있다.
이러한 불변성이 적용된 데이터베이스 시스템도 있는데, 이를 불변(immutable) 데이터베이스라고 한다.


Datomic을 예로 들 수 있다.

Datomic - Overview
 

 

Datomic - Overview

Never Forget Critical insights come from knowing the full story of your data, not just the most recent state. Datomic stores a record of immutable facts, which gives your applications strong consistency combined with horizontal read scalability, plus b

www.datomic.com

 

엥? DB가 불변이면 삭제를 어떻게 하나!

라는 물음에 답을 하자면, 실제로 삭제하지 않아도 된다. Datomic에서 무언가를 삭제할 때, 실제로 데이터를 지우는 것이 아니라 데이터베이스가 "이제부터는 거짓이다"라고 기록하는 것이다. 즉, 더 이상 사실이 아닌 사실로 기록된다.
즉, History를 불변적으로 관리하고, 이를 통해 현재 상태를 쿼리하는 것이다.
 

이거 완전 CQRS 이벤트 소싱 아니냐?

 
간단히 설명하자면, DBMS 자체가 애초부터 이벤트 소싱처럼 디자인되었다고 보면 된다.
이로부터 얻는 이점은 Time Traveling이 가능하다는 것이다.
결국 기본 쿼리의 최종 결과는 [태초의 시간 - 현재 시간]까지의 최종 결과이다.
Datomic은 애초에 태초의 시간부터 현재까지 모든 정보를 불변적으로 다 담고 있기 때문에 [t1 - t2]를 넣으면 해당 기간만큼의 결과를 가져올 수 있다.
즉, 내가 원하는 "그" 시점에 DB의 정확한 상태를 알 수 있다는 점이다. 마치 Git과 같다.

 

Temporal (시간성)

(ns myname.myapp
  (:gen-class))

(require '[datomic.client.api :as d])

(def client (d/client {:server-type :datomic-local
                       :storage-dir :mem
                       :system "ci"}))

(defn create-db []
  (d/create-database client {:db-name "mydb"}))

(defn connect-db []
  (d/connect client {:db-name "mydb"}))

(defn setup-schema [conn]
  (d/transact conn {:tx-data [{:db/ident :person/name
                               :db/valueType :db.type/string
                               :db/cardinality :db.cardinality/one
                               :db/doc "A person's name"}]}))

(defn add-person [conn name]
  (d/transact conn {:tx-data [{:person/name name}]}))

(defn get-db-at-time [conn time]
  (d/as-of (d/db conn) time))

(defn -main [_]
  (create-db)
  (let [conn (connect-db)
        _ (setup-schema conn)
        tx (add-person conn "Alice")
        past-time  (.v (first (:tx-data tx)))
        past-db (get-db-at-time conn past-time)]

    (add-person conn "Bob")

    (println "현재 DB: " (d/q '[:find ?e ?n :where [?e :person/name ?n]] (d/db conn)))
    (println "과거 DB: " (d/q '[:find ?e ?n :where [?e :person/name ?n]] past-db))))
    
;현재 DB: [[83562883711051 Bob] [79164837199946 Alice]]
;과거 DB: [[79164837199946 Alice]]

 

그렇기 때문에 이를 temporal(시간성) 이라고 한다!
물론 그냥 RDBMS를 써도 사용자가 Application Level에서 이런 식으로 이벤트를 다 기록하고 작성하면 비슷한 결과를 얻을 수 있다. 다만 Datomic은 이 기능을 Native로 지원한다는 것이다. Native로 지원되느냐 사용자 수준에서 직접 지원하느냐는 완성도와 UX (개발자 관점에서의 UX)가 크게 차이 나게 된다.

 




Bitemporal (시간성)


그럼 여기에 Bi가 붙은 Bitemporal(쌍시간성) 이 무엇이냐 하면,
Datomic은 TX 시간 즉, 트랜잭션이 언제 기록되었는지만을 시간축으로 사용한다.

Bitemporal DB 는 TX 시간축 + Valid Time (해당 Record 가 실제로 "유효한") 시간축을 사용한다.

대표적인 DB 가 XTDB 되시겠다.

XTDB

 

XTDB — immutable SQL database for data compliance

Want to be the first to know about XTDB news, updates and events (no spam, we promise)?

xtdb.com

 

XTDB 와 같은 Bitemporal DB의 경우
여기에 실제 이 기록이 어느 시점까지 유효한지를 저장할 수 있다.

예를 들어 1년 유효기간을 가진 쿠폰이 있다고 치자. 쿠폰을 얻는 순간 TX를 기록할 텐데, TX로부터 새 쿠폰이 만들어져도 1년 뒤 쿼리하면 해당 쿠폰은 이미 유효기간이 지났기 때문에 조회되지 않아야 한다. (의도 적으로 유효기간이 지난 쿠폰을 쿼리하는 경우 말고)

다음과 같은 방법이 있을 수 있다:

  1. Cron 같은 걸로 체크해서 지운다.
  2. 유통기한을 record에 넣고 (audit field) 쿼리한 다음에 알아서 사용하지 않는다 (Application Level 필터).
  3. SQL 문에서 필터링해서, 쿼리할 때부터 필터링 한다 (DBMS 쿼리시 필터).

모두 몹시 귀찮은 작업이 아닐 수 없다.
그런데 만약 DBMS에서 Native로 "유효기간"을 필터링한다면?
그게 Bitemporal (Tx 시간축 + 유효기간 시간축)이다.
 

엥 그냥 validTime이 칼럼으로 추가된 거 아니냐?

 
아까와 같이 이게 DBMS Native로 지원되느냐의 차이다.
개발자는 Bitemporal 성을 가지는 DB를 사용하는 경우, 단순히
"흠, 이건 1년 뒤까지만 유효하겠네!" 하고
ValidTime 필드에 1년 뒤라고 기록하면
1년 뒤 쿼리하는 입장에서 (의도적으로 유효하지 않은 기록을 가져오려는 쿼리를 작성하지 않는 이상) DBMS가 기록을 쭉 살펴보고,
"이 친구는 쓸모가 없는 놈이구만?" 하고 빼고 준다.

 

(ns myname.myapp
  (:gen-class))

(require '[xtdb.api :as xt])

(defn start-node []
  (let [node (xt/start-node {})]
    (println "XTDB 노드가 시작되었습니다.")
    node))


(defn -main [_]
  (let [node (start-node)
        now (java.util.Date.)
        valid-time-start (-> now .getTime (+ 10000) java.util.Date.)
        valid-time-end (-> now .getTime (+ 20000) java.util.Date.)]

    (println now)
    (println valid-time-start)
    (println valid-time-end)

    (xt/submit-tx node [[::xt/put {:xt/id :person-1 :person/name "Alice"} valid-time-start valid-time-end]])

    (println "valid 이전" (xt/q (xt/db node) '{:find [e] :where [[e :person/name ?name]]}))
    (Thread/sleep 10000)
    (println "valid 중" (xt/q (xt/db node) '{:find [e] :where [[e :person/name ?name]]}))
    (Thread/sleep 11000)
    (println "valid 이후" (xt/q (xt/db node) '{:find [e] :where [[e :person/name ?name]]}))
  ))

; XTDB 노드가 시작되었습니다.
; #inst "2024-06-30T15:09:42.456-00:00"
; #inst "2024-06-30T15:09:52.456-00:00"
; #inst "2024-06-30T15:10:02.456-00:00"
; valid 이전 #{}
; valid 중 #{[:person-1]}
; valid 이후 #{}

시간 "축" 이므로, Start - End 타임을 둘다 정할 수 있으며 예시처럼, Valid Time 자체가 미래일수도 있다!




여담.
1. 데이터 저장비용은 조상님이 내주냐? -> 더 들긴 할텐데 특수 케이스를 제외하고 어마 무시무시하게 들지않음.
A Datomic database value is a **persistent data structure**, similar to Clojure’s collections.

2. datomic은 SQL대신 datalog을 이용하는데 prolog 논리 프로그래밍 처럼 선언적으로 쿼리하는 기법이다.

728x90