User Tools

Site Tools


lecture:ring:docs

소개

Leiningen으로 새 프로젝트를 생성한다.

$ lein new hello-world
$ cd hello-world

project.clj 파일의 dependencies에 ring-core와 ring-jetty-adapter를 추가한다.

(defproject hello-world "1.0.0-SNAPSHOT"
  :description "FIXME: write"
  :dependencies [[org.clojure/clojure "1.4.0"]
                 [ring/ring-core "1.1.8"]
                 [ring/ring-jetty-adapter "1.1.8"]])

다음으로 src/hello_world/core.clj 파일을 열어, 간단한 핸들러를 작성한다.

(ns hello-world.core)
 
(defn handler [request]
  {:status 200
   :headers {"Content-Type" "text/html"}
   :body "Hello World"})

이제 핸들러를 어댑터에 연결하면 된다. Leiningen으로 REPL을 실행한다.

$ lein repl

REPL에서 위의 핸들러로 Jetty 어댑터를 실행한다.

=> (use 'ring.adapter.jetty)
=> (use 'hello-world.core)
=> (run-jetty handler {:port 3000})

이제, http://localhost:3000으로 브라우져로 접속하면 된다.

왜 링인가?

Ring을 사용할 때의 장점

  • 클로져의 함수와 맵으로 웹 어플리케이션 작성이 가능하다.
  • 자동으로 재로딩되는 개발 서버에서 웹 어플리케이션을 구동할 수 있다.
  • 웹 어플리케이션을 자바 서블릿으로 컴파일한다.
  • 웹 어플리케이션을 자바 war 파일로 패키징한다.
  • 많은 기작성된 미들웨어를 사용할 수 있다.
  • Amazon Elastic Beanstalk이나 Heroku 같은 클라우드 환경에 배포할 수 있다.

Ring은 클로져로 웹 어플리케이션을 작성하는 현재 사실상의 표준이다. Compojure나 Moustache, Noir 같은 고수준 프레임워크들은 Ring을 공통 기반으로 하고 있다.

Ring이 저수준 인터페이스를 제공하지만, Ring이 어떻게 동작하는지 이해하는 것이 고수준 인터페이스를 사용할 때 도움이 된다. Ring에 대한 기본적인 이해없이는, 미들웨어 작성을 할 수가 없고, 디버깅하는 것도 매우 어렵다.

개념

Ring으로 개발되는 웹 어플리케이션은 다음 4개의 구성요소를 갖는다.

  • 핸들러
  • 요청
  • 응답
  • 미들웨어

핸들러

핸들러는 웹 어플리케니션을 정의하는 함수이다. 핸들러는 HTTP 요청을 나타내는 클로져 맵을 인자로 받고, HTTP 응답을 나타내는 클로져 맵을 리턴한다.

다음 예제를 보자.

(defn what-is-my-ip [request]
  {:status 200
   :headers {"Content-Type" "text/plain"}
   :body (:remote-addr request)})

이 함수는 맵을 리턴하는데, Ring은 이것을 HTTP 응답으로 바꾼다. 응답은 웹 어플리케이션을 접근하는데 사용된 IP 주소를 담은 평이한 TEXT 파일로 리턴된다.

핸들러는 다양한 방법을 거쳐 웹 어플리케이션으로 전환될 수 있다. 다음 섹션에서 이를 설명한다.

요청

HTTP 요청은 클로져 맵으로 나타낸다. 항상 존재하는 표준키도 있지만, 요청은 미들웨어에 의해 사용자 키를 갖기도 한다.

표준키들:

  • :server-port : 요청이 처리되는 포트
  • :server-name : 서버 이름, 혹은 서버 IP 주소
  • :remote-addr : 클라이언트, 혹은 요청이 보내진 최종 프록시의 IP 주소
  • :uri : 요청 URI (도메인 이름 뒤의 전체 경로)
  • :query-string : 쿼리 스트링
  • :scheme : 전송 프로토콜 :http 혹은 :https
  • :request-method : HTTP 요청 메소드 :get, :head, :options, :put, :post, :delete.
  • :content-type : 요청 바디의 MIME type
  • :content-length : 요청 바디의 바이트 길이.
  • :character-encoding : 요청 바디에서 사용되는 문자 인코딩.
  • :headers : 해당 헤더 값 스트링에 대한 소문자 헤더 이름 스트링의 클로져 맵.
  • :body : 요청 바디에 대한 InputStream.

응답

응답 맵은 핸들러에 의해 만들어지는데, 3개의 키를 갖는다.

  • :status : HTTP 상태 코드. (200, 302, 404 등)
  • :headers : HTTP 헤더 이름에 대한 헤더 값의 클로져 맵. 이 값은 스트링일 경우 하나의 이름/값 헤더가 HTTP 응답으로 보내지고, 스트링 컬렉션인 경우 이름/값 헤더가 각 값에 따라 보내진다.
  • :body : 응답 바디가 응답 상태 코드에 적절하다면, 응답 바디를 나타낸다. 바디는 다음 4가지 타입중 하나일 수 있다.
    • String : 바디가 클라이언트에 바로 보내진다.
    • ISeq : 시퀀스의 각 요소가 클라이언트에 스트링으로 보내진다.
    • File : 참조된 파일의 콘텐츠가 클라이언트에 보내진다.
    • InputStream : 스트림의 콘텐츠가 파일에 보내진다. 스트림이 비게 되면 스트림은 닫힌다.

미들웨어

미들웨어는 핸들러에 추가적인 기능을 더하는 고계함수이다. 미들웨어 함수의 첫 인자는 핸들러이고, 리턴 값은 신규 핸들러 함수이다.

여기 간단한 예제가 있다.

(defn wrap-content-type [handler content-type]
  (fn [request]
    (let [response (handler request)]
      (assoc-in response [:headers "Content-Type"] content-type))))

이 미들웨어 함수는 핸들러에 의해 생성된 모든 응답에 “Content-Type” 헤더를 더한다.

이 미들웨어를 핸들러에 적용하려면 다음과 같이 한다.

(def app
  (wrap-content-type handler "text/html"))

app은 새로운 핸들러가 되는데, 기존 handler 핸들러에 wrap-content-type 미들웨어가 적용되는 것이다.

스레딩 매크로 (→)는 미들웨어 체인을 형성한다.

(def app
  (-> handler
      (wrap-content-type "text/html")
      (wrap-keyword-params)
      (wrap-params)))

미들웨어는 Ring에서 자주 사용되며, raw HTTP 요청의 처리 이상의 많은 기능을 제공하는데 사용된다. 파라미터, 세션, 그리고 파일 업로딩은 모두 Ring 표준 라이브러리의 미들웨어로 처리된다.

응답 만들기

Ring 응답을 직접 만들 수 있는데, ring.util.response 이름공간은 이런 작업을 쉽게 하는 많은 유용한 함수들이 있다.

response 함수는 기본 “200 OK” 응답을 만든다:

(response "Hello World")
 
=> {:status 200
    :headers {}
    :body "Hello World"}

content-type 함수를 사용하면 추가적인 헤더와 다른 콤포넌트에 추가되는 기본 응답을 변경할 수 있다.

(-> (response "Hello World")
    (content-type "text/plain"))
 
=> {:status 200
    :headers {"Content-Type" "text/plain"}
    :body "Hello World"}

리다이렉트를 만드는 특수 함수도 있다.

(redirect "http://example.com")
 
=> {:status 302
   :headers {"Location" "http://example.com"}
   :body ""}

파일이나 리소스를 리턴할 수도 있다.

(file-response "readme.html" {:root "public"})
 
=> {:status 200
    :headers {}
    :body (io/file "public/readme.html")}
(resource-response "readme.html" {:root "public"})
 
=> {:status 200
    :headers {}
    :body (io/input-stream (io/resource "public/readme.html"))}

더 많은 정보는 ring.util.response API 문서를 보라.

정적 리소스

웹 어플리케이션은 종종 이미지나 스타일쉬트 같은 정적 콘텐츠를 사용한다. Ring은 이를 위해 2개의 미들웨어 함수를 제공한다.

하나는 wrap-file인데, 로컬 파일시스템상의 디렉토리에서 정적 콘텐츠를 서비스한다.

(use 'ring.middleware.file)
(def app
  (wrap-file your-handler "/var/www/public"))

다른 하나는 wrap-resource인데, JVM 클래스패스에서 콘텐츠를 서비스한다.

(use 'ring.middleware.resource)
(def app
  (wrap-resource your-handler "public"))

만일 Leiningen이나 Cake같은 클로져 빌드 툴을 사용한다면, 프로젝트의 비 소스파일 리소스를 “resources” 디렉토리에 넣어두라. 이 디렉토리의 파일들은 자동으로 jar나 war 파일에 포함된다.

위 예제에서는 “resources/public” 디렉토리의 파일들이 정적 파일로 서비스될 것이다.

때로는 wrap-file과 wrap-resource를 wrap-file-info 미들웨어과 결합하고 싶을 것이다.

(use 'ring.middleware.resource
     'ring.middleware.file-info)
 
(def app
  (-> your-handler
      (wrap-resource "public")
      (wrap-file-info)))

wrap-file-info 미들웨어는 파일의 수정 날짜와 확장자를 검사하여, Content-Type과 Last-Modified 헤더를 추가한다. 이를 통해 브라우져는 파일의 유형을 파악하고 파일이 브라우져 캐쉬에 있을 경우 재요청하지 않는다.

wrap-file-info 미들웨어는 wrap-resource와 wrap-file 함수를 항상 랩핑해야 함을 기억하라.

콘텐트 유형

wrap-content-type 미들웨어는 URI의 파일 확장자에 기반하여 Content-Type 헤더를 더한다.

(use 'ring.middleware.content-type)
 
(def app
  (wrap-content-type your-handler))

사용자가 스타일쉬트에 접근한다면

  http://example.com/style/screen.css
  

content-type 미들웨어는 다음 헤더를 추가한다.

  Content-Type: text/css
  

ring-core/src/ring/util/mime_types.clj에서 기본 콘텐트 유형 매핑을 볼 수 있다.

:mime-type 옵션을 사용하여 사용자 mime-type를 추가할 수 있다.

(use 'ring.middleware.content-type)
 
(def app
  (wrap-content-type
   your-handler
   {:mime-types {"foo" "text/x-foo"}}))

파라미터

URL에 인코드된 파라미터들은 브라우져가 웹어플리케이션에 값을 전달하는 기본적인 방식이다. URL 인코드 파라미터들은 사용자가 FORM을 제출할 때 전송되며, 보통 페이지네이션(pagenation)에 이용된다.

ring은 저수준 인터페이스이기 때문에, 해당 미들웨어를 적용해야 파라미터를 지원된다.

(use 'ring.middleware.params)
(def app
  (wrap-params your-handler))

wrap-params 미들웨어는 query 스트링이나 HTTP request Body의 URL 인코드 파라미터를 지원해준다.

하지만 파일 업로드를 위해서는 wrap-multipart-params 미들웨어를 사용해야 한다. 자세한 내용은 파일 업로드를 보라.

wrap-params 함수는 두번째 인수로 options라는 map을 추가로 받을 수 있는데, 현재는 이 map에 하나의 키만 있다.

  • :encoding - 파라미터의 문자 인코딩. 디폴트로는 요청(request)에서의 문자 인코딩인데, 이것이 정해져 있지 않으며 UTF-8이다.

이 미들웨어가 handler에 적용되면, 3개의 새로운 키가 요청 맵에 추가된다.

  • :query-params - query 스트링의 파라미터 맵
  • :form-params - 제출된 폼(form)의 파라미터 맵
  • :params - 모든 파라미터를 합친 맵 (위 2 파라미터를 합친 것이다)

예를 들어 다음과 같은 요청을 받았다면

{:http-method :get
 :uri "/search"
 :query-string "q=clojure"}

wrap-params 미들웨어는 요청 맵을 다음과 같이 수정한다.

{:http-method :get
 :uri "/search"
 :query-string "q=clojure"
 :query-params {"q" "clojure"}
 :form-params {}
 :params {"q" "clojure"}}

보통은 params 키만 사용해도 되지만, 파라미터가 query 스트링에서 온 것인지 아니면 post 메소드로 제출된 폼(form)에서 온 것인지 구분할 필요가 있을 때는 :query-params나 :form-params 키를 사용하면 된다.

파라미터 맵의 키는 스트링이고, 값(value)은 만약 URL 인코딩 파라미터에 그 키에 값이 하나만 있으면 그냥 스트링이지만, 값이 2개 이상이면 벡터이다.

예를 들어 URL이 다음과 같다면

http://example.com/demo?x=hello

파라미터 맵은 다음과 같이 된다.

{"x" "hello"}

하지만 아래처럼 URL 인코딩 파라미터에 같은 이름의 파라미터가 2개 있다면(아래에서는 “x”라는 이름의 파라미터)

http://example.com/demo?x=hello&x=world

파라미터 맵은 다음과 같이 벡터로 된다.

{"x" ["hello", "world"]}

쿠키

Ring 핸들러에 쿠키 기능을 추가하고 싶다면 wrap-cookies 미들웨어로 감싸주어야 한다.

(use 'ring.middleware.cookies)
(def app
  (wrap-cookies your-handler))

이 미들웨어는 요청맵에 :cookies 키를 추가하는데, 그 값은 다음과 같은 식의 맵이다.

{"username" {:value "alice"}}

쿠키를 설정하기위해서는 응답맵에 :cookies 키를 추가해야 한다.

{:status 200
 :headers {}
 :cookies {"username" {:value "alice"}}
 :body "Setting a cookie."}

위에서처럼 “username”이라는 쿠키를 설정하고 그 값으로 alice를 설정할 수 있다. “username” 쿠키에 :value를 설정하는 것과 같은 방식으로 다름 추가적인 속성을 설정할 수 있다.

  • :domain - 쿠키를 특정 도메인에만 국한한다.
  • :path - 쿠키를 특정 path에만 국한한다.
  • :secure - 쿠키를 HTTPS URL인 경우에만 제한한다.
  • :http-only - 쿠키를 HTTP에만 제한한다(자바스크립트를 통해 접근 불가)
  • :max-age - 지정된 시간(초 단위)가 지나면 쿠키 파기.
  • :expires - 쿠키가 파기되는 날짜와 시간을 정함.

예들들어, 한 시간후에 파기되는 secure 쿠키는 다음과 같다.

{"secret" {:value "foobar", :secure true, :max-age 3600}}

세션

Ring에서 세션은 약간 다른데, 왜냐면 Ring은 가능한 한 함수형 스타일로 처리하기 때문이다.

세션 데이타는 요청맵에 :session 키로 전달된다. 다음 예제는 세션에서 현재 유저를 프린트한다.

(use 'ring.middleware.session
     'ring.util.response)
 
(defn handler [{session :session}]
  (response (str "Hello " (:username session))))
 
(def app
  (wrap-session handler))

세션 데이타를 바꾸려면, 응답맵에 :session 키를 추가하고 바꾸려는 데이타를 넣으면 된다. 다음 예제는 현재 세션에서 현재 페이지에 접근한 횟수를 넣는다.

(defn handler [{session :session}]
  (let [count   (:count session 0)
        session (assoc session :count (inc count))]
    (-> (response (str "You accessed this page " count " times."))
        (assoc :session session))))

세션을 전부 업애려면, 응답맵의 :session 키에 nil을 설정한다.

(defn handler [request]
  (-> (response "Session deleted.")
      (assoc :session nil)))

브라우저에서 세션 쿠키가 존속되는 시간을 지정하려면, :cookie-attrs 옵션을 사용하여 세션 쿠키의 속성을 변경할 수 있다.

(def app
  (wrap-session handler {:cookie-attrs {:max-age 3600}}))

위 코드의 경우 쿠키의 최대 존속 기간은 3600초, 즉 1시간이다.

세션 쿠키가 HTTPS만 되도록 하려면 다음과 같이 한다.

(def app
  (wrap-session handler {:cookie-attrs {:secure true}}))

세션 스토어

세션 데이타는 세션 스토어에 저장된다. Ring에는 2개의 스토어가 있다.

  • ring.middleware.session.memory/memory-store - 세션이 메모리에 저장된다.
  • ring.middleware.session.cookie/cookie-store - 암호화된 세션이 쿠키에 저장된다.

Ring은 기본적으로 세션 데이타를 메모리에 저장하지만, :store 옵션으로 변경될 수 있다.

(use 'ring.middleware.session.cookie)
 
(def app
  (wrap-session handler {:store (cookie-store {:key "a 16-byte secret"})})

ring.middleware.session.store/SessionStore 프로토콜을 구현하여 사용자 세션 스토어를 만들 수 있다.

(use 'ring.middleware.session.store)
 
(deftype CustomStore []
  SessionStore
  (read-session [_ key]
    (read-data key))
  (write-session [_ key data]
    (let [key (or key (generate-new-random-key))]
      (save-data key data)
      key))
  (delete-session [_ key]
    (delete-data key)
    nil))

파일 업로드

인터랙티브 개발

API RING

써드파티 라이브러리

벤치마크

예제

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