User Tools

Site Tools


lecture:ring:srcs

Ring 프로젝트

Ring 소스 디렉토리 트리

ring
│
│  project.clj
│  README.md
│  
├─ring-core
│  │  project.clj
│  │
│  ├─src
│  │  └─ring
│  │      ├─middleware
│  │      │  │  content_type.clj
│  │      │  │  cookies.clj
│  │      │  │  file.clj
│  │      │  │  file_info.clj
│  │      │  │  flash.clj
│  │      │  │  head.clj
│  │      │  │  keyword_params.clj
│  │      │  │  multipart_params.clj
│  │      │  │  nested_params.clj
│  │      │  │  params.clj
│  │      │  │  resource.clj
│  │      │  │  session.clj
│  │      │  │  
│  │      │  ├─multipart_params
│  │      │  │      byte_array.clj
│  │      │  │      temp_file.clj
│  │      │  │      
│  │      │  └─session
│  │      │          cookie.clj
│  │      │          memory.clj
│  │      │          store.clj
│  │      │          
│  │      └─util
│  │              codec.clj
│  │              data.clj
│  │              io.clj
│  │              mime_type.clj
│  │              response.clj
│  │              test.clj
│  │              
│  └─test
│                      
├─ring-devel
│  │  project.clj
│  │  
│  ├─resources
│  │  └─ring
│  │      └─css
│  │              dump.css
│  │              stacktrace.css
│  │              
│  ├─src
│  │  └─ring
│  │      ├─handler
│  │      │      dump.clj
│  │      │      
│  │      └─middleware
│  │              lint.clj
│  │              reload.clj
│  │              stacktrace.clj
│  │              
│  └─test
│                      
├─ring-jetty-adapter
│  │  project.clj
│  │  
│  ├─src
│  │  └─ring
│  │      └─adapter
│  │              jetty.clj
│  │              
│  └─test
│                      
└─ring-servlet
    │  project.clj
    │  
    ├─src
    │  └─ring
    │      └─util
    │              servlet.clj
    │              
    └─test
                        

Ring 추상화

Ring은 클로져 웹 프로그래밍을 위해 어댑터와 미들웨어라는 2가지 추상화를 제공한다.

어댑터 추상화

어댑터 추상화는 웹 서버가 무엇이든지 상관없이 Ring 어플리케이션이 그 위에서 수정없이 실행될 수 있는 기능을 제공한다.

미들웨어 추상화

미들웨어 추상화는 클로져 맵으로 전이된 HTTP Request/Response 에 대한 파이프라인 처리 기능을 제공한다.

(사실 Ring의 미들웨어 추상화를 어떻게 분류할 지는 사람마다 좀 다르다. 'Clojure Programming'의 저자인 Chas Emerick은 Command 패턴으로 보고 있고, Konrad Garus는 Functional Decorator 패턴으로 보고 있다. Brian Marick도 “Pipes and Filters” 패턴으로 보고 있다. 본인의 경우에는 파이프라인을 구현한 모나드로 보는 것이 맞다고 생각한다. 이에 대해서는 뒤에서 자세히 논의할 것이다. )

위 그림은 Ring 구조를 그린 것이다.

미들웨어는 Ring 핸들러를 인자로 받아서 새로운 Ring 핸들러를 리턴한다. 이 리턴된 핸들러를 다른 미들웨어가 인자로 받아서 새로운 핸들러가 리턴되는 방식으로 핸들러들이 파이프라인을 구성하게 된다.

파이프라인을 구성하는 것은 핸들러이지 미들웨어가 아니다. 왜냐면 Request를 인자로 받는 것은 핸들러이지 미들웨어가 아니기 때문이다. 인터넷의 일부 Ring 설명 다이어그램들이 미들웨어가 파이프라인을 구성하는 것으로 되어 있는데 이는 잘못된 것이다.

미들웨어는 핸들러들의 파이프라인을 구성하는 역할만을 할 뿐이다.

다중 프로젝트 구조

Ring 프로젝트는 일반적인 lein 프로젝트와는 다르게 다음 4개의 서브 프로젝트를 갖는 다중 프로젝트이다.

Ring은 클로져 웹 프로그래밍을 위해서 어댑터와 미들웨어라는 2가지 추상화를 제공하는데, 각 서브 프로젝트들은 이 추상화 역할에 따라 분류할 수 있다.

프로젝트 소스파일수 역할 설명
ring-core 24 미들웨어 post와 get 등의 파라미터와 쿠키등 Ring의 HTTP 웹 핵심 기능 구현
ring-devel 5 미들웨어 Ring 앱 개발 및 디버깅 관련 기능 구현
ring-jetty-adapter 2 어댑터 jetty 서버를 사용하는 Ring 어댑터
ring-servlet 2 어댑터 Ring 핸들러로 자바 서블릿 구현

서브 프로젝트 기능은 Leiningen의 기본 기능은 아니고 lein-sub라는 플러그인에 의해 추가되는 기능이다.

;;; ring/project.clj
 
(defproject ring "1.2.0"
  :description "A Clojure web applications library."
  :url "https://github.com/ring-clojure/ring"
  :license {:name "The MIT License"
            :url "http://opensource.org/licenses/MIT"}
  :dependencies
    [[ring/ring-core "1.2.0"]            ;; 서브 프로젝트들에 의존성을 건다.
     [ring/ring-devel "1.2.0"]           ;;
     [ring/ring-jetty-adapter "1.2.0"]   ;;
     [ring/ring-servlet "1.2.0"]]        ;;
  :plugins
    [[lein-sub "0.2.4"]                  ;; <== 서브 프로젝트 기능을 추가하는 플러그인
     [codox "0.6.4"]]                    ;; <== Clojure 소스 코드 문서화 플러그인
  :sub                                   ;; 플러그인 lein-sub에 의해 처리되는 키
    ["ring-core"                         ;; 서브 프로젝트들 나열
     "ring-devel"                        ;;
     "ring-jetty-adapter"                ;;
     "ring-servlet"]                     ;;
  :codox
    {:src-dir-uri "http://github.com/ring-clojure/ring/blob/1.2.0"
     :src-linenum-anchor-prefix "L"
     :sources ["ring-core/src"
               "ring-devel/src"
               "ring-jetty-adapter/src"
               "ring-servlet/src"]})

서브 프로젝트

어댑터 추상화 프로젝트

ring-jetty-adapter 프로젝트

이 프로젝트에는 jetty.clj 소스 파일 하나만 있다.

역할은 jetty 서버를 구동하고 jetty 서버의 request를 clojure map으로 바꾸고, 또 clojure map을 jetty 서버의 response 바꾼다.

다음은 jetty 서버를 구동시키는 run-jetty 함수이다.

;;; ring-jetty-adaper/jetty.clj
 
(defn ^Server run-jetty
  [handler options]
  (let [^Server s (create-server (dissoc options :configurator))
        ^QueuedThreadPool p (QueuedThreadPool. ^Integer (options :max-threads 50))]
    (when (:daemon? options false)
      (.setDaemon p true))
    (doto s
      (.setHandler (proxy-handler handler))
      (.setThreadPool p))
    (when-let [configurator (:configurator options)]
      (configurator s))
    (.start s)
    (when (:join? options true)
      (.join s))
    s))

run-jetty 함수는 create-server 함수로 실제의 Jetty 서버를 생성하고 구동시킨다. Jetty의 SelectChannelConnector 클래스로 커넥션 연결을 준비하고 Jetty의 Server 클래스를 사용하여 관리한다.

run-jetty 함수의 인자 handler는 clojure map으로 된 request를 받고 clojure map으로 된 response를 리턴하는 clojure 함수이다. 이 함수는 proxy-handler 함수에 의해 랩핑되면서 jetty에 전달된다. proxy-handler는 jetty의 자바 세계와 클로져 세계를 경계를 짓는 역할을 한다.

;;; ring-jetty-adaper/jetty.clj
 
(defn- proxy-handler
  [handler]
  (proxy [AbstractHandler] []
    (handle [_ ^Request base-request request response]
      (let [request-map  (servlet/build-request-map request)
            response-map (handler request-map)]
        (when response-map
          (servlet/update-servlet-response response response-map)
          (.setHandled base-request true))))))

proxy-handler 함수는 clojure proxy 매크로를 사용하여 jetty의 AbstractHandler 클래스의 익명 자식 클래스를 만들고 AbstractHandler의 handle 함수를 구현하고 있다.

이 AbstractHandler의 handle 함수는 jetty의 request와 response를 인자로 받는데, 이것을 clojure의 handler 함수가 처리할 수 있게 clojure map으로 변환하는 작업을 한다.

이런 작업의 구체적인 기능의 구현은 ring-servelt 프로젝트의 build-request-map과 update-servlet-response 함수에 있다.

여기서 잠깐 생각해 볼 것은 만약 proxy-handler를 다르게 구현한다면 얼마든지 jetty이외의 다른 서버에도 clojure handler를 붙일 수 있다는 것이다.

따라서 서버단에서 request와 response를 어떻게 처리하든지 간에 그것을 clojure map으로 변환하는 것이 가능하다면 얼마든지 clojure handler를 사용하는 것이 가능하다는 얘기가 되고 이것은 clojure map을 사용하는 clojure 단은 전혀 건드리지 않게 된다는 것이다.

실제로 Netty 서버의 경우에 다음과 같이 ring handler를 그대로 받아들여 사용하고 있다.

(use 'ring.adapter.netty)
 
(defn app [req]
  {:status  200
   :headers {"Content-Type" "text/html"}
   :body    "Hello World from Ring-Netty"})
 
(run-netty app {:port 8080})

run-netty 함수는 run-jetty 함수와 마찬가지로 Netty의 ChannelPipelineFactory를 proxy한 후 getPipeline 함수를 구현하고 있는데, 이는 run-jetty 함수에서 proxy-handler 함수가 하는 일과 정학하게 동일하다.

또한 고성능을 자랑하는 http-kit의 경우 다음과 같이 ring handler를 그대로 받아들여 사용하고 있다.

(defn app [req]
  {:status  200
   :headers {"Content-Type" "text/html"}
   :body    "hello HTTP!"})
(run-server app {:port 8080})

다만 다른 점은 http-kit이 처음부터 clojure 서버를 겨냥한 프로젝트라서 clojure 단에서 proxy를 사용하지 않고 클로져의 핸들러를 수용하는 RingHandler 클래스를 통해 ring handler를 받고 있다는 점이다.

;;; http-kit/server.clj
 
(defn run-server
  [handler {:keys [port thread ip max-body max-line worker-name-prefix queue-size]
            :or   {ip "0.0.0.0"  ; which ip (if has many ips) to bind
                   port 8090     ; which port listen incomming request
                   thread 4      ; http worker thread count
                   queue-size 20480 ; max job queued before reject to project self
                   worker-name-prefix "worker-" ; woker thread name prefix
                   max-body 8388608             ; max http body: 8m
                   max-line 4096}}]  ; max http inital line length: 4K
  (let [h (RingHandler. thread handler worker-name-prefix queue-size)
        s (HttpServer. ip port h max-body max-line)]
    (.start s)
    (fn stop-server [] (.close h) (.stop s))))

즉 proxy 역할이 clojure 층위에서가 아니라 java 층위에서 되는 것이다. RingHandler 클래스가 ring handler를 받을 때는 Runnable 인터페이스를 구현하는 HttpHandler 클래스를 구현하여 사용하는데 역시 내부적으로는 buildRequestMap 함수를 통해 http-kit request를 clojure-map으로 바꾸고 있다.

이와 같이 기존의 구현에 붙일 수 있기 때문에 이것은 Adapter 패턴이 되는데, 만일 어떤 프로토콜이 clojure map으로 변환하는 것이 손쉽게 된다면 Ring Adapter패턴이 얼마든지 적용될 수 있을 것이다.

(이름이 Ring인 것도 서로 다른 두 영역의 인터페이스를 하나로 묶는 역할을 하는 어댑터 패턴에서 온 것이라고 볼 수 있다)

ring-servlet 프로젝트

이 서브 프로젝트도 servlet.clj 소스파일 하나만 갖는다.

이것의 역할은 Jetty의 request/response를 clojure의 map 형태로 변환하는데 있다.

다음은 Jetty의 request를 clojure map을 바꾸는 build-request-map 함수이다.

;;; ring-servlet/servlet.clj
 
(defn build-request-map
  "Create the request map from the HttpServletRequest object."
  [^HttpServletRequest request]
  {:server-port        (.getServerPort request)
   :server-name        (.getServerName request)
   :remote-addr        (.getRemoteAddr request)
   :uri                (.getRequestURI request)
   :query-string       (.getQueryString request)
   :scheme             (keyword (.getScheme request))
   :request-method     (keyword (.toLowerCase (.getMethod request)))
   :headers            (get-headers request)
   :content-type       (.getContentType request)
   :content-length     (get-content-length request)
   :character-encoding (.getCharacterEncoding request)
   :ssl-client-cert    (get-client-cert request)
   :body               (.getInputStream request)})

이 함수는 아주 단순하다. Jetty의 HttpServletRequest 클래스의 각 속성들을 읽어서 clojure map으로 만들고 있다.

다음은 clojure map을 Jetty의 response로 바꾸는 build-request-map 함수이다.

;;; ring-servlet/servlet.clj
 
(defn update-servlet-response
  "Update the HttpServletResponse using a response map."
  [^HttpServletResponse response, {:keys [status headers body]}]
  (when-not response
    (throw (Exception. "Null response given.")))
  (when status
    (set-status response status))
  (doto response
    (set-headers headers)
    (set-body body)))

이 함수는 Jetty의 HttpServletResponse 클래스에 clojure map 정보의 전달하는 역할을 한다. status, headers, body 별로 set-status/set-headers/set-body 함수를 통해 설정하고 있다. (set-status/set-headers/set-body들에 대한 자세한 분석은 생략.)

미들웨어 추상화 프로젝트

ring-core 프로젝트

ring-core 프로젝트는 ring 프로젝트의 모든 핵심적 기능을 담당하고 있다.

다음은 ring-core 프로젝트의 src 폴더의 내용이다.

다음은 ring-core의 project.clj 파일이다.

;;; ring-core/project.clj
 
(defproject ring/ring-core "1.2.0"
  :description "Ring core libraries."
  :url "https://github.com/ring-clojure/ring"
  :license {:name "The MIT License"
            :url "http://opensource.org/licenses/MIT"}
  :dependencies [[org.clojure/clojure "1.3.0"]
                 [org.clojure/tools.reader "0.7.3"]
                 [ring/ring-codec "1.0.0"]
                 [commons-io "2.4"]
                 [commons-fileupload "1.3"]
                 [javax.servlet/servlet-api "2.5"]
                 [clj-time "0.4.4"]]
  :profiles
  {:1.4 {:dependencies [[org.clojure/clojure "1.4.0"]]}
   :1.5 {:dependencies [[org.clojure/clojure "1.5.1"]]}})

ring-core는 clojure 버전 1.4.0과 1.5.1에서 테스트되고 있다.

ring-devel 프로젝트

;;; ring-devel/project.clj
 
(defproject ring/ring-devel "1.2.0"
  :description "Ring development and debugging libraries."
  :url "https://github.com/ring-clojure/ring"
  :license {:name "The MIT License"
            :url "http://opensource.org/licenses/MIT"}
  :dependencies [[ring/ring-core "1.2.0"]
                 [hiccup "1.0.3"]
                 [clj-stacktrace "0.2.5"]
                 [ns-tracker "0.2.1"]]
  :profiles
  {:1.4 {:dependencies [[org.clojure/clojure "1.4.0"]]}
   :1.5 {:dependencies [[org.clojure/clojure "1.5.1"]]}})

미들웨어와 핸들러

역할

다시 한 번 정의를 보자.

  • 미들웨어 : 핸들러에 추가적인 기능을 더하는 HOF. 미들웨어의 첫 인자는 핸들러여야 하고, 새로운 핸들러를 리턴해야 한다.
  • 핸들러 : 웹 어플리케이션을 정의하는 함수. HTTP Request를 나타내는 맵을 유일한 인자로 받고, HTTP Response를 나타내는 맵을 리턴한다.
(defn handler [request]
  {:status 200
   :headers {"Content-Type" "text/html"}
   :body "Hello World"})

위는 전형적인 핸들러 함수 코드 예제이다. request 인자 하나만을 받고 있으며, HTTP Response를 나타내는 맵을 리턴한다.

(defn wrap-blabla [old-handler ...]
  (fn new-handler [request]
    ... old-handler ...))

미들웨어 핸들러 함수를 인자로 받는다. 이후 필요한데로 인자를 더 받을 수 있다. 보통 option 성격의 인자를 맵을 받는다. 핸들러 인자는 첫 인자여야 한다. 이런 관례는 미들웨어 함수를 사용하는 쪽에서 그런 가정하에 코딩하고 있기 때문에 중요하다. 예를 들어 → 스레딩 문구를 사용하여 미들웨어 함수를 호출하는데, → 스레딩 문구의에 미들웨어 함수를 사용하기 위해서는 첫 인자가 핸들러여야 하는 것이다.

또한 위의 코드처럼 미들웨어 함수의 이름은 'wrap-'으로 시작하는 것이 관례이다. 사실 미들웨어라는 말보다는 차리리 Wrapper라고 하는 것이 더 정확한 의미 전달이지 싶다. 왜냐면 미들웨어라고 하니까 뭔가 중간에 끼어드는 것으로 생각되는데, 사실 미들웨어의 실제 기능은 그것이 아니고 old-handler를 감싼(Wrapping) new-handler를 리턴하는 것이기 때문이다.

위 코드를 해석하면 old-handler를 감싸서 blabla라는 기능을 추가한 new-handler를 리턴한다는 뜻이다.

미들웨어의 종류

자 이제 미들웨어와 핸들러의 관계에 대해서 알았으니, 어떤 미들웨어들이 가능한지 보자. 이를 위해 다음과 같이 간단한 핸들러를 정의해 본다.

(defn handler-inc-a [request]
  (update-in request [:a] inc))
 
(handler-inc-a {:a 1})
;=> {:a 2}

handler-a 핸들러는 request의 :a 키의 값을 하나 증가시키는 아주 단순한 일을 한다.

Request 조작 미들웨어

우선 가장 일반적으로는 Request 조작 미들웨어다.

(defn wrap-add-b [handler b]
  (fn [request]
    (handler 
      (update-in request [:a] #(+ % b)))))
 
(def app (wrap-add-b handler-inc-a 10))
(app {:a 1})
;=> {:a 2 :b 1}

Request 조작 미들웨어는 기존 핸들러가 Request를 처리하기 전에 자신이 먼저 Request를 조작한 후에 기존 핸들러에 넘겨준다.

다음은 Request 조작 미들웨어들이다.

  • wrap-params
  • wrap-nested-params
  • wrap-multipart-params
  • wrap-keyword-params

Response 조작 미들웨어

다음으로는 Response 조작 미들웨어다.

(defn wrap-double [handler]
  (fn [request]
    (let [response (handler request)]
      (update-in response [:a] #(+ % %)))))
 
(def app (wrap-double handler-inc))
 
(app {:a 1})
;=> {:a 4}

Response 조작 미들웨어는 Request는 기존 핸들러에 넘겨주고 리턴된 Response를 조작한다.

다음은 Response 조작 미들웨어들이다.

  • wrap-file-info
  • wrap-content-type

Request/Response 조작 미들웨어

다음은 Request/Response 조작 미들웨어들이다.

  • wrap-head
  • wrap-flash
  • wrap-cookies
  • wrap-session

필터링 미들웨어

필터링 미들웨어도 가능하다.

(defn wrap-filter [handler]
  (fn [request]
    (if (= 1 (:a request))
      {:b 1}
      (handle request))))
 
(def app (wrap-filter handler-inc-a))
 
(app {:a 1})
;=> {:b 1}

필터링 미들웨어는 특정 조건의 Request를 기존 핸들러에 전달하지 않고 자신이 직접 처리한다.

다음은 필터링 미들웨어들이다.

  • wrap-resource
  • wrap-file

wrap-params

(defn wrap-params
  "Middleware to parse urlencoded parameters from the query string and form
  body (if the request is a urlencoded form). Adds the following keys to
  the request map:
    :query-params - a map of parameters from the query string
    :form-params  - a map of parameters from the body
    :params       - a merged map of all types of parameter
  Takes an optional configuration map. Recognized keys are:
    :encoding - encoding to use for url-decoding. If not specified, uses
                the request character encoding, or \"UTF-8\" if no request
                character encoding is set."
  [handler & [opts]]
  (fn [request]
    (-> request
        (params-request opts)
        handler)))

wrap-parmas는 HTTP GET으로 들어온 :query-params 키 추가하고 query-string을 파싱하여 맵 형태롤 변환하여 넣는다. HTTP POST의 form params는 :form-params 키로 추가되며, 이 둘 전체는 :params 키로 추가된다.

→ 스레딩 문구를 사용한 전형적인 Request 미들웨어 코드를 보여준다. → 스레딩 문구의 첫 요소는 request이고, 두번째 요소인 params-request의 첫 인자가 된다. 이 함수가 wrap-params가 제공하는 추가적인 기능의 모든 것을 담당한다. params-request가 :query-params, :form-params, :params 등의 키를 기존의 request 맵에 추가하는 것이다. 이렇게 변형된 request가 params-request 함수의 결과로 리턴되면, 그 다음에 기존 핸들러에 전달되는 것이다.

(defn params-request [request & [opts]]
  (let [encoding (or (:encoding opts)
                     (:character-encoding request)
                     "UTF-8")
        request  (if (:form-params request)
                   request
                   (assoc-form-params request encoding))]
    (if (:query-params request)
      request
      (assoc-query-params request encoding))))

params-request 함수는 우선 request에서 edcoding을 구하는데, 없으면 “UTF-8”이 된다. 만약 request에 이미 :form-params나 :query-params가 있으면 아무 것도 하지않고 그냥 request를 돌려주지만, 없으면 assoc-form-params와 assoc-query-params 함수를 호출한다.

(defn assoc-form-params
  "Parse and assoc parameters from the request body with the request."
  [request encoding]
  (merge-with merge request
    (if-let [body (and (urlencoded-form? request) (:body request))]
      (let [params (parse-params (slurp body :encoding encoding) encoding)]
        {:form-params params, :params params})
      {:form-params {}, :params {}})))

merge-with merge 를 했기 때문에 request와 결과맵들의 키들이 서로 merge 된다. 결과맵에는 :params 키가 있는데 기존 :params 키에 merge 된다.

FORM Params는 HTTP BODY로 오기 때문에 request의 :body 키로 구하고, slurp으로 해당 인코딩으로 읽어 들인 후, parse-params 함수로 파싱한다.

(defn- assoc-query-params
  "Parse and assoc parameters from the query string with the request."
  [request encoding]
  (merge-with merge request
    (if-let [query-string (:query-string request)]
      (let [params (parse-params query-string encoding)]
        {:query-params params, :params params})
      {:query-params {}, :params {}})))

assoc-query-params 함수도 거의 비슷하다. request의 :query-string 키로 query-string을 읽어들인 후, 같은 과정을 거친다.

(defn- parse-params [params encoding]
  (let [params (codec/form-decode params encoding)]
    (if (map? params) params {})))

parse-params 함수는 codec 이름공간의 form-decode 함수를 이용하여 파싱한다. 이것은 ring-codec 이라는 전혀 다른 프로젝트의 함수이다. ring-core는 ring-codec에 대한 의존성을 갖고 있다.

wrap-cookies

(defn wrap-cookies
  "Parses the cookies in the request map, then assocs the resulting map
  to the :cookies key on the request.
 
  Each cookie is represented as a map, with its value being held in the
  :value key. A cookie may optionally contain a :path, :domain or :port
  attribute.
 
  To set cookies, add a map to the :cookies key on the response. The values
  of the cookie map can either be strings, or maps containing the following
  keys:
 
  :value     - the new value of the cookie
  :path      - the subpath the cookie is valid for
  :domain    - the domain the cookie is valid for
  :max-age   - the maximum age in seconds of the cookie
  :expires   - a date string at which the cookie will expire
  :secure    - set to true if the cookie is valid for HTTPS only
  :http-only - set to true if the cookie is valid for HTTP only"
  [handler]
  (fn [request]
    (-> request
        cookies-request
        handler
        cookies-response)))

wrap-cookies 함수는 전형적인 Request/Response 조작 미들웨어이다. cookies-request와 cookies-response 함수로 각 각 request와 resposne를 조작하고 있다. 이것만 봐도 어떤 일을 할 것인지에 대한 전체적인 그림이 한눈에 들어온다. 아주 잘 짠 코드이다.

이것 또한 미들웨어의 관례이다. wrap-blabla의 request 조작함수는 blabla-request이고, response 조작함수는 blabla-response이다.

(defn cookies-request
  "Parses cookies in the request map."
  [request]
  (if (request :cookies)
    request
    (assoc request :cookies (parse-cookies request))))
 
(defn cookies-response
  "For responses with :cookies, adds Set-Cookie header and returns response without :cookies."
  [response]
  (-> response
      (set-cookies)
      (dissoc :cookies)))    

cookies-request 함수는 request에 :cookies 키가 없을 때만 :cookies 키를 넣는다. request에서 cookies의 값을 뽑아내는 일은 parse-cookies 함수가 한다. cookeis-response 함수는 reponse에 대해 소정의 cookies 작업을 한 후 :cookies 키를 제거한다.

(defn- parse-cookies
  "Parse the cookies from a request map."
  [request]
  (if-let [cookie (get-in request [:headers "cookie"])]
    (-> cookie
      parse-cookie-header
      normalize-quoted-strs
      to-cookie-map
      (dissoc "$Version"))
    {}))

parse-cookies 함수가 실제적인 작업을 하는 함수이다. 이 함수는 request의 :header에서 “cookie”키의 값을 가져와서 cookie-map으로 만드는 역할을 한다. 이 과정은 3개의 함수의 이름에 의해 잘 설명되고 있다.

parse-cookie-header 함수는 cookie 스트링을 파싱한다. (즉 키 값 쌍으로 나눈다) normalize-quoted-strs 함수는 파싱된 키 값 쌍 중에서 인용부호 인 것들을 정규화한다. to-cookie-map 이렇게 파싱된 것을 map으로 변환한다.

(defn- parse-cookie-header
  "Turn a HTTP Cookie header into a list of name/value pairs."
  [header]
  (for [[_ name value] (re-seq re-cookie header)]
    [name value]))

parse-cookie-header 함수는 re-cookie 정규표현식으로 파싱해서 name value의 백터의 리스트를 반환한다.

(def ^{:private true
       :doc "HTTP token: 1*<any CHAR except CTLs or tspecials>. See RFC2068"}
  re-token
  #"[!#$%&'*\-+.0-9A-Z\^_`a-z\|~]+")
 
(def ^{:private true
       :doc "HTTP quoted-string: <\"> *<any TEXT except \"> <\">. See RFC2068."}
  re-quoted
  #"\"(\\\"|[^\"])*\"")
 
(def ^{:private true
       :doc "HTTP value: token | quoted-string. See RFC2109"}
  re-value
  (str re-token "|" re-quoted))
 
(def ^{:private true
       :doc "HTTP cookie-value: NAME \"=\" VALUE"}
  re-cookie
  (re-pattern (str "\\s*(" re-token ")=(" re-value ")\\s*[;,]?")))

re-cookie는 re-token과 re-value를 잡아내는 정규표현식이다. 특히 re-value는 인용부호를 잡아낼 수 있도록 re-quoted를 포함하고 있다.

(defn- normalize-quoted-strs
  "Turn quoted strings into normal Clojure strings using read-string."
  [cookies]
  (remove nil?
    (for [[name value] cookies]
      (if-let [value (codec/form-decode-str value)]
        (if (.startsWith ^String value "\"")
          [name (edn/read-string value)]
          [name value])))))

normalize-quoted-strs 함수는 parse-cookie-header 함수에서 리턴된 name value 쌍들을 순회하면서 value를 form decoding 한 후 만약 “로 시작하면 edn의 read-string 처리를 한다.

edn은 extensible data notation 의 약자로 클로져의 데이타 표현식을 그 자체로 Serializaion용으로 사용하는 것을 말한다. 리치 히키가 만든 것인데 보통 json과 대응해서 생각해 볼 수 있다. 클로져 어플끼리의 데이타 전송을 edn으로 하면 파싱할 필요없이 바로 clojure reader로 읽어버리면 되기 때문에 파싱후 변환 과정이 필요없게 된다.

wrap-session

wrap-session도 request와 response를 둘 다 조작한다. 즉 session-request와 session-response 함수가 사용되고 있는데, 이 2 함수들은 쿠키 조작을 한 번 거친 다음에 진짜 세션 처리를 하는 bare-session-request와 bare-session-response 함수를 호출한다

(defn session-request
  "Reads current HTTP session map and adds it to :session key of the request."
  [request & [opts]]
  (-> request
      cookies/cookies-request
      (bare-session-request opts)))
 
(defn session-response
  "Updates session based on :session key in response."
  [response request & [opts]]
  (if response
    (-> response
        (bare-session-response request opts)
        cookies/cookies-response)))      

session-request와 session-response 함수는 각각 cookis의 cookies-request와 cookies-response 함수를 호출하는데, 이 함수들은 request에 이미 :cookies 키가 있으면 그냥 리턴하기 때문에, wrap-session은 wrap-cookies와 충돌하지 않는다. 사실 wrap-session을 하면 wrap-cookies는 하지 않아도 된다.

세션에 관한 실제 작업은 bare-session-request와 bare-session-response 함수에서 한다.

(defn- bare-session-request
  [request & [{:keys [store cookie-name]}]]
  (let [req-key  (get-in request [:cookies cookie-name :value])
        session  (store/read-session store req-key)
        session-key (if session req-key)]
    (merge request {:session (or session {})
                    :session/key session-key})))

bare-session-request 함수는 쿠키값을 읽은 다음 그것을 키로해서 세션 스토어에서 세션값을 찾아서 요청맵에 :session 키를 만들어 세션값을 설정한다. 또한 쿠키값은 세션 스토어에서 키로 사용되기 때문에 :session/key 키에 값으로 설정한다.

(defn- bare-session-response
  [response {session-key :session/key}  & [{:keys [store cookie-name cookie-attrs]}]]
  (let [new-session-key (if (contains? response :session)
                          (if-let [session (response :session)]
                            (store/write-session store session-key session)
                            (if session-key
                              (store/delete-session store session-key))))
        session-attrs (:session-cookie-attrs response)
        cookie {cookie-name
                (merge cookie-attrs
                       session-attrs
                       {:value (or new-session-key session-key)})}
        response (dissoc response :session :session-cookie-attrs)]
    (if (or (and new-session-key (not= session-key new-session-key))
            (and session-attrs (or new-session-key session-key)))
      (assoc response :cookies (merge (response :cookies) cookie))
      response)))

bare-session-response는 응답맵에 :session 키와 그 값이 있으면 세션 스토어에 :session/key를 키로해서 넣고, :session 키는 있는데 그 값이 없으면 세션 스토어에서 :session/key 키를 제거한다.

응답맵에서 :session-cookie-attrs를 읽어서 cookie의 속성으로 설정하고 쿠키값으로는 new-session-key와 session-key로 한다. 그리고 응답맵에서 :session과 :session-cookie-attrs를 제거한다.

wrap-multipart-params

lecture/ring/srcs.txt · Last modified: 2019/02/04 14:26 (external edit)