Ring - 정리중

Ring은 클로저 웹 개발을 위한 Spec과 라이브러리다. Ring이 라이브러리를 제공하긴 하지만 중요한 것은 Spec이다. 지난 시간에 Hello World 예제를 만들 때 요청과 응답을 클로저의 맵으로 만들었다. 요청 맵에 :method:path 같은 키가 있었고 응답 맵은 :status, :headers, :body와 같은 키를 가지고 있었다. 이런 요청과 응답을 맵 형식으로 정의 한 것이 Ring Spec이다. 많은 클로저 웹 관련 라이브러리가 Ring Spec을 따르고 있기 때문에 기본 적인 Ring Spec을 알아야 웹 라이브러리를 쉽게 다룰 수 있다.

요청과 응답

Ring은 요청과 응답 맵에 대한 키를 정의 하고 있다. 예를 들면 요청 맵에는 :request-method에 키워드 형식으로 요청 메서드가 들어있고 응답 맵의 상태 코드는 :status 키에 값으로 표현된다라는 식이다. Spec은 여기에 정의되어 있고 요청 맵과 응답 맵의 예는 다음과 같다.

요청 맵의 예

{:ssl-client-cert nil
 :protocol "HTTP/1.1"
 :remote-addr "0:0:0:0:0:0:0:1"
 :headers {"cookie" "JSESSIONID=QOm1Y9_QqqCFdMCkhkJHqzjK6jGYO99yEpG0kKFF; XSRF-TOKEN=Jd3xsCJYWB; ring-session=3feda23c-c493-4365-b7c6-821350460a34"
           "cache-control" "max-age=0"
           "accept" "text/htmlapplication/xhtml+xmlapplication/xml;q=0.9image/webp*/*;q=0.8"
           "upgrade-insecure-requests" "1"
           "connection" "keep-alive"
           "user-agent" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML like Gecko) Chrome/51.0.2704.103 Safari/537.36"
           "host" "localhost:3000"
           "accept-encoding"
           "gzip deflate sdch"
           "accept-language" "ko-KRko;q=0.8en-US;q=0.6en;q=0.4it;q=0.2"}
 :server-port 3000
 :content-length nil
 :content-type nil
 :character-encoding nil
 :uri "/"
 :server-name "localhost"
 :query-string nil
 :body #object[org.eclipse.jetty.server.HttpInputOverHTTP 0x5247e9b6 "HttpInputOverHTTP@5247e9b6"]
 :scheme :http
 :request-method :get}

요청 맵은 요청에 대한 다양한 값들이 있다. 대부분 서블릿 Request에 있는 내용을 거의 그대로 가지고 있다. 웹 서버 어플리케이션은 보통 요청에 따라 여러가지 다양한 동작을 하도록 만들기 때문에 요청 맵에서 :uri, :query-string, :body, :request-method 같은 키를 자주 사용한다.

응답 맵의 예

{:status 200
 :headers {"Content-Type" "text/html"}
 :body "Hello World"}

응답 맵은 요청 맵 보다 단순하다. :status, :headers, :body 키를 정의 하고 있다. :body의 값은 문자열, 시퀀스, 파일, InputStream이 될 수 있다. :status:headers는 응답에 필수로 포함해야한다.

핸들러 함수

동기 핸들러 함수

Ring은 요청을 처리해 응답을 주는 함수에 대한 형식도 정의하고 있다. 이 함수를 Ring은 handler라고 부른다. 핸들러는 단순하게 요청 맵을 인자로 받고 응답 맵을 리턴하는 함수다. 아래는 접속 아이피 주소를 text/html 형식으로 출력하는 핸들러 함수다.

(defn sample-handler [request]
  {:status 200
   :headers {"Content-Type" "text/html"}
   :body (:remote-addr request)})

비동기 핸들러 함수

Ring은 비동기 핸들러 함수에 대한 형식도 정의하고 있다. 비동기 핸들러 함수는 아래와 같이 생겼다.

(defn sample-async-handler [request respond raise]
  (respond {:status 200
            :headers {"Content-Type" "text/html"}
            :body (:remote-addr request)}))

비동기 핸들러 함수는 request 외에 respondraise 함수를 받는다. respond함수는 응답을 보내는 함수이고 raise 함수는 예외를 보내는 함수다. 비동기 핸들러는 핸들러 함수가 종료되어도 응답이 전달되지 않고 respondraise함수를 불러야 응답이 전달된다. (글을 작성하는 시점에 비동기 핸들러 함수는 ring에서 만든 jetty 어댑터외에 아직 지원하는 어댑터가 없다.)

미들웨어

Ring은 핸들러 함수 이전과 이후에 공통 로직을 분리해서 재활용 할 수 있도록 미들웨어라는 Spec을 정의 했다. Ring 미들웨어는 로깅, 응답 변환, 요청 변환, 인증, 예외 처리, 라우팅등 여러가지 공통 로직들을 만드는데 사용할 수 있다. 그리고 이미 만들어 놓은 많은 미들웨어가 있기 때문에 가져다 사용할 수 있다.

미들웨어는 핸들러 함수를 인자로 받아서 다시 핸들러 함수를 리턴하는 함수다.

(defn wrap-bypass-middleware [handler]
  (fn [request]
    (handler request)))

위에 예제는 핸들러 함수를 받아서 핸들러 함수를 실행하는 핸들러 함수를 리턴하는 미들웨어다. 결국 아무것도 하지 않는 미들웨어다. 사용은 아래와 같이 하면 된다. 보통 미들웨어의 이름은 warp-으로 시작한다.

(defn sample-handler [request]
  {:status 200
   :headers {"Content-Type" "text/html"}
   :body (:remote-addr request)})

(def app (wrap-bypass-middleware sample-handler))

아래는 수행시간을 포함한 요청 맵을 로그에 출력하는 뭔가 일을 하는 미들웨어 예제다.

(defn wrap-processing-time [handler]
  (fn [request]
    (let [start-time (System/currentTimeMillis)
          response (handler request)]
      (log/info (assoc request :processing-time (- (System/currentTimeMillis) start-time)))
      response)))

(def app (wrap-processing-time sample-handler))

만약 미들웨어를 여러개 적용하고 싶다면 연속적으로 미들웨어를 적용하면 된다.

(def app 
  (-> sample-handler
      wrap-processing-time
      warp-access-log
      wrap-not-found))

미들웨어는 중첩된 함수이기 때문에 미들웨어 적용 순서가 다음과 같이 풀어보면 실행순서를 어렵지 않게 이해할 수 있다.

(defn hello-world-handler [request]
  (log/info "hello world handler")
  {:status 200 :headers {} :body "Hello world"})

(defn wrap-middleware1 [handler]
  (fn [request]
    (log/info "pre handler - middleware1")
    (let [response (handler request)]
      (log/info "post handler - middleware1")
      response)))

(defn wrap-middleware2 [handler]
  (fn [request]
    (log/info "pre handler - middleware2")
    (let [response (handler request)]
      (log/info "post handler - middleware2")
      response)))

(def app (-> hello-world-handler
           wrap-middleware1
           wrap-middleware2))

위와 같이 hello-world-handler 핸들러에 wrap-middleware1wrap-middleware2를 순서대로 적용한 핸들러를 실행하면 아래와 같은 순서로 로그가 출력 된다.

pre handler - middleware2
pre handler - middleware1
hello world handler
post handler - middleware1
post handler - middleware2

핸들러를 처리하기 전에 실행되는 로직은 가장 마지막에 적용한 미들웨어 부터 적용되고 핸들러를 처리한 후에 실행되는 로직은 가장 먼저 적용한 미들웨어 부터 실행되는 것을 볼 수 있다.

어댑터

Ring에서 HTTP 요청과 응답을 어떻게 맵으로 표현하고 또 HTTP 요청을 처리해서 응답을 생성하는 함수가 어떻게 생겼는지 알아봤다. Ring은 서버 라이브러리도 아니고 서버를 어떻게 구현해야하는지에 대한 Spec은 없다. 그래서 Ring을 사용하려면 Ring 핸들러 함수를 실행해줄 서버 구현체가 필요하다. 이 구현체를 Ring 어댑터라고 부른다. 다행이도 Ring은 표준 뿐만 아니라 라이브러리 를 제공하고 있는데 그 중에 Jetty 서버로 Ring 어댑터를 구현한 ring-jetty-adapter라는 라이브러리를 제공한다. 하지만 Netty, Undertow, Grizzly, Tomcat, Nginx등 다양한 Ring 어댑터가 있기 때문에 꼭 Ring에서 제공하는 ring-jetty-adapter를 사용하지 않아도 된다.

지난 시간에 만든 Hello World는 Ring과 비슷하게 요청 맵과 응답 맵을 정의하고 어댑터와 비슷한 run-jetty 함수를 만들었다. 하지만 몇가지 Ring Spec과 맞지 않는 부분이있다. 먼저 build-request-map은 요청 중에서 :method:path만 처리할 수 있다. 또 run-jetty 함수는 두번째 인자로 서버 옵션을 받을 수 있어야 Ring 어댑터 Spec에 맞다.

그럼 Ring Spec에 맞게 고쳐보자. 아래는 지난 시간에 만든 webapp.server 코드다.

(ns webapp.server
  (:import [org.eclipse.jetty.server Server ServerConnector]
           org.eclipse.jetty.server.handler.AbstractHandler))

(defn- build-request-map [^HttpServletRequest request]
  {:method (keyword (lower-case (.getMethod request)))
   :path (str (.getPathInfo request) "?"
           (.getQueryString request))})

(defn- new-handler [handler]
  (proxy [AbstractHandler] []
    (handle [target base-request request ^HttpServletResponse response]
      (let [request-map (build-request-map request)
            {:keys [status body]} (handler request-map)]
        (.setStatus response status)
        (with-open [writer (.getWriter response)]
          (.print writer body))))))

(defn run-jetty [handler]
  (let [^Server server (Server.)
        ^ServerConnector connector (doto (ServerConnector. server)
                                     (.setPort 8080))]
    (doto server
      (.addConnector connector)
      (.setHandler (new-handler handler))
      (.start)
      (.join))))

results matching ""

    No results matching ""