- Learn about Wiki
- Lectures
- Study
- Tips
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을 사용할 때의 장점
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 요청은 클로져 맵으로 나타낸다. 항상 존재하는 표준키도 있지만, 요청은 미들웨어에 의해 사용자 키를 갖기도 한다.
표준키들:
응답 맵은 핸들러에 의해 만들어지는데, 3개의 키를 갖는다.
미들웨어는 핸들러에 추가적인 기능을 더하는 고계함수이다. 미들웨어 함수의 첫 인자는 핸들러이고, 리턴 값은 신규 핸들러 함수이다.
여기 간단한 예제가 있다.
(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에 하나의 키만 있다.
이 미들웨어가 handler에 적용되면, 3개의 새로운 키가 요청 맵에 추가된다.
예를 들어 다음과 같은 요청을 받았다면
{: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를 설정하는 것과 같은 방식으로 다름 추가적인 속성을 설정할 수 있다.
예들들어, 한 시간후에 파기되는 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은 기본적으로 세션 데이타를 메모리에 저장하지만, :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))