[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 같은 것들은 존재하지 않는다.