2022년 7월에 기록할만한 정보들

Table of Contents

정보는 책만이 아니므로 다른 리소스를 보고 대략적인 정리를 하면 좋겠다고 생각한다.

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 레퍼런스 (읽어볼 녀석들)

  1. HOW TO DESIGN PROGRAM (옛날에 읽었던 책)
  2. Patterns, Principles, and Practices of Domain-Driven Design Scott Millet with Nick Tune
  3. Domain Modeling Made Functional Scott Wlaschin
  4. 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 응답을 리턴하는 스텝이다. 서버가 정말 바쁘면 이것을 리턴함. 이 영상에서는 이 기능을 스킵함. 하지만 레퍼런스를 확인하니 부연설명이 있다.

  1. Check that your service is not overwhelmed with request.
  2. 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!))))

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가 현저하기 작다는 것이 특징이다.

Author: 남영환

Created: 2024-01-04 Thu 09:13

Emacs 27.2 (Org mode 9.4.4)

Validate