[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 는 다음을 수행한다.

  1. create-server 함수로 서버를 생성한다.
  2. proxy-handler 함수로 Servlet를 생성한다.
  3. 서버의 setHandler 메소드를 이용하여 핸들러를 추가한다.
  4. 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 같은 것들은 존재하지 않는다.

Date: 2022-01-03 Mon 00:00

Author: 남영환

Created: 2024-04-16 Tue 10:05

Emacs 27.2 (Org mode 9.4.4)

Validate