Skip to content

Latest commit

ย 

History

History
778 lines (528 loc) ยท 25.8 KB

File metadata and controls

778 lines (528 loc) ยท 25.8 KB

Duct Framework ๊ฐ€์ด๋“œ

๋จธ๋ฆฌ๋ง

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์„ ์‚ฌ์šฉํ•˜๋ฉด ๋”ฐ๋กœ ํ•ด์ค„ ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋‹ค๋ฅธ ์†Œ์Šค ์ปจํŠธ๋กค์„ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ์ด ํŒŒ์ผ๋“ค์ด ์†Œ์Šค ์ปจํŠธ๋กค์— ๊ด€๋ฆฌ๋˜์ง€ ์•Š๋„๋ก ์ˆ˜๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.

REPL ์‹œ์ž‘ํ•˜๊ธฐ

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" ์‘๋‹ต์„ ๋ฐ›์•˜์Šต๋‹ˆ๋‹ค. ์•„์ง ์•„๋ฌด ๋ผ์šฐํ„ฐ๋„ ์ถ”๊ฐ€ํ•˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์— ์˜ˆ์ƒ๋œ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค.

Configuration

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
    }
]

์ด์ œ ์“ธ๋งŒํ•œ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ผˆ๋Œ€๊ฐ€ ์ƒ๊ฒผ์Šต๋‹ˆ๋‹ค.

์ข€ ๋” RESTfulํ•˜๊ฒŒ ๋งŒ๋“ค๊ธฐ

์ด์ œ ์—”ํŠธ๋ฆฌ์˜ ๋ชฉ๋ก์— 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, ์ด๋ฉ”์ผ, ํ•ด์‰ฌ๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์žˆ๋„ค์š”.