[220103] Clojure Ring에 대해서 알아보자
Table of Contents
1 Ring이란 무엇인가?
Ring은 Clojure 유명한 웹 어플리케이션 개발 라이브러리 중 하나이다. 이 페이지에서는 Ring의 주요 네임스페이스들을 둘러볼 것이다.
2 소스코드
ring을 멀리서 조감할 수 있게 중요한 기능만 모아놓았다. 아래 링크로 소스코드를 쉽게 구경할 수 있다. https://github.com/ssisksl77/diy-ring-jetty 이 글로 ring을 사용하는데 어려움은 줄고 즐거움이 더해지길 바란다.
3 ring.adapter.jetty 네임스페이스
ring의 위키 페이지를 보자. [link] 아래 코드가 보일 것이다.
(defn handler [request] {:status 200 :handlers {"Content-Type" "text/html"} :body "Hello World"}) (use 'ring.adapter.jetty) (run-jetty handler {:port 3000 :join? false})
run-jetty
함수에 핸들러를 던지면 동작한다. 직관적이다.
이 run-jetty
가 있는 ring.adapter.jetty
네임스페이스를 보자. [link]
아래 코드는 run-jetty
구현을 단순화한 모습니다.
(defn ^Server run-jetty [handler options] (let [server (create-server options)] (.setHandler server (proxy-handler handler)) (try (.start server) server (catch Exception ex (.stop server) (throw ex)))))
run-jetty
는 다음을 수행한다.
create-server
함수로 서버를 생성한다.proxy-handler
함수로 Servlet를 생성한다.- 서버의
setHandler
메소드를 이용하여 핸들러를 추가한다. start
메소드를 호출하고 리턴한다.
3.1 create-server 함수
create-server
함수는 서버를 생성한다.
또한 서버에서 사용할 스레드풀을 생성해준다. 아래 코드를 보자. 간단히 하기 위해 스레드풀 설정코드는 제거하였다.
(ns yhnam.jetty-wrapper (:import [org.eclipse.jetty.util.thread QueuedThreadPool])) (defn thread-pool [] (QueuedThreadPool.)) (defn http-connector [server options] (let [sc (ServerConnector. server)] (.setPort sc (options :port 3000)) sc)) (defn create-server [opts] (let [server (Server. (thread-pool))] (.addConnector server (http-connector server opts)) server)) (comment (create-server {:port 8080}) ;; )
3.2 proxy-handler 함수
proxy-handler
는 ring 프로젝트에서 중요한 함수들 중 하나다. clojure의 단순 핸들러 함수를 자바 Servlet
으로 행동할 수 있도록 한다.
(ns yhnam.jetty-wrapper (:import [javax.servlet.http HttpServletRequest HttpServletResponse] [org.eclipse.jetty.server.handler AbstractHandler])) (defn ^AbstractHandler proxy-handler [handler] (proxy [AbstractHandler] [] (handle [this ^Request base-request ^HttpServletRequest req ^HttpServletResponse response] (let [req-map (build-request-map req) response-map (handler req-map)] (update-servlet-response response response-map) (.setHandled base-request true)))))
build-request
, response-map
가 핵심 함수이다.
build-request
: Servlet의 불편한 메소드 호출들을 전부 제거해준다.update-servlet-response
: 핸들러의 response-map을 이용하여HttpServletResponse
를 설정한다. 하여 우리의 핸들러는 순수함수를 유지할 수 있다.이 두 함수는 다른 네임스페이스에 있으니 다음 장에서 다루도록 하겠다.
4 ring.util.servlet 네임스페이스
이름처럼 이 네임스페이스는 자바 Servlet
과 소통하기 위한 네임스페이스다. [link]
4.1 build-request-map
이 함수는 우리가 해야 하는 귀찮은 일들을 대신해주고 있다.
(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) Locale/ENGLISH)) :protocol (.getProtocol 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)})
request로 단순 데이터를 받는 것이
HttpServletRequest
보다 간단하다. 나는 이것이 ring 프로젝트의 정수라고 생각한다. 이것은 Clojure의 정수이기도 하다.
4.2 update-servlet-response 함수
이 함수는 응답 데이터를 단순 해시맵으로 리턴해도 되도록 만든다. update-servlet-response
가 그 일을 대신 해줄 것이다.
(require '[ring.core.protocols :as protocols]) (defn set-headers [^HttpServletResponse response headers] (let [[key val-or-vals] headers] (if (string? val-or-vals) (.setHeader response key val-or-vals) (doseq [val val-or-vals] (.addHeader response key val)))) (when-let [content-type (get headers "Content-Type")] (set-headers response (:headers response-map)) (.setContentType response content-type))) (defn update-servlet-response [response response-map] (let [output-stream (make-output-stream response)] (set-headers response (:headers response-map)) (protocols/write-body-to-stream (:body response-map) response output-stream)))
흥미로운 코드가 있다. 바로 set-hadears
다.
HttpServletResponse
에는 .setHeader
, .addHeader
라는 두가지 메소드가 있다. 개인적으로 좋지 않은 설계라고 생각한다. 차라리 .setHeader
가 컬렉션을 인자로 받는 녀석도 만들어줬어야 하는 것 아닌가 생각한다. set-headers
가 이일을 대신한다. (물론 자바로도 쉽게 가능하다)
write-body-to-stream
은 응답바디를 outputStream에 write한다.
자바의 기존 구현체는 물론 추후 확장에도 유용하도록 defprotocol
을 사용하였다.
각 구현체에 따라 write-body-to-stream
은 다르게 동작할 것이다.
5 ring.core.protocols 네임스페이스
5.1 StreamableResponseBody 프로토콜
ring 핸들러 함수는 응답바디에 여러가지 자료구조를 리턴할 수 있다. [link]
StreamableResponseBody
은 응답바디를 받아서 outputstream에 쓰고 닫는다. 각 자료구조마다 이 문제를 해결하는 방법이 다르다. 이 문제를 ring은 다형성으로 풀었다.
자바에서는 동적으로 class
를 확장할 수 없다.
클로저는 extend-protocol
로 존재하는 자바 타입을 동적으로 확장할 수 있다.
(ns ring.core.protocols "Protocols necessary for Ring." (:import [java.io Writer OutputStream]) (:require [clojure.java.io :as io])) (defprotocol StreamableResponseBody (write-body-to-stream [body response output-stream])) (extend-protocol StreamableResponseBody (Class/forName "[B") (write-body-to-stream [body _ ^OutputStream output-stream] (.write output-stream ^Byte body) (.close output-stream)) String (write-bo-to-stream [body response output-stream] (doto (io/writer output-stream) (.write body) (.close))) ,,, ;; other types described. )
6 미들웨어 추가하기
run-jetty
함수를 다시보자. ring에서는 오로지 하나의 핸들러 함수만을 받는다. 미들웨어를 추가할 수 있는 방법이 없다.
하지만, 함수 하나만으로 충분하다.
;; simple handler (defn hello-world [req] {:status 200 :headers {"Content-Type" "text/plain"} :body "Hello World"}) ;; middleware (defn log-middleware [handler] (fn [req] (println req) (handler req))) (comment (def app (run-jetty (-> hello-world log-middleware))) (.stop app) ;; )
미들웨어도 단순히 함수일 뿐이다. 어노테이션(@Controller
, @Component
)나 implements XXXInterceptor
같은 것들은 존재하지 않는다.