2022년 7월에 기록할만한 정보들
Table of Contents
- 1. [구글 엔지니어는 엔지니어는 이렇게 일한다] - 타이터스 윈터스, 톰 맨쉬렉
- 2. [퍼스널 MBA] - 조쉬 카우프만
- 3. [Combining clojure.spec, Design Recipies, and Domain-Driven Design (by Leandro Doctors)
- 4. Building a RESTful Web API in Clojure - a new approach - Malcolm Sparks
- 4.1. Step 1 - Initialize the request state
- 4.2. Step 2 - Service Available? (Optional)
- 4.3. Step 3 - Method Implemented?
- 4.4. Step 4 - Locate the resource
- 4.5. Step 5 - redirect
- 4.6. Step 6 - Current Representations
- 4.7. Step 7 Content negotiation
- 4.8. Step 8 - Authenticate
- 4.9. Step 9 - Authorize
- 4.10. Step 10 - Method Allowed?
- 4.11. Step 11 - Perform the method
- 4.12. Step 11 - Evaluate pre-conditions
- 4.13. Step 11 - Receive request payload
- 4.14. Step 12 - Prepare the Response
- 4.15. Step 13 - Security Headers
- 4.16. Step 14 - Error Handling
- 4.17. Step 15 - Logging
- 4.18. 레퍼런스
- 5. Want your Clojure code to go really Fast? Decompile it!
- 6. "Exotic Functional Data Structures: Hitchhiker Trees" by David Greenberg
정보는 책만이 아니므로 다른 리소스를 보고 대략적인 정리를 하면 좋겠다고 생각한다.
1 [구글 엔지니어는 엔지니어는 이렇게 일한다] - 타이터스 윈터스, 톰 맨쉬렉
구글이 어떻게 개발하고 코드를 관리하는지 알 수 있다.
테스트나 버전관리에 대한 고민은 구글과 아주 유사해서 술술 읽혔다.
하지만 비슷한 의견이어도 그것을 도출하는 방식은 하늘과 땅 차이였다.
일례로 코딩스타일가이드가 필요하다고 생각하고 있었고 개인적인 견해가 정리는 되어 있었으나 구글처럼 진지하게 이것에 대해 조사하고 수치로 검사를 해서 의사결정을 하겠다는 생각조차 한적이 없어서 반성하게 된다.
2 [퍼스널 MBA] - 조쉬 카우프만
그냥 그랬다.
3 [Combining clojure.spec, Design Recipies, and Domain-Driven Design (by Leandro Doctors)
링크 : youtube.com/watch?v=zOoSxaqKdlo 제목 그대로 DDD를 하기위해 clojure.spec과 함께 어떻게 만들어가는지를 설명한다. 간단하게 말하자면 `s/def`, `s/fdef` 를 잘 사용하자로 보인다.
3.1 기본적인 디자인
넓이를 구하는 함수를 생각해보자. (동영상의 코드를 조금 손보았다)
;; Length --> Area ;; Compute the area of a square with side length 'len'. (defn area-of-square-fn [len] ...)
이제 우리는 도메인을 한번 정의해보자.
(s/def ::length nat-int?) (s/def ::area nat-int?)
이것을 함수에 접목시키자
(s/fdef area-of-square :args :len ::length :ret ::area :fn area-of-square-fn)
테스트는 어떻게할까?
(deftest ^:stable test-area-computation (testing "Area computation" (is (= (area-of-square 2) 4)) (is (= (area-of-square 7) 49))))
여기서 우리는 spec을 통한 테스트 또한 가능하다.
(deftest ^:unstable exercise-area-of-square (testing "Exercising the function `area-of-function`" (is (= {:total 1 :check-passed 1} (stest/summarize-results (stest/check `area-of-square))))))
3.2 DDD
Value <-> Transformation : 데이터를 받아서 다른 데이터로 변환하여 리턴하는 것.
(s/fdef area-of-square :args :len ::length :ret ::area)
Event : 인자를 리턴하는 것이 아닌 유저에게 보여주는 것 같은 이벤트를 생성할 수도 있다.
(s/fdef square-area-shown-to-user :args :area ::area :ret nil?) ;; 아래처럼 사용 (square-area-shown-to-user (area-of-square 2))
3.3 레퍼런스 (읽어볼 녀석들)
- HOW TO DESIGN PROGRAM (옛날에 읽었던 책)
- Patterns, Principles, and Practices of Domain-Driven Design Scott Millet with Nick Tune
- Domain Modeling Made Functional Scott Wlaschin
- Contract Driven Development = Test Driven Development - Writing Test Cases Andreas Leitner, llinca Ciupa, …
4 Building a RESTful Web API in Clojure - a new approach - Malcolm Sparks
영상 : https://www.youtube.com/watch?v=JWa4NhjWNHQ
ring middleware handler를 사용할 때 15가지의 스텝을 따르면 좋은 REST API를 만들 것이라고 말함.
(defn ring-middleware [opts] [wrap-ring-1-adapter wrap-healthcheck #(wrap-initialize-request % opts) wrap-service-unavailable? wrap-log-request wrap-store-request wrap-error-handling wrap-method-not-implemented? wrap-locate-resource wrap-redirect wrap-find-current-representations wrap-negotiate-representation wrap-authenticate wrap-authorize wrap-method-not-allowed? wrap-initialize-response wrap-security-headers wrap-invoke-method])
4.1 Step 1 - Initialize the request state
내가 좋아하는 방식이다. 코드를 먼저 보여주는 사람.
(defn wrap-initialize-request "Initialize request." [h] (fn [req] (let [extended-req (into req {:start-date (java.util.Date.) :request-id (java.util.UUID/randomUUID) :uri (str "https://" (get-in req [:ring.request/headers "host"]) (:ring.request/path req))})] (h extended-req))))
요청을 초기화하는 미들웨어이다. 코드에도 나와있듯이 요청을 받아서 몇개의 정보를 추가한다.
4.2 Step 2 - Service Available? (Optional)
503 응답을 리턴하는 스텝이다. 서버가 정말 바쁘면 이것을 리턴함. 이 영상에서는 이 기능을 스킵함. 하지만 레퍼런스를 확인하니 부연설명이 있다.
—
- Check that your service is not overwhelmed with request.
- If it is, throw an exception. Otherwise, go to the next step.
In Clojure, when throwing an exception, embed the Ring response as exception data.
(throw (ex-info "Service unavailable" {::response ;; Embed the Ring response as exception data. {:status 503 :headers {"retry-after" "120"} :body "Service Unavailable\r\n"}}))
The catch block should catch the exception, extract the Ring response, and return it to the Ring adapter of the web server your are running. —
4.3 Step 3 - Method Implemented?
(defn wrap-method-not-implemented? [h] (fn [{:ring.request/keys [method] :as req}] (when-not (contains? #{:get :head :post :put :delete :options :patch :mkcol :propfind} method) (throw (ex-info "Method not implemented" (into req {:ring.response/status 501 :ring.response/body "Not Implemented\r\n"})))) (h req)))
4.4 Step 4 - Locate the resource
The target of an HTTP request is called a "resource".
- Resourcec – Section 2 RFC 7231
여기서 리소스란 HTTP요청의 타깃을 말함. 즉, 핸들링할 함수를 찾아주는 것이다.
링크 상에는 여러가지 내용이 있지만 간단히 하면, 현재 요청의 정보(URI, method, auth and etc)를 갖고 위치시켜줄 리소스를 찾아주는 것이다.
An origin server maintains a mapping from resource identifiers to the set of representations corresponding to each resource
Roy Fielding -
Architectural Style and the Design of Network-based Software Architecture
Reitit 같은 것을 사용하면 쉽게 라우팅을 할 수 있다.
(defn locate-resource [req] (case (:ring.request/path req) "/weather" {:id :weather :description "Today's weather" ;; Resource state :weather/precipitation 35 :weather/outlook "Overcast" :weather/temperature 16 ;; Resource configuration :http/method #{:get :head :options}})) (defn wrap-locate-resource [h] (fn [req] (h (assoc req :resource (locate-resource req)))))
4.5 Step 5 - redirect
(defn wrap-redirect [h] (fn [{:keys [resource] :ring.request/keys [method] :as req}] (when-let [location (:http/redirect resource)] (throw (ex-info "Redirect" (-> req (assoc :ring.response/status ;; get head면 302, 아니면 307 (case method (:get :head) 302 307)) (update :ring.response/headers assoc "location" location))))) (h req)))
4.6 Step 6 - Current Representations
- representation은 다음을 포함해야 한다.
- payload
- representation metadata
- content-type
- content-length
- 표현할 정보가 없다면 404를 리턴한다. (not for PUT)
(defn current-representations [req] (let [req (:resource req)] ;; url로 볼 수 있지만, id로 하는 것이 좋다 url은 바뀔 수 있으니까 (case (:id res) :weather [{:http/content-type "text/html;charset=utf-8" :http/content-language "en" :http/content-length 210} {:http/content-type "text/html;charset=utf-8" :http/content-language "es" :http/content-length 228} {:http/content-type "application/json" :http/content-encoding "gzip" :http/content-length 189}]))) (defn wrap-find-current-representations [h] (fn [{:ring.request/keys [method] :as req}] (if (#{:get :head :put} method) (let [cur-reps (seq (current-representations req))] (when (and (#{:get :head} method) (empty? cur-reps)) (throw (ex-info "Not Found" (into req {:ring.response/status 404 :ring.response/body "Not Found\r\n"}))) (h (assoc req :cur-reps cur-reps))) (h req)))))
4.7 Step 7 Content negotiation
(requre '[juxt.pick.alpha.ring :refer [pick]]) (defn negotiate-representation [{:ring.request/keys [method] :as req} cur-reps] (let [{rep :juxt.pick.alpha/representation vary :juxt.pick.alpha/vary} (pick req cur-reps {:juxt.pick.alpha/vary? true})] (when (#{:get :head} method) (when-not rep (throw (ex-info "Not Acceptable" (into req {:ring.response/status 406 :ring.response/body "Not Acceptable\r\n"}))))) (cond-> rep (not-empty vary) (assoc :http/vary vary))))
pick은 저자가 만든 라이브러리다. 아파치의 content negotiation을 사용한다고 한다.
4.8 Step 8 - Authenticate
누구인지 알아내는 일
(defn authenticate [req] ;; Check Authorization header ;; Check request for session cookies ;; Extract subject from JWT, or from session store ;; Redirect to an identity provider if necessary ) (defn wrap-authenticate [h] (fn [{:ring.request/keys [method] :as req}] (if-let [subject (when-not (= method :options) (authenticate req))] (h (assoc req :subject subject)) (h req))))
4.9 Step 9 - Authorize
권한이 있는지 확인하는 것. 요청을 허용하거나 거부할 수 있어야 한다.
- 401 - authentication required
- 403 - authenticated but still no
발표자는 Datalog 라는 것을 사용한다.
;; Alow read access to all resources tagged as public [[request :ring.request/method #{:get :head :options}] [resource :classification "PUBLIC"]] ;; Allow read access to all resources tagged as ;; INTERNAL for logged in users [[request :ring.request/method #{:get :head :options}] [resource :classification "INTERNAL"] [subject :crux.db/id _]]
crux의 datalog 라는 것인가보다.
4.10 Step 10 - Method Allowed?
(defn join-keywords [methods upper-case?] (->> methods seq distinct (map (comp (if upper-case? str/upper-case identity) name)) (str/join ", "))) (defn wrap-method-not-allowed? [h] (fn [{:keys [resource] :ring.request/keys [method] :as req}] (let [allowed-methods (set (:http/methods resource))] (when-not (contains? allowed-methods method) (throw (ex-info "Method not allowed" (into req {:ring.response/status 405 :ring.response/headers {"allow" (join-keywords allowed-methods true)} :ring.response/body "Method Not Allowed\r\n"})))) (h (assoc req :allowed-methods allowed-methods)))))
4.11 Step 11 - Perform the method
드디어 수행하는 듯
(defn wrap-invoke-method [h] (fn [{:ring.request/keys [method] :as req}] (h (case method (:get :head) (GET req) :post (POST req) :put (PUT req) :patch (PATCH req) :delete (DELETE req) :options (OPTIONS req) :propfind (PROPFIND req) :mkcol (MKCOL req)))))
이것보다 간단할 줄 알았다. 메소드에 따라 함수가 있다는 가정인 것 같다.
(defn GET [{:keys [selected-rep] :as req}] (evaluate-preconditions! req) (let [{:keys [body]} selected-rep] (cond-> (assoc req :ring.response/status 200) body (assoc :ring.response/body body)))) (defn POST [{:keys [resource] :as req}] (let [rep (receive-representation req) req (assoc req :received-representation rep) post-fn (:post-fn resource)] (if (fn? post-fn) (post-fn req) (throw (ex-info "No post-fn function!" (into req {:ring.response/status 500 :ring.response/body "Internal Error\r\n"})))))) (defn POST [{:keys [resource] :as req}] (let [rep (receive-representation req) req (assoc req :received-representation rep) put-fn (::site/put-fn resource)] (if (fn? put-fn) (post-fn req) (throw (ex-info "No put-fn function!" (into req {:ring.response/status 500 :ring.response/body "Internal Error\r\n"})))))) (defn DELETE [{:keys [crux-node uri] :as req}] (crux.api/submit-tx crux-node [[:crux.tx/delete uri]]) (into req {:ring.response/status 202 :ring.response/body "Accepted\r\n"})) (defn OPTIONS [{:keys [resource allowed-methods] :as req}] (-> (into req {:ring.response/status 200}) (update :ring.response/headers merge (:options resource) {"allow" (join-keywords allowed-methods true)}) ;; Also, add CORS headers for pre-flight requests ))
4.12 Step 11 - Evaluate pre-conditions
- RFC 7232 - Conditional Requests
- Exploited by caches to re-validate
- Mitigates 'lost-update' problem
- Uses representation metadata
- last-modified
- entity tags
- ranges
(defn evaluate-preconditions [{:ring.request/keys [headers method] :as req}] (when (not (#{:connect :options :trace} method)) (if (get headers "if-match") (evaluate-if-match req) (when (get headers "if-unmodified-since") (evaluate-if-unmodified-since req))) (if (get headers "if-none-match") (evaluate-if-none-match req) (when (#{:get :head} (:ring.request/method req)) (when (get headers "if-modified-since") (evaluate-if-modified-since req))))))
4.13 Step 11 - Receive request payload
- No content length? 411
- Bad content length? 400
- Content length too large? 413
- No body? 400
- Unacceptable content-type? 415
- Unacceptable content-encoding? 409
- Unacceptable content-language? 409
- Content-Range header? 400
- Got here? Slurp and return the body
4.14 Step 12 - Prepare the Response
(defn response [:keys [selected-rep body] :ring.request/keys [method] :as req] (cond-> req true (update :ring.response/headers assoc "date" (format-http-date (java.util.Date.))) selected-rep (update :ring.response/headers representation-headers selected-rep body) (= method :head) (dissoc :ring.response/body))) (defn wrap-initialize-response [h] (fn [req] (response (h req)))) (defn representation-headers [headers rep body] (into headers {"content-type" (some-> rep :http/content-type) "content-encoding" (some-> rep :http/content-encoding) "content-language" (some-> rep :http/content-language) "content-location" (some-> rep :http/content-location str) "last-modified" (some-> rep :http/last-modified format-http-date) "etag" (some-> rep :http/etag) "vary" (some-> rep :http/vary) "content-length" (or (some-> rep :http/content-length str) (when (counted? body) (some-> body count str))) "content-range" (:http/content-range rep) "trailer" (:http/trailer rep) "transfer-encoding" (:http/transfer-encoding rep)}))
4.15 Step 13 - Security Headers
여기에 추가할 걸 추가하셈? 뭐 https 만 쓰던가. 그런 말을 함.
4.16 Step 14 - Error Handling
(defn wrap-error-handling [h] (fn [req] (try (h req) (catch clojure.lange.ExceptionInfo e (let [{:ring.response/keys [status] :as exdata} (ex-data e)] (log/errorf e "%s: %s" (.getMessage e) (pr-str exdata)) (response (merge {:ring.response/status 500 :ring.response/body "Internal Error\r\n" :error (.getMessage e) :error-stack-trace (.getStackTrace e)} exdata {:selected-rep (error-representation e)})))))))
4.17 Step 15 - Logging
(defn log-request! [{:ring.request/keys [method] :as req}] (assert method) (log/infof "%-7s %s %s %d" (str/upper-case (name method)) (:ring.request/path req) (:ring.request/protocol req) (:ring.response/status req))) (defn wrap-log-request [h] (fn [req] (doto (h req) (log-request!))))
4.18 레퍼런스
5 Want your Clojure code to go really Fast? Decompile it!
Calva가 얼마나 강력한지 소개하기 위함인 것 같다. 나도 Calva로 이걸 써봐야겠다.
자료 : https://www.youtube.com/watch?v=sPP4LCpBic8
(set! *unchecked-math* false) ;; true (set! *unchecked-math* :warn-on-boxed)
뭐 이런걸로 숫자를 다룰 때, 불필요한 캐스팅이 줄어드나보다. 실제로 그런지 확인하기위해 발표자는 디컴파일을 해본다.
com.clojure-goes-fast/clj-java-decompiler
라는 것을 소개한다.
setting.json
에서 decompile 하는 명령어가 있다. 이걸 이용해서 *unchecked-math*
를 토글하면서 확인해줌.
6 "Exotic Functional Data Structures: Hitchhiker Trees" by David Greenberg
새로운 자료구조 히치하이커 트리를 설명한다.
발표자료는 바이너리서리트리 -> B-트리 -> B+ -> 프랙탈 순으로 트리의 변천사를 설명한다. 모두 이전에 설명한 자료구조 위에 개념이 쌓아올라간다.
히치하이커 트리는 트랙탈 트리 위에 쌓아올라간 개념이나 삽입에 사용되는 IO가 현저하기 작다는 것이 특징이다.