(loop (print (eval (read))))

;;닭집을 차리기 위한 여정

Schema , SqlKorma 그리고 defmulti

타입이 쓸만한가?

예전에 c로 struct 같은 키워드로 타입을 구현하던 기억을 떠올려보자. 그때의 타입은 메모리의 위치를 참조하기 편하게 하기 위해 타입을 썼던것 같다.

뭐, 그때는 메모리는 당연히 고정되어야 하는것이고 원시타입의 크기에 따라 크기를 가늠할 수 있었기 때문이라고 생각.

그렇기 때문에 요즘에는, 그러니까 데이터의 사이즈가 막 변하고, 어떻게 조합될지도 예측하기 어려운 상황에서 그런용도로 타입을 쓰는 사람은 드물지 않을까?

오히려 다음 두 가지 이유때문에 타입을 쓰는 사람이 많을 거라 생각한다.

  • 문서화
  • 멤버변수의 모양새 제한

그렇다면, 동일한 가치를 제공하는 다른 개념이 있으면 타입은 안써도 되지 않을까?

schema

https://github.com/plumatic/schema

스키마의 핵심적인 역할을 하는 함수는 validate 이다. 요걸로 확인하고자 하는 값이 미리 정의한 스키마에 부합하는지 판별한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(def Data
"A schema for a nested data type"
{:a {:b s/Str
:c s/Int}
:d [{:e s/Keyword
:f [s/Num]}]})
(s/validate
Data
{:a {:b 123
:c "ABC"}})
;; Exception -- Value does not match schema:
;; {:a {:b (not (instance? java.lang.String 123)),
;; :c (not (integer? "ABC"))},
;; :d missing-required-key}

공홈의 저 예제를 보면 저 스키마의 형태가 상당히 직관적임을 알 수 있다. 물론 함수를 정의하면서 인자의 스키마를 지정할 수도 있고, 해당 함수 실행시에 인자가 스키마를 따르는지 확인 할 수도 있다.

그러나 매번 validate 를 하기엔 귀찮으므로 이러한 스키마를 지켜야 하는 namespace 안에서는 기본 clojure 기능들을 override 해버리자.

1
2
3
4
5
6
(ns korma-run.core
(:refer-clojure :exclude [atom fn defn defmethod letfn defrecord])
(:require [korma.core :refer :all]
[schema-tools.core :as st]
[medley.core :as md]
[schema.core :refer :all]))

그리고 나서 전역 설정을 먹이면 매 함수 호출마다 스키마를 검사하게 된다.

1
(set-fn-validation! true)

이제 이것과 SqlKorma 를 엮으면 데이터베이스 조회시 조금은 더 신뢰할 수 있는 결과가 나오겠지?

1
2
3
4
5
6
7
8
9
10
11
12
(defentity discounts (table :discount))
(defschema Discount
{(optional-key :id) Num
(required-key :type) Str
(required-key :sale?) Bool
(required-key :amount) Num})
(defentity items (table :item))
(defschema Item
{(optional-key :id) Num
(required-key :price) Num
(required-key :name) Str})

이제 defmulti 를 써서 자바에서 주로 내 밥벌이로 쓰던 mybatisdiscriminator 같은 기능을 훨씬 쉽고 더 좋게 쓸 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(defmulti promote
(fn
[_
discount :- Discount]
(:type discount)))
(defmethod promote
"fixed"
[item :- Item
discount :- Discount]
(- (:price item)
(:amount discount)))
(defmethod promote
"rate"
[item :- Item
discount :- Discount]
(* (:price item)
(- 1 (:amount discount))))