Duct๋ Clojure ํ๋ก๊ทธ๋๋ฐ ์ธ์ด๋ก ์๋ฒ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ค๊ธฐ ์ํ data-driven ํ๋ ์์ํฌ์ ๋๋ค. ์ด ๊ฐ์ด๋๋ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์์ ๋ก Duct ์ฌ์ฉ๋ฒ์ ์์ธํ ์ค๋ช ํ๊ธฐ ์ํด ์ฐ์ต๋๋ค.
์ด ๊ฐ์ด๋๋ฅผ ์ ๋๋ก ์ฝ์ผ๋ ค๋ฉด Leiningen์ด ์ค์น๋์ด ์๊ณ Clojure ์ค๋ฌด ์ง์์ด ์์ด์ผํฉ๋๋ค. ๊ผญ ํ์ํ ๊ฒ์ ์๋์ง๋ง Ring์ ๊ธฐ์ด์ ์ธ ๋ถ๋ถ์ ์๊ณ ์์ผ๋ฉด ์ข์ต๋๋ค.
Duct Leiningen ํ ํ๋ฆฟ์ผ๋ก ๋ฐ๋ก ์์ํด ๋ณผ ์ ์์ต๋๋ค. Duct๋ก ๋ค์ํ ์๋ฒ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ค ์ ์์ง๋ง ์ด ๊ฐ์ด๋์ ๋ชฉ์ ์ ์ํด SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ๋ ์น ์๋น์ค๋ฅผ ๋ง๋ค์ด ๋ณด๊ฒ ์ต๋๋ค.
์ ์์ ๋ค์๊ณผ ๊ฐ์ด ์คํํฉ๋๋ค:
$ lein new duct todo +api +ataraxy +sqlite
์ด๋ฐ ๊ฒฐ๊ณผ๊ฐ ๋์ต๋๋ค:
Generating a new Duct project named todo... Run 'lein duct setup' in the project directory to create local config files.
+๋ก ์์ํ๋ ํ๋ผ๋ฏธํฐ๋ ํ๋กํ ํํธ์
๋๋ค. ์ ์์ ๋ ์น ์๋น์ค (+api)์ Ataraxy ๋ผ์ฐํ
๋ผ์ด๋ธ๋ฌ๋ฆฌ
(+ataraxy), SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค (+sqlite)๋ฅผ ์ฌ์ฉํ๋ ํ๋ก์ ํธ๋ฅผ ํ
ํ๋ฆฟ์ ๋ง๋๋ ์์ ์
๋๋ค.
์ฌ์ฉํ ์ ์๋ ํ๋กํ ํํธ๋ฅผ ๋ชจ๋ ๋ณด๋ ค๋ฉด ์๋ ๋ช ๋ น์ด๋ฅผ ์คํํฉ๋๋ค:
$ lein new :show duct
์ด์ ์กฐ๊ธ ์ ์ ๋ง๋ todo ํ๋ก์ ํธ ๋๋ ํฐ๋ฆฌ๋ก ๋ค์ด๊ฐ ๋ด
์๋ค:
$ cd todo
๊ทธ๋ฆฌ๊ณ ๋ก์ปฌ ์ ์ ์ ์คํํฉ๋๋ค:
$ lein duct setup
์ ์ ์ ์คํํ๊ณ ๋๋ฉด ์์ค ์ปจํธ๋กค์๋ ์ ์ธ๋์ด ์๋ ํ์ผ 4๊ฐ๊ฐ ์๊น๋๋ค:
Created profiles.clj Created .dir-locals.el Created dev/resources/local.edn Created dev/src/local.clj
์ด ํ์ผ๋ค์ .gitignore์ ์ถ๊ฐ๋์ด ์๊ธฐ ๋๋ฌธ์ Git์ ์ฌ์ฉํ๋ฉด ๋ฐ๋ก ํด์ค ์ผ์ด ์์ต๋๋ค. ํ์ง๋ง
๋ค๋ฅธ ์์ค ์ปจํธ๋กค์ ์ฌ์ฉํ๋ค๋ฉด ์ด ํ์ผ๋ค์ด ์์ค ์ปจํธ๋กค์ ๊ด๋ฆฌ๋์ง ์๋๋ก ์๋์ผ๋ก ์ฒ๋ฆฌํด์ค์ผ ํฉ๋๋ค.
Duct๋ REPL์ ์ค์ฌ์ผ๋ก ๊ฐ๋ฐ ํ๋๋ก ๋์ด ์์ต๋๋ค. ๊ทธ๋์ Cursive๋ Emacs์ CIDER, Vim์ fireplace.vim, Atom์ Proto REPL๊ฐ์ ์๋ํฐ REPL ํตํฉ ํ๊ฒฝ์ ์ฌ์ฉํ๋ ๊ฒ์ ์ถ์ฒํฉ๋๋ค. ํ์ง๋ง ์ด ๊ฐ์ด๋๋ ์๋ํฐ ํตํฉ ์์ด ์ปค๋งจ๋ ๋ผ์ธ์์ ์ง์ ์คํํด๋ณผ ์ ์๋๋ก ๋์ด ์์ต๋๋ค.
REPL์ ์์ํฉ๋๋ค:
$ lein repl
๋จผ์ ๊ฐ๋ฐ ํ๊ฒฝ์ ๋ก๋ํ๊ธฐ ์ํด ํ๋กฌํํธ์ ๋ค์๊ณผ ๊ฐ์ด ์ ๋ ฅํฉ๋๋ค:
user=> (dev)
:loaded
dev=>๊ฐ๋ฐ ํ๊ฒฝ์ ์๋ฌ๊ฐ ์๋ ๊ฒฝ์ฐ REPL์ด ์คํ๋์ง ์์ ์ ์๊ธฐ ๋๋ฌธ์ ๊ฐ๋ฐ ํ๊ฒฝ์ ์๋์ผ๋ก ๋ก๋๋์ง ์์ต๋๋ค.
dev ๋ค์์คํ์ด์ค๋ก ๋ฐ๋๋ฉด ์ ํ๋ฆฌ์ผ์ด์
์ ์คํํด๋ณผ ์ ์์ต๋๋ค:
dev=> (go)
:duct.server.http.jetty/starting-server {:port 3000}
:initiated์น ์๋ฒ๋ 3000๋ฒ ํฌํธ๋ก ์คํ๋ฉ๋๋ค. HTTP ๋ฆฌํ์คํธ๋ฅผ ๋ณด๋ด ์ ์คํ๋์๋์ง ํ์ธํด๋ด ์๋ค. ๋ณดํต ์ปค๋งจ๋ ๋ผ์ธ์์ curl์ด๋ wget์ผ๋ก ์น ์๋น์ค๋ฅผ ํ ์คํธ ํ์ง๋ง ์ ๋ HTTPie๋ฅผ ๋ ์ข์ํฉ๋๋ค:
$ http :3000
HTTP/1.1 404 Not Found
Content-Length: 21
Content-Type: application/json; charset=utf-8
Date: Wed, 06 Dec 2017 11:27:22 GMT
Server: Jetty(9.2.21.v20170120)
{
"error": "not-found"
}
"not found" ์๋ต์ ๋ฐ์์ต๋๋ค. ์์ง ์๋ฌด ๋ผ์ฐํฐ๋ ์ถ๊ฐํ์ง ์์๊ธฐ ๋๋ฌธ์ ์์๋ ๊ฒฐ๊ณผ์ ๋๋ค.
Duct ์ดํ๋ฆฌ์ผ์ด์
์ edn ์ค์ ํ์ผ ์ฃผ์์์ ๋น๋๋ฉ๋๋ค.
Configuration ํ์ผ์ ์ดํ๋ฆฌ์ผ์ด์
์ ๊ตฌ์กฐ์ ๋ํ๋์๋ฅผ ์ ์ํฉ๋๋ค.
์ด ๊ฐ์ด๋์์์ ๋ง๋ ํ๋ก์ ํธ์์, ์ค์ ํ์ผ์ ๋ค์ ์์น์ ์์ต๋๋ค:
resources/todo/config.edn.
Config ํ์ผ์ ์ดํด๋ณด๊ฒ ์ต๋๋ค:
{:duct.core/project-ns todo
:duct.core/environment :production
:duct.module/logging {}
:duct.module.web/api {}
:duct.module/sql {}
:duct.module/ataraxy
{}}์ ์ index ๋ผ์ฐํธ๋ฅผ ์ถ๊ฐํ๋ ๊ฒ์ผ๋ก ์์ํ ์ ์์ํ
๋ฐ,
Ataraxy๊ฐ ์ฌ์ฉํ ๋ผ์ฐํฐ์ด๊ธฐ ๋๋ฌธ์ :duct.module/ataraxy ๋ผ๊ณ ํ ์ค์ ์ถ๊ฐํฉ๋๋ค:
:duct.module/ataraxy
{[:get "/"] [:index]}์ด๊ฒ์ ๋ผ์ฐํธ [:get "/"] ๋ฅผ [:index] ๋ก ์ฐ๊ฒฐํฉ๋๋ค.
Ataraxy ๋ชจ๋์ ์๋์ผ๋ก ์ค์ ์ ์ด๋ฆ๊ณผ ์ผ์นํ๋ Ring ํธ๋ค๋ฌ๋ฅผ ์ฐพ์ ์์ ์ด๋ฃน๋๋ค.
๊ฒฐ๊ณผ ํค๊ฐ :index ์ด๊ธฐ ๋๋ฌธ์, ํธ๋ค๋ฌ ํค๋ :todo.handler/index ๊ฐ ๋ฉ๋๋ค.
์ค์ ์ ๊ทธ ์ด๋ฆ์ ๊ฐ์ง ์ํธ๋ฆฌ๋ฅผ ์ถ๊ฐํด๋ด
์๋ค:
[:duct.handler.static/ok :todo.handler/index]
{:body {:entries "/entries"}}์ด๋ฒ์๋ ๋ฒกํฐ๋ฅผ ํค๋ก ์ฌ์ฉํฉ๋๋ค; Duct์์๋ ์ด๊ฒ์ ๋ณตํฉ (composite key) ๋ผ๊ณ ํฉ๋๋ค.
๋ณตํฉ ํค๋ ๋ณตํฉ ํค์ ์ํ ๋ชจ๋ ํค์๋์ ์์ฑ์ ์์ ๋ฐ์ต๋๋ค;
๋ฒกํฐ์ :duct.handler.static/ok ๊ฐ ํฌํจ๋์ด ์๊ธฐ ๋๋ฌธ์,
์ค์ ์ํธ๋ฆฌ๊ฐ ์ ์ ํธ๋ค๋ฌ๋ฅผ ์์ฑํฉ๋๋ค.
์ด ๋ณ๊ฒฝ์ฌํญ์ ์ดํ๋ฆฌ์ผ์ด์ ์ ์ ์ฉํด ๋ณด๊ฒ ์ต๋๋ค. ๋ ํ๋ก ๋์๊ฐ์ ์คํํด๋ณด์ธ์:
dev=> (reset)
:reloading (todo.main dev user)
:resumed์ด๊ฒ์ ์ค์ ๊ณผ ๋ณ๊ฒฝ๋ ํ์ผ์ ๋ค์ ๋ก๋ํฉ๋๋ค. ์ด์ ๋ ์น ์๋ฒ์ ์์ฒญ์ ๋ณด๋ด, ์์๋ ์๋ต์ ๋ฐ์ ์ ์์ต๋๋ค:
$ http :3000
HTTP/1.1 200 OK
Content-Length: 22
Content-Type: application/json; charset=utf-8
Date: Wed, 06 Dec 2017 13:28:52 GMT
Server: Jetty(9.2.21.v20170120)
{
"entries": "/entries"
}
๋ ๋ง์ ๋์ ๋ผ์ฐํธ๋ฅผ ์ถ๊ฐํ๊ณ ์ถ์ง๋ง, ๊ทธ์ ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง๋ฅผ ์์ฑํด์ผํฉ๋๋ค. Duct๋ Ragtime ์ ์ฌ์ฉํด ๋ง์ด๊ทธ๋ ์ด์ ์ ํ๊ณ , ๊ฐ ๋ง์ด๊ทธ๋ ์ด์ ์ ์ค์ ์ ์ ์๋ฉ๋๋ค.
์ค์ ์ ๋ ๊ฐ์ ํค๋ฅผ ๋ ์ถ๊ฐํฉ๋๋ค.
:duct.migrator/ragtime
{:migrations [#ig/ref :todo.migration/create-entries]}
[:duct.migrator.ragtime/sql :todo.migration/create-entries]
{:up ["CREATE TABLE entries (id INTEGER PRIMARY KEY, content TEXT)"]
:down ["DROP TABLE entries"]}:duct.migrator/ragtime ํค๋ ๋ง์ด๊ทธ๋ ์ด์
์ ์์๋๋ก ๊ฐ์ง๋๋ค.
๊ฐ ๋ง์ด๊ทธ๋ ์ด์
์ ๋ณตํฉํค์์ :duct.migrator.ragtime/sql ์ ํฌํจ์์ผ ์ ์ํ ์ ์์ต๋๋ค.
:up ๊ณผ :down ์ต์
์ ์คํํ SQL์ ๋ฒกํฐ๋ฅผ ๊ฐ์ง๋๋ค;
up์ ๋ง์ด๊ทธ๋ ์ด์
์, down์ ๋กค๋ฐฑ์ ํ๊ฒ ๋ฉ๋๋ค.
๋ง์ด๊ทธ๋ ์ด์
์ ์ํด์ REPL์์ reset ์ ๋ค์ ์คํํฉ๋๋ค:
dev=> (reset)
:reloading ()
:duct.migrator.ragtime/applying :todo.migration/create-entries#b34248fc
:resumed๋ง์ด๊ทธ๋ ์ด์ ์ ์ ์ฉํ ์ดํ์ ์คํค๋ง๋ฅผ ๋ฐ๊พธ๊ธฐ๋ก ํ๋ค๊ณ ๊ฐ์ ํด๋ณด๊ฒ ์ต๋๋ค. ๋ค๋ฅธ ๋ง์ด๊ทธ๋ ์ด์ ์ ์๋ก ์์ฑํด๋ณผ์๋ ์์ง๋ง, ์ฝ๋๊ฐ ์ปค๋ฐ์ด ์๋์๊ฑฐ๋ ํ๋ก๋์ ์ ๋ฐฐํฌํ์ง ์์๊ฒฝ์ฐ ๊ฐ์ง๊ณ ์๋ ๋ง์ด๊ทธ๋ ์ด์ ์ ํธ์งํ๋ ๊ฒ์ด ์ข๋ ํธ๋ฆฌํฉ๋๋ค.
๋ง์ด๊ทธ๋ ์ด์ ์ ๋ณ๊ฒฝํ๊ณ ,``content`` ์ปฌ๋ผ์ ์ด๋ฆ์``description`` ์ผ๋ก ๋ฐ๊ฟ๋ด ์๋ค:
[:duct.migrator.ragtime/sql :todo.migration/create-entries]
{:up ["CREATE TABLE entries (id INTEGER PRIMARY KEY, description TEXT)"]
:down ["DROP TABLE entries"]}๊ทธ๋ฆฌ๊ณ reset:
dev=> (reset)
:reloading ()
:duct.migrator.ragtime/rolling-back :todo.migration/create-entries#b34248fc
:duct.migrator.ragtime/applying :todo.migration/create-entries#5c2bb12a
:resumed์ด์ ๋ฒ์ ์ ๋ง์ด๊ทธ๋ ์ด์ ์ ์๋์ผ๋ก ๋กค๋ฐฑ๋๊ณ ์ ๋ฒ์ ์ ๋ง์ด๊ทธ๋ ์ด์ ์ด ๋์ ์ ์ฉ๋ฉ๋๋ค.
ํ๋ก๋์ ํ๊ฒฝ์์๋ ์ฝ๊ฒ ๋ง์ด๊ทธ๋ ์ด์ ์ ํ ์ ์์ต๋๋ค:
$ lein run :duct/migrator
๊ฐ๋ฐ์์ Heroku๋ฅผ ์ฐ๊ณ ์๋ค๋ฉด, Procfile์ ํตํด ๋ฆด๋ฆฌ์ฆ ๋จ๊ณ์ ์ฝ๊ฒ ์ถ๊ฐํด๋ณผ์ ์์ต๋๋ค.
web: java -jar target/sstandalone.jar release: lein run :duct/migrator
์ด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ
์ด๋ธ์ด ์๊ฒผ์ผ๋ฏ๋ก ์ฟผ๋ฆฌ ๋ผ์ฐํธ๋ฅผ ์์ฑํด์ผํฉ๋๋ค.
duct/handler.sql ๋ผ๊ณ ๋ถ๋ฆฌ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ ๊ฒ์
๋๋ค.
์ด๊ฒ์ project.clj ํ์ผ์ :dependencies ํค์ ์ถ๊ฐ๋ผ์ผ ํฉ๋๋ค:
[duct/handler.sql "0.3.1"]๋ํ๋์๋ ์ด์ ๋ค์๊ณผ ๊ฐ์ด ๋ณด์ผ ๊ฒ์ ๋๋ค :
:dependencies [[org.clojure/clojure "1.9.0-RC1"]
[duct/core "0.6.1"]
[duct/handler.sql "0.3.1"]
[duct/module.logging "0.3.1"]
[duct/module.web "0.6.3"]
[duct/module.ataraxy "0.2.0"]
[duct/module.sql "0.4.2"]
[org.xerial/sqlite-jdbc "3.20.1"]]๋ํ๋์๋ฅผ ์ถ๊ฐํ์ ๋์๋ REPL์ ๋ค์ ์์ํด์ผํ๋ฏ๋ก, ์ผ๋จ REPL์์ ๋น ์ ธ๋์ต๋๋ค.
dev=> (exit)
Bye for now!๊ทธ๋ฆฌ๊ณ ๋ค์ ์์ํฉ๋๋ค:
$ lein repl
๊ทธ๋ฆฌ๊ณ ์ดํ๋ฆฌ์ผ์ด์ ์ ๋ค์ ์คํํฉ๋๋ค:
์ด์ ํ๋ก์ ํธ ์ค์ ์ผ๋ก ๋์๊ฐ์, ์๋ก์ด Ataraxy ๋ผ์ฐํธ๋ฅผ ์ถ๊ฐํ๋ ๊ฒ์ผ๋ก ์์ํด๋ด ์๋ค:
:duct.module/ataraxy
{[:get "/"] [:index]
[:get "/entries"] [:entries/list]}์์ ๋ณธ ๊ฒ๊ณผ ๊ฐ์ด, [:entries/list] ๋ ์ ์ ํ๊ฒ ์ด๋ฆ ๋ถ์ฌ์ง Ring ํธ๋ค๋ฌ์ ์์ ์ด๋ค์ผํฉ๋๋ค.
Ataraxy ๋ชจ๋์ ์ด ํธ๋ค๋ฌ ์ด๋ฆ์ด :todo.handler.entries/list ์ด๊ธฐ๋ฅผ ๊ธฐ๋ํ๊ธฐ ๋๋ฌธ์,
:duct.handler.sql/query ํค์ ํจ๊ป ๊ทธ ์ด๋ฆ์ ์ฌ์ฉํ๊ฒ ๋ฉ๋๋ค:
[:duct.handler.sql/query :todo.handler.entries/list]
{:sql ["SELECT * FROM entries"]}์ผ๋จ ํธ๋ค๋ฌ๊ฐ ์ค์ ์ ์ ์๋๋ฉด, reset ์ ํ ์ ์์ต๋๋ค :
dev=> (reset)
:reloading (todo.main dev user)
:resumed๊ทธ๋ฆฌ๊ณ HTTP ์์ฒญ์ ๋ณด๋ด์ ๋ผ์ฐํธ๋ฅผ ํ์ธํฉ๋๋ค:
$ http :3000/entries HTTP/1.1 200 OK Content-Length: 2 Content-Type: application/json; charset=utf-8 Date: Thu, 07 Dec 2017 10:13:34 GMT Server: Jetty(9.2.21.v20170120) []
์ ํจํ ์๋ต์ด์ง๋ง, ๋น์ด์๋ ์๋ต์
๋๋ค.
entries ํ
์ด๋ธ์ ์๋ฌด ๋ฐ์ดํฐ๋ ๋ฃ์ง ์์๊ธฐ ๋๋ฌธ์ธ ๊ฒ์ ์์ ์์ต๋๋ค.
๋ค์์ผ๋ก๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์
๋ฐ์ดํธ ํ๋ ๋ผ์ฐํธ๋ฅผ ์ถ๊ฐํ๋ ค๊ณ ํฉ๋๋ค.
๋ค์ duct/handler.sql ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ ๊ฒ์ด์ง๋ง,
๋ผ์ฐํธ์ ํธ๋ค๋ฌ๋ ๋ ๋ณต์กํด ์ง ๊ฒ์
๋๋ค.
์ผ๋จ, ์๋ก์ด ๋ผ์ฐํธ์ ๋๋ค:
:duct.module/ataraxy
{[:get "/"] [:index]
[:get "/entries"] [:entries/list]
[:post "/entries" {{:keys [description]} :body-params}]
[:entries/create description]}์๋ก์ด Ataraxy ๋ผ์ฐํธ๋ ์์ฒญ์ ๋ฉ์๋์ URI๋ฅผ ์ผ์น์ํฌ๋ฟ๋ง ์๋๋ผ, ์์ฒญ์ body๋ฅผ ๋์คํธ๋ญ์ฒ๋ง ํ๊ณ todo ์ํธ๋ฆฌ์ ์ค๋ช ๋ ๋ฃ์ ์ ์์ต๋๋ค.
๊ด๋ จ๋ ํธ๋ค๋ฌ๋ฅผ ์์ฑํ ๋, ๊ฒฐ๊ณผ์์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ฌ ์ ์๋ ๋ฐฉ๋ฒ์ด ํ์ํฉ๋๋ค.
Ataraxy๋ ๊ฒฐ๊ณผ๋ฅผ ์์ฒญ ๋งต์ :ataraxy/result ํค์ ๋ฃ์ต๋๋ค.
๊ทธ๋์ ์ ์คํธ๋ฆฌ์ ์ค๋ช
์ ์ฐพ๊ธฐ ์ํด ์์ฒญ์ ๋์คํธ๋ญ์ฒ๋ง ํ ์ ์์ต๋๋ค:
[:duct.handler.sql/insert :todo.handler.entries/create]
{:request {[_ description] :ataraxy/result}
:sql ["INSERT INTO entries (description) VALUES (?)" description]}๊ทธ๋ฆฌ๊ณ reset:
dev=> (reset)
:reloading (todo.main dev user)
:resumed๊ทธ๋ฆฌ๊ณ ํ ์คํธ:
$ http post :3000/entries description="Write Duct guide"
HTTP/1.1 201 Created
Content-Length: 0
Content-Type: application/octet-stream
Date: Thu, 07 Dec 2017 11:29:46 GMT
Server: Jetty(9.2.21.v20170120)
$ http get :3000/entries
HTTP/1.1 200 OK
Content-Length: 43
Content-Type: application/json; charset=utf-8
Date: Thu, 07 Dec 2017 11:29:51 GMT
Server: Jetty(9.2.21.v20170120)
[
{
"description": "Write Duct guide",
"id": 1
}
]
์ด์ ์ธ๋งํ ์ดํ๋ฆฌ์ผ์ด์ ์ ๋ผ๋๊ฐ ์๊ฒผ์ต๋๋ค.
์ด์ ์ํธ๋ฆฌ์ ๋ชฉ๋ก์ GET๊ณผ POST๋ฅผ Todo ์ดํ๋ฆฌ์ผ์ด์ ์ ๋ ๋ ค๋ณผ ์ ์์ง๋ง, DELETE๋ ๋ง๋ค์ด๋ด ์๋ค. ์ด๋ฅผ ์ํด์๋ ๊ฐ ์ํธ๋ฆฌ๊ฐ ๊ณ ์ ํ URI๋ฅผ ๊ฐ์ ธ์ผํฉ๋๋ค.
๋ฆฌ์คํธ ํธ๋ค๋ฌ์ ํ์ดํผํ ์คํธ ์ฐธ์กฐ๋ฅผ ์ถ๊ฐํด๋ด ์๋ค.
[:duct.handler.sql/query :todo.handler.entries/list]
{:sql ["SELECT * FROM entries"]
:hrefs {:href "/entries/{id}"}}:hrefs ์ต์
์ URI templates ์ ์ฌ์ฉํด ์๋ต์ ํ์ดํผํ
์คํธ ์ฐธ์กฐ๋ฅผ ์ถ๊ฐํ ์ ์๊ฒํฉ๋๋ค. reset ์ ํ๋ฉด:
dev=> (reset)
:reloading (todo.main dev user)
:resumed๊ทธ๋ฆฌ๊ณ ํ ์คํธ:
$ http :3000/entries
HTTP/1.1 200 OK
Content-Length: 63
Content-Type: application/json; charset=utf-8
Date: Thu, 07 Dec 2017 21:13:20 GMT
Server: Jetty(9.2.21.v20170120)
[
{
"description": "Write Duct guide",
"href": "/entries/1",
"id": 1
}
]
์ด์ ๊ฐ ๋ฆฌ์คํธ ์ํธ๋ฆฌ์ ์ ํค๊ฐ ์๊ธด ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค. ํฌ๊ฐ์ง ์๋ก์ด Ataraxy ๋ผ์ฐํธ๋ฅผ ์์ฑํด๋ณด๊ฒ ์ต๋๋ค:
:duct.module/ataraxy
{[:get "/"] [:index]
[:get "/entries"] [:entries/list]
[:post "/entries" {{:keys [description]} :body-params}]
[:entries/create description]
[:get "/entries/" id] [:entries/find ^int id]
[:delete "/entries/" id] [:entries/destroy ^int id]}์ด ๋ผ์ฐํธ๋ URI์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์์, ์๋ก์ด ํ์ ์ผ๋ก ๊ฐ์ ํ๋ ๋ฐฉ๋ฒ์ ๋ณด์ฌ์ค๋๋ค.
๋ผ์ฐํธ์๋ ๊ด๋ จ๋ ํธ๋ค๋ฌ๊ฐ ํ์ํฉ๋๋ค. ์์ ๋์จ duct/handler.sql ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ query-one ์ execute ํธ๋ค๋ฌ ํ์ ์ ์ฌ์ฉํด๋ด ๋๋ค:
[:duct.handler.sql/query-one :todo.handler.entries/find]
{:request {[_ id] :ataraxy/result}
:sql ["SELECT * FROM entries WHERE id = ?" id]
:hrefs {:href "/entries/{id}"}}
[:duct.handler.sql/execute :todo.handler.entries/destroy]
{:request {[_ id] :ataraxy/result}
:sql ["DELETE FROM entries WHERE id = ?" id]}๋ํ ์ํธ๋ฆฌ ์์ฑ ๋ผ์ฐํธ๋ฅผ ๊ฐ์ ํ๊ณ , `Location`๋ฅผ ์ ๊ณตํด ๋ฆฌ์์ค๋ฅผ ์์ฑํ ์ ์์ต๋๋ค:
[:duct.handler.sql/insert :todo.handler.entries/create]
{:request {[_ description] :ataraxy/result}
:sql ["INSERT INTO entries (description) VALUES (?)" description]
:location "/entries/{last_insert_rowid}"}`last_insert_rowid`๋ SQLite์์๋ง ์ฌ์ฉํ๋ ๊ฒฐ๊ณผ ์งํฉ ์ปฌ๋ผ์ ๋๋ค. ๋ค๋ฅธ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ์์ฑ๋ row๋ณ ID๋ฅผ ๋ค๋ฅธ ๋ฐฉ์์ผ๋ก ๋ฐํํฉ๋๋ค.
์๋ฃํ์ผ๋ฉด `reset`์ ํฉ๋๋ค :
dev=> (reset)
:reloading ()
:resumed๊ทธ๋ฆฌ๊ณ ํ ์คํธ:
$ http :3000/entries/1
HTTP/1.1 200 OK
Content-Length: 61
Content-Type: application/json; charset=utf-8
Date: Sat, 09 Dec 2017 12:59:05 GMT
Server: Jetty(9.2.21.v20170120)
{
"description": "Write Duct guide",
"href": "/entries/1",
"id": 1
}
$ http delete :3000/entries/1
HTTP/1.1 204 No Content
Content-Type: application/octet-stream
Date: Sat, 09 Dec 2017 12:59:12 GMT
Server: Jetty(9.2.21.v20170120)
$ http :3000/entries/1
HTTP/1.1 404 Not Found
Content-Length: 21
Content-Type: application/json; charset=utf-8
Date: Sat, 09 Dec 2017 12:59:18 GMT
Server: Jetty(9.2.21.v20170120)
{
"error": "not-found"
}
$ http post :3000/entries description="Continue Duct guide"
HTTP/1.1 201 Created
Content-Length: 0
Content-Type: application/octet-stream
Date: Sat, 09 Dec 2017 13:18:46 GMT
Location: http://localhost:3000/entries/1
Server: Jetty(9.2.21.v20170120)
์ง๊ธ๊น์ง ์ค์ ์ ์ฌ์ฉํด์ Duct ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ค์ด ๋ดค์ต๋๋ค. ๋จ์ํ ๊ธฐ๋ฅ์ ๋ง๋ค ๋๋ ์ค์ ๋ง์ผ๋ก ๋ง๋ค ์ ์์ง๋ง ๋๋ถ๋ถ์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฝ๋๋ฅผ ์์ฑํด์ผ ํฉ๋๋ค.
์ค์ ์ ์ฌ์ฉํ ๋ฐ์ดํฐ ๊ธฐ๋ฐ์ ํธ๋ค๋ฌ๋ ์ฅ์ ์ด ์์ง๋ง ๋๋ฌด ๊ณผํ์ง ์๋๋ก ํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค. ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ค ๋ ์ค์ ์ ๊ณจ๊ฒฉ์ผ๋ก ์ฝ๋๋ ๊ทผ์ก๊ณผ ๊ธฐ๊ด์ผ๋ก ์๊ฐํ๋ฉด ์ข์ต๋๋ค.
์ง๊ธ๊น์ง ์ฌ์ฉ์๊ฐ ํ๋ช
์ธ ์ ํ๋ฆฌ์ผ์ด์
์ ๋ง๋ค์์ต๋๋ค. ์ด์ users ํ
์ด๋ธ์ ์ถ๊ฐํด์ ์ฌ์ฉ์๊ฐ ์ฌ๋ฌ๋ช
์ธ
์ ํ๋ฆฌ์ผ์ด์
์ผ๋ก ๋ฐ๊ฟ ๋ด
์๋ค. ๋จผ์ ์ค์ ์ ์ ๋ง์ด๊ทธ๋ ์ด์
์ฐธ์กฐ๋ฅผ ์ถ๊ฐํฉ๋๋ค:
:duct.migrator/ragtime
{:migrations [#ig/ref :todo.migration/create-entries
#ig/ref :todo.migration/create-users]}๊ทธ๋ฆฌ๊ณ ๋ง์ด๊ทธ๋ ์ด์ ์ ๋ง๋ญ๋๋ค:
[:duct.migrator.ragtime/sql :todo.migration/create-users]
{:up ["CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE, password TEXT)"]
:down ["DROP TABLE users"]}์ ๋ง์ด๊ทธ๋ ์ด์
์ ์ ์ฉํ๊ธฐ ์ํด reset์ ์คํํฉ๋๋ค:
dev=> (reset)
:reloading ()
:duct.migrator.ragtime/applying :todo.migration/create-users#66d6b1f8
:resumed์ฌ์ฉ์๋ฅผ ์ ์ฅํ ํ
์ด๋ธ์ด ์๊ฒผ์ผ๋ ์ด์ ์ฌ์ฉ์๋ค์ด ์น ์๋น์ค์์ ๊ฐ์
ํ ์ ๋ฐฉ๋ฒ์ด ํ์ํฉ๋๋ค.
duct/handler.sql ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก ํธ๋ค๋ฌ๋ฅผ ๋ง๋ค ์ ์์ง๋ง ๊ทธ๋ ๊ฒ ํ๋ฉด ๋น๋ฐ๋ฒํธ๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์
๊ทธ๋๋ก ์ ์ฅํ๊ฒ ๋์ด ๋ณด์์ ์ข์ง ์์ต๋๋ค.
๋์ ๋น๋ฐ๋ฒํธ ๋ณด์ ๋ฐฉ์ ์ค ํ๋์ธ key derivation function(๋๋ KDF)๋ฅผ ์ด์ฉํด์ ์ํธํ๋ ๋น๋ฐ๋ฒํธ๋ฅผ ์ ์ฅํ๋๋ก ํธ๋ค๋ฌ ํจ์๋ฅผ ์ง์ ๋ง๋ค์ด ๋ด ์๋ค. ๋จผ์ ์๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ๋ก์ ํธ ๋ํ๋์์ ์ถ๊ฐํฉ๋๋ค:
[buddy/buddy-hashers "1.3.0"]์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ถ๊ฐํ๋ฉด ํค ์ ๋ ํจ์(KDF)๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค. ๋ํ๋์๋ฅผ ์ถ๊ฐํ ํ์ REPL์ ์ข ๋ฃํฉ๋๋ค:
dev=> (exit)
Bye for now!๊ทธ๋ฆฌ๊ณ ๋ค์ ์์ํฉ๋๋ค:
$ lein repl
๋ค์์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์์ํด์ค๋๋ค:
์ด์ ์ฌ์ฉ์๋ฅผ ์์ฑํ๊ธฐ ์ํ Ataraxy ๋ผ์ฐํฐ๋ฅผ ์ถ๊ฐํฉ๋๋ค:
:duct.module/ataraxy
{[:get "/"] [:index]
[:get "/entries"] [:entries/list]
[:post "/entries" {{:keys [description]} :body-params}]
[:entries/create description]
[:get "/entries/" id] [:entries/find ^int id]
[:delete "/entries/" id] [:entries/destroy ^int id]
[:post "/users" {{:keys [email password]} :body-params}]
[:users/create email password]}๊ทธ๋ฆฌ๊ณ ํธ๋ค๋ฌ ์ค์ ์ ์ถ๊ฐํฉ๋๋ค:
:todo.handler.users/create
{:db #ig/ref :duct.database/sql}๋ฐฉ๊ธ ์ถ๊ฐํ ํธ๋ค๋ฌ ์ค์ ์ ๋ณตํฉ ํค(Composite Key)๋ฅผ ์ฌ์ฉํ์ง ์์์ต๋๋ค. ์๋ํ๋ฉด ๊ธฐ์กด์ ์๋ ๊ธฐ๋ฅ์ ์์ํ์ง ์๊ณ ์๋ก์ด ๊ธฐ๋ฅ์ ๋ง๋ค๋ ค๊ณ ํ๊ธฐ ๋๋ฌธ์ ๋๋ค.
๊ทธ๋ฆฌ๊ณ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐธ์กฐ๋ฅผ ์ถ๊ฐํ์ต๋๋ค. Duct์ ์๋ ๋ชจ๋ SQL ๋ฐ์ดํฐ๋ฒ ์ด์ค ํค๋ :duct.database/sql๋ฅผ
์์ ๋ฐ์ต๋๋ค. Duct๋ ์ด ํค๋ฅผ ์ด์ฉํด์ ์ฌ์ฉ ๊ฐ๋ฅํ SQL ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฐพ์ต๋๋ค.
duct.handler.sql ํค๋ :duct.module.sql/requires-db ํค์๋๋ฅผ ์์ํ๊ณ ์๊ธฐ ๋๋ฌธ์
:duct.module/sql ๋ชจ๋์ด ์๋์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐธ์กฐ๋ฅผ ์ถ๊ฐํด์ค๋๋ค. ํ์ง๋ง ์ฌ๊ธฐ์๋
duct.handler.sql ํค๋ฅผ ์ฌ์ฉํ์ง ์๊ณ ๋ช
์์ ์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐธ์กฐ๋ฅผ ์ถ๊ฐํ์ต๋๋ค.
์ด์ ํธ๋ค๋ฌ ์ฝ๋๋ฅผ ๋ง๋ค์ด ๋ด
์๋ค. ํค์๋์ ์ฌ์ฉํ ๋ค์์คํ์ด์ค๋ todo.handler.users ์
๋๋ค.
๊ทธ๋์ ์ฝ๋์ ์๋ ๋ค์์คํ์ด์ค๋ ๊ฐ์ ๊ฒ์ ์ฌ์ฉํ๋ ค๊ณ ํฉ๋๋ค. src/todo/handler/users.clj
ํ์ผ์ ๋ง๋ค๊ณ ๋ค์์คํ์ด์ค๋ฅผ ์ ์ธํฉ๋๋ค:
(ns todo.handler.users
(:require [ataraxy.response :as response]
[buddy.hashers :as hashers]
[clojure.java.jdbc :as jdbc]
duct.database.sql
[integrant.core :as ig]))ํค ์ ๋ ํจ์(KDF)๋ฅผ ์ฐ๊ธฐ ์ํด buddy.hashers๊ฐ ํ์ํ๊ณ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ๊ทผํ๊ธฐ ์ํด
clojure.java.jdbc๊ฐ ํ์ํฉ๋๋ค. integrant.core ๋ค์์คํ์ด์ค๋ Integrant ๋ฉํฐ๋ฉ์๋๋ฅผ
๋ง๋ค๊ธฐ ์ํด ํ์ํ์ง๋ง ataraxy.response์ duct.database.sql๋ ์ถ๊ฐํ๋ ๋ชฉ์ ์ด
์์ง ๋ช
ํํ์ง ์์ต๋๋ค. (๋ค์์ ์์ ๋ด
๋๋ค.)
์ด์ ์ ์ฌ์ฉ์๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ถ๊ฐํ๋ ํจ์๋ฅผ ๋ง๋ค๊ณ ์ถ๊ฐ๋ row ์์ด๋๋ฅผ ๋ฆฌํดํ๋ ํจ์๋ฅผ ๋ง๋ค์ด๋ด ์๋ค:
(defprotocol Users
(create-user [db email password]))
(extend-protocol Users
duct.database.sql.Boundary
(create-user [{db :spec} email password]
(let [pw-hash (hashers/derive password)
results (jdbc/insert! db :users {:email email, :password pw-hash})]
(-> results ffirst val))))Duct๋ฅผ ์ฒ์ ์ฌ์ฉํ๋ค๋ฉด ์ฌ๊ธฐ์ ํ๋กํ ์ฝ์ ์ด๋ค๋ ์ ์ด ์์ํ ๊ฒ์
๋๋ค. ์ ํจ์๋ฅผ ๋ฐ๋ก ์ฐ์ง ์์๊น์?
์ ์ด์ํ duct.database.sql.Boundary ํ์
์ ํ๋กํ ์ฝ์ ๊ตฌํ์ ํ๋๊ฑธ๊น์?
๋ถ๋ช
ํ ์ ์ ํจ์๋ก ๋ง๋ค์ด๋ ๋๊ณ ๊ทธ๋ ๊ฒํ๋ฉด ์ฝ๋๋ฅผ ๋ช ์ค ๋ ์ค์ผ ์ ์์ต๋๋ค. ํ์ง๋ง ํ๋กํ ์ฝ์ ์ฌ์ฉํ๋ฉด
๊ฐ๋ฐ ํ๊ฒฝ์ด๋ ํ
์คํธ ํ๊ฒฝ์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ Mock์ผ๋ก ๋์ฒดํ ์ ์๋ค๋ ์ฅ์ ์ด ์์ต๋๋ค. ์ด๋ฐ ์ด์ ๋ก Duct๋
duct.database.sql.Boundary๋ผ๊ณ ๋ถ๋ฅด๋ ๋น์ด ์๋ '๋ฐ์ด๋๋ฆฌ' ๋ ์ฝ๋๋ฅผ ์ ๊ณตํฉ๋๋ค. ์์์
duct.database.sql ๋ค์์คํ์ด์ค๋ฅผ ํฌํจ์ํจ ์ด์ ์
๋๋ค. ๊ทธ๋ ์ง ์์ผ๋ฉด ๋ ์ฝ๋๊ฐ ๋ก๋๋์ง ์์ต๋๋ค.
๋ง์ง๋ง์ผ๋ก create ํค์๋๋ฅผ ์ํ init-key ๋ฉ์๋๋ฅผ ๋ง๋ญ๋๋ค:
(defmethod ig/init-key ::create [_ {:keys [db]}]
(fn [{[_ email password] :ataraxy/result}]
(let [id (create-user db email password)]
[::response/created (str "/users/" id)])))Ataraxy๋ Ring ์๋ต ๋งต ๋์ ๋ฐฑํฐ๋ฅผ ๋ฆฌ๋ฐ ํ ์ ์์ต๋๋ค. ์ด ๊ธฐ๋ฅ์ ์ถ์ํ์ ํธ๋ฆฌํจ์ ์ค๋๋ค.
์ ์์ ์์ Ataraxy๋ 201 Created ์๋ต์ ๋ด๋ ค์ฃผ๊ฒ ๋ฉ๋๋ค.
์ด์ reset์ ํด๋ด
์๋ค:
dev=> (reset)
:reloading (todo.main todo.handler.users dev user)
:resumed๊ทธ๋ฆฌ๊ณ ํ์ธํด๋ด ๋๋ค:
$ http post :3000/users email=bob@example.com password=hunter2 HTTP/1.1 201 Created Content-Length: 0 Content-Type: application/octet-stream Date: Mon, 11 Dec 2017 14:10:31 GMT Location: http://localhost:3000/users/1 Server: Jetty(9.2.21.v20170120)
์์ง ์ ๋์๋์ง ๋์ผ๋ก ํ์ธํด ๋ณผ ๋ฐฉ๋ฒ์ ์์ต๋๋ค. ๊ทธ๋์ ์ด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ดํด๋ณผ ํ์๊ฐ ์์ต๋๋ค.
๊ฐ๋ฐ์ ํ๋ฉด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐ์ดํฐ๊ฐ ์ ๋ค์ด๊ฐ๊ณ ์๋์ง ํ์ธํ ํ์๊ฐ ์์ต๋๋ค.
๊ฐ๋ฐ์ ํธ์๋ฅผ ์ํด dev/src/dev.clj ํ์ผ์ dev ๋ค์์คํ์ด์ค๋ฅผ ์ถ๊ฐํฉ์๋ค.
๋จผ์ clojure.java.jdbc ๋ค์์คํ์ด์ค๊ฐ ํ์ํฉ๋๋ค:
[clojure.java.jdbc :as jdbc]๋ค์์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ์ด ํ์ํฉ๋๋ค. ๊ฐ๋ฐ ํ๊ฒฝ์์ Duct๋ system var์ ํ์ฌ ๋์ํ๋
์์คํ
์ ๋ณด๋ฅผ ์ ์ฅํ๊ณ ์์ต๋๋ค. ๊ทธ๋์ JDBC ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํ์ ๊ฐ์ ธ์ค๋ ๊ฐ๋จํ ํจ์๋ฅผ ์๋์ ๊ฐ์ด
๋ง๋ค ์ ์์ต๋๋ค:
(defn db []
(-> system (ig/find-derived-1 :duct.database/sql) val :spec))๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ์ ๊ฐ์ ธ์์ผ๋ ์ด์ ์ฟผ๋ฆฌ๋ฅผ ๋์์ฃผ๋ ๊ฐ๋จํ ํจ์๋ฅผ ๋ง๋ค์ด ๋ด ์๋ค:
(defn q [sql]
(jdbc/query (db) sql))๋ค ํ์ผ๋ฉด reset์ ์คํํด ์ค๋๋ค:
dev=> (reset)
:reloading (dev)
:resumed๋ค์์ users ํ
์ด๋ธ์ ์ฟผ๋ฆฌ๋ฅผ ์คํํด ๋ด
๋๋ค:
dev=> (q "SELECT * FROM users")
({:id 1,
:email "bob@example.com",
:password
"bcrypt+sha512$f4c1bc592ecd1869d0bf802f7c8f6e36$12$19a9ae3ed9118cb6cbfcd8c4a31aadb6b00162288b1fce50"})์ ๋ ๊ฒ ๊ฐ์ต๋๋ค. ID, ์ด๋ฉ์ผ, ํด์ฌ๋ ๋น๋ฐ๋ฒํธ๊ฐ ์๋ค์.