๐ 13.1 API์์ DSL๋ก: ํํ๋ ฅ์ด ์ข์ ์ปค์คํ ์ฝ๋ ๊ตฌ์กฐ ๋ง๋ค๊ธฐ
| ์ผ๋ฐ ๊ตฌ๋ฌธ | ๊ฐ๊ฒฐํ ๊ตฌ๋ฌธ | ์ฌ์ฉํ ์ธ์ด ํน์ฑ |
|---|---|---|
StringUtil.capitalizes(s) |
s.capitalize() |
ํ์ฅ ํจ์ |
1.to("one") |
1 to "one" |
์ค์ ํธ์ถ |
set.add(2) |
set += 2 |
์ฐ์ฐ์ ์ค๋ฒ๋ก๋ฉ |
map.get("key") |
map["key"] |
get ๋ฉ์๋์ ๋ํ ๊ด๋ก |
file.use({ f -> f.read() }) |
file.use { it.read() } |
๋๋ค๋ฅผ ๊ดํธ ๋ฐ์ผ๋ก ๋นผ๋ด๋ ๊ด๋ก |
sb.append("yes") |
with(sb) { append("yes") append("no") } |
์์ ๊ฐ์ฒด ์ง์ ๋๋ค |
- ๊น๋ํ API๋ฅผ ์์ฑํ ์ ์๊ฒ ๋๋ ์ฝํ๋ฆฐ ๊ธฐ๋ฅ์๋ ์์ ๊ฐ์ ๊ธฐ๋ฅ๋ค์ด ์๋ค.
๐ 13.1.1 ๋๋ฉ์ธ ํนํ ์ธ์ด
- ๊ฐ์ฅ ์ต์ํ DSL์ SQL๊ณผ ์ ๊ท์์ผ ๊ฒ์ด๋ค.
- DSL์ด ๋ฒ์ฉ ํ๋ก๊ทธ๋๋ฐ ์ธ์ด์ ๋ฌ๋ฆฌ ๋ ์ ์ธ์ ์ด๋ค.
- ๋ฒ์ฉ ํ๋ก๊ทธ๋๋ฐ ์ธ์ด๋ ๋ช ๋ น์ ์ด๋ค.
- ์ ์ธ์ ์ธ์ด๋ ์ํ๋ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ์ ํ๊ธฐ๋ง ํ๊ณ ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋ฌ์ฑํ๊ธฐ ์ํด ํ์ํ ์ธ๋ถ ์คํ์ ์ธ์ด๋ฅผ ํด์ํ๋ ์์ง์ ๋งก๊ธด๋ค.
- DSL์ ๊ฐ์ฅ ํฐ ๋จ์ ์ ๋ฒ์ฉ ์ธ์ด๋ก ๋ง๋ ํธ์คํธ ์ ํ๋ฆฌ์ผ์ด์
๊ณผ DSL์ ํจ๊ป ์กฐํฉํ๊ธฐ๊ฐ ์ด๋ ต๋ค๋ ๊ฒ
- DSL์ ์์ฒด ๋ฌธ๋ฒ์ด ์๊ธฐ ๋๋ฌธ์ ๋ค๋ฅธ ์ธ์ด์ ํ๋ก๊ทธ๋จ ์์ ์ง์ ํฌํจ์ํฌ ์๊ฐ ์๋ค.
- ์ด๋ฌํ ๋จ์ ์ ํด๊ฒฐํ๋ฉด์ DSL์ ๋ค๋ฅธ ์ด์ ์ ์ด๋ฆฌ๋ ๋ฐฉ๋ฒ์ผ๋ก ์ฝํ๋ฆฐ์์๋ ๋ด๋ถ DSL์ ๋ง๋ค ์ ์๊ฒ ํด์ค๋ค.
๐ 13.1.2 ๋ด๋ถ DSL์ ํ๋ก๊ทธ๋จ์ ๋๋จธ์ง ๋ถ๋ถ๊ณผ ๋งค๋๋ฝ๊ฒ ํตํฉ๋๋ค
- ๋ ๋ฆฝ์ ์ธ ๋ฌธ๋ฒ ๊ตฌ์กฐ๋ฅผ ๊ฐ๋ ์ธ๋ถ DSL๊ณผ๋ ๋ฐ๋๋ก ๋ด๋ถ DSL์ ๋ฒ์ฉ ์ธ์ด๋ก ์์ฑ๋ ํ๋ก๊ทธ๋จ์ ์ผ๋ถ๋ฉฐ, ๋ฒ์ฉ ์ธ์ด์ ๋์ผํ ๋ฌธ๋ฒ์ ์ฌ์ฉํ๋ค.
SELECT country.name, COUNT(customer.id)
FROM country
JOIN customer ON customer.country_id = country.id
GROUP BY country.name
ORDER BY COUNT(customer.id) DESC LIMIT 1;
- ์ด๋ฅผ ์ฝํ๋ฆฐ๊ณผ ์ต์คํฌ์ฆ๋(์ฝํ๋ฆฐ์ผ๋ก ์์ฑ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ๋ ์์ํฌ)๋ฅผ ์ฌ์ฉํด ๊ตฌํํ ์๋ ์๋์ ๊ฐ๋ค.
(Country innerJoin Customer)
.slice(Country.name, Count(Customer.id))
.selectAll()
.groupBy(Country.name)
.orderBy(Count(Customer.id), order = SortOrder.DESC)
.limit(1)
- ์ด๋ฅผ ๋ด๋ถ DSL์ด๋ผ๊ณ ๋ถ๋ฅธ๋ค.
๐ 13.1.3 DSL์ ๊ตฌ์กฐ
- DSL์๋ง ์กด์ฌํ๋ ํน์ง์ ๊ตฌ์กฐ(๋ฌธ๋ฒ)์ด๋ค.
- ์ผ๋ฐ์ ์ธ API๋ ๋ช ๋ น-์ง์ API์ด๋ค.
- DSL์ ๋ฌธ๋ฒ์ ์ํด ์ ํด์ง๋ค.
- ์ด๋ฌํ ๋ฌธ๋ฒ ๋๋ฌธ์ ๋ด๋ถ DSL์ ์ธ์ด๋ผ๊ณ ๋ถ๋ฅผ ์ ์๋ค.
- ์ฌ๋ฌ ํจ์ ํธ์ถ์ ์กฐํฉํด์ ์ฐ์ฐ์ ๋ง๋ค๋ฉฐ ํ์ ๊ฒ์ฌ๊ธฐ๋ ์ฌ๋ฌ ํจ์ ํธ์ถ์ด ๋ฐ๋ฅด๊ฒ ์กฐํฉ๋๋์ง๋ฅผ ๊ฒ์ฌํ๋ค.
- ๋ฉ์๋ ํธ์ถ ์ฐ์๋ DSL ๊ตฌ์กฐ๋ฅผ ๋ง๋๋ ๋ ๋ค๋ฅธ ๋ฐฉ๋ฒ์ด๋ค.
๐ 13.1.4 ๋ด๋ถ DSL๋ก HTML ๋ง๋ค๊ธฐ
kotlinx.html๋ก HTML์ ๋ง๋ค ์ ์๋ค.- ์ง์ HTML ํ ์คํธ๋ฅผ ์์ฑํ์ง ์๊ณ ์ฝํ๋ฆฐ ์ฝ๋๋ก HTML์ ๋ง๋ค๋ฉด ํ์ ์์ ์ฑ์ ๋ณด์ฅํ๋ค.
- ๋ด๋ถ์ ์ฝํ๋ฆฐ ์ฝ๋๋ฅผ ์ํ๋ ๋๋ก ์ฌ์ฉํ ์ ์๋ค.
- HTML์ ๊ณ ์ ์ ์ธ ๋งํฌ์ ์ธ์ด ์์ ์ด๋ฉฐ DSL์ ๊ฐ๋ ์ ์ ๋๋ก ๋ณด์ฌ์ค๋ค.
- XML๊ณผ ๊ฐ์ด HTML๊ณผ ๊ตฌ์กฐ๊ฐ ๋น์ทํ ๋ค๋ฅธ ๋ชจ๋ ์ธ์ด์ ์ด์ ๋น์ทํ ์ ๊ทผ ๋ฐฉ์์ ํํ ์ ์๋ค.
๐ 13.2 ๊ตฌ์กฐํ๋ API ๊ตฌ์ถ: DSL์์ ์์ ๊ฐ์ฒด ์ง์ ๋๋ค ์ฌ์ฉ
๐ 13.2.1 ์์ ๊ฐ์ฒด ์ง์ ๋๋ค์ ํ์ฅ ํจ์ ํ์
fun buildString(
builderAction: (StringBuilder) -> Unit
): String {
val sb = StringBuilder()
builderAction(sb)
return sb.toString()
}
fun main() {
val s = buildString {
it.append("Hello, ")
it.append("World!")
}
println(s)
}
- ๋๋ค๋ฅผ ์ธ์๋ก ๋ฐ๋
buildString์ ์ - ๋๋ค ๋ณธ๋ฌธ์์ ๋งค๋ฒ it์ ์ฌ์ฉํด
StringBuilder์ธ์คํด์ค๋ฅผ ์ฐธ์กฐํด์ผ ํ๋ค.
fun buildString(
builderAction: StringBuilder.() -> Unit
): String {
val sb = StringBuilder()
sb.builderAction()
return sb.toString()
}
fun main() {
val s = buildString {
this.append("Hello, ")
this.append("World!")
}
println(s)
}
- ์์ ๊ฐ์ฒด ์ง์ ๋๋ค๋ฅผ ์ธ์๋ก ๋๊ธฐ๊ธฐ ๋๋ฌธ์ ๋๋ค ์์์ it์ ์ฌ์ฉํ์ง ์์๋ ๋๋ค.
val appendExcl: StringBuilder.() -> Unit =
{ this.append("!") }
fun main() {
val stringBuilder = StringBuilder("Hi")
stringBuilder.appendExcl()
println(stringBuilder)
println(buildString(appendExcl))
}
- ์ฝ๋์์ ์์ ๊ฐ์ฒด ์ง์ ๋๋ค๋ ์ผ๋ฐ ๋๋ค์ ๋๊ฐ๋ค ๋ณด์ธ๋ค.
fun builderString(builderAction: StringBuilder.() -> Unit): String =
StringBuilder().apply(builderAction).toString()
- ํ์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ์
builderString๊ตฌํ์ ๋ ์งง๋ค. - ๊ธฐ๋ณธ์ ์ผ๋ก
apply์with๋ ๋ชจ๋ ์์ ์ด ์ ๊ณต๋ฐ์ ์์ ๊ฐ์ฒด๋ฅผ ๊ฐ๊ณ ํ์ฅ ํจ์ ํ์ ์ ๋๋ค๋ฅผ ํธ์ถ
๐ 13.2.2 ์์ ๊ฐ์ฒด ์ง์ ๋๋ค๋ฅผ HTML ๋น๋ ์์์ ์ฌ์ฉ
- HTML์ ๋ง๋ค๊ธฐ ์ํ ์ฝํ๋ฆฐ DSL์ ๋ณดํต์ HTML ๋น๋๋ผ๊ณ ๋ถ๋ฅธ๋ค.
fun createSimpleTable() = createHTML().table {
tr {
td { +"cell" }
}
}
- ๋ชจ๋ ํ๋ฒํ ํจ์๋ค.
- ๊ฐ ์์ ๊ฐ์ฒด ์ง์ ๋๋ค๊ฐ ์ด๋ฆ ๊ฒฐ์ ๊ท์น์ ๋ฐ๊พผ๋ค.
open class Tag
class TABLE : Tag {
fun tr(init: TR.() -> Unit)
}
class TR : Tag {
fun td(init: TD.() -> Unit)
}
class TD : Tag
- ๋ชจ๋ ์ ํธ๋ฆฌํฐ ํด๋์ค๋ค.
fun createSimpleTable() = createHTML().table {
this@table.tr {
(this@tr).td {
+"cell"
}
}
}
- this ์ฐธ์กฐ๋ฅผ ์ฐ์ง ์์๋ ๋๋ฉด ๋น๋ ๋ฌธ๋ฒ์ด ๊ฐ๋จํด์ง๊ณ ์ ์ฒด์ ์ธ ๊ตฌ๋ฌธ์ด ์๋์ HTML ๊ตฌ๋ฌธ๊ณผ ๋น์ทํด์ง๋ค.
@DslMarker
annotation class HtmlTagMarker
@DslMarker์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํด ๋ดํฌ๋ ๋๋ค์์ ์ธ๋ถ ๋๋ค์ ์์ ๊ฐ์ฒด์ ์ ๊ทผํ์ง ๋ชปํ๊ฒ ์ ํํ ์ ์๋ค.- ๋ฉํ ์ด๋ ธํ ์ด์ ์ด๋ค.
@DslMarker
annotation class HtmlTagMarker
@HtmlTagMarker
open class Tag(val name: String) {
private val children = mutableListOf<Tag>()
protected fun <T : Tag> doInit(child: T, init: T.() -> Unit) {
child.init()
children.add(child)
}
override fun toString() = "<$name>${children.joinToString("")}</$name>"
}
fun table(init: TABLE.() -> Unit) = TABLE().apply(init)
class TABLE : Tag("table") {
fun tr(init: TR.() -> Unit) = doInit(TR(), init)
}
class TR : Tag("tr") {
fun td(init: TD.() -> Unit) = doInit(TD(), init)
}
class TD : Tag("td")
fun createTable() = table {
tr {
td {
}
}
}
- ๊ฐ๋จํ HTML ๋น๋์ ์ ์ฒด ๊ตฌํ
๐ 13.2.3 ์ฝํ๋ฆฐ ๋น๋: ์ถ์ํ์ ์ฌ์ฌ์ฉ์ ๊ฐ๋ฅํ๊ฒ ํด์ค๋ค
fun buildBookList() = createHTML().body {
ul {
li { a("#1") { +"The Three-Body Problem" } }
li { a("#2") { +"The Cartesian Product Problem" } }
li { a("#3") { +"The Conjugation Problem" } }
}
h2 { id = "1"; +"The Three-Body Problem" }
p { +"The Three-Body Problem is a classic physics problem from the early 20th century." }
h2 { id = "2"; +"The Cartesian Product Problem" }
p { +"The Cartesian product is a useful mathematical construct for a wide variety of problems." }
h2 { id = "3"; +"The Conjugation Problem" }
p { +"The Conjugation Problem is a classic computational problem from the 1970s." }
}
- ๋ชฉ์ฐจ๋ก ์์ํ๋ ํ์ด์ง๋ฅผ ์ฝํ๋ฆฐ HTML ๋น๋๋ก ๋ง๋ค๊ธฐ
- ์ด๊ฑธ ์ข ๋ ์ด์๊ฒ ๋ค๋ฌ์ ์ ์๋ค.
fun buildBookList() = createHTML().body {
listWithToc {
item(
"The Three-Body Problem",
"The Three-Body Problem is a classic physics problem from the early 20th century."
)
item(
"The Cartesian Product Problem",
"The Cartesian product is a useful mathematical construct for a wide variety of problems."
)
item(
"The Conjugation Problem",
"The Conjugation Problem is a classic computational problem from the 1970s."
)
}
}
@HtmlTagMarker
class LISTWITHTOC {
val entries = mutableListOf<Pair<String, String>>()
fun item(headline: String, body: String) {
entries += headline to body
}
}
@HtmlTagMarker์ด๋ ธํ ์ด์ ์ ๋ถ์ฌ DSL ์์ญ ๊ท์น์ ๋ฐ๋ฅด๊ฒ ํ ์ ์๋ค.
fun BODY.listWithToc(block: LISTWITHTOC.() -> Unit) {
val listWithToc = LISTWITHTOC()
listWithToc.block()
ul {
for ((index, entry) in listWithToc.entries.withIndex()) {
li {
a("#${index}") { +entry.first }
}
}
}
for ((index, entry) in listWithToc.entries.withIndex()) {
h2 { id = "$index"; +entry.first }
p { +entry.second }
}
}
- ์ถ์ํ์ ์ฌ์ฌ์ฉ์ ํตํด ์ฝ๋๋ฅผ ๊ฐ์ ํ๊ณ ์ดํดํ๊ธฐ ์ฝ๊ฒ ๋ง๋๋ ๋ฐฉ๋ฒ์ด๋ค.
๐ 13.3 invoke ๊ด๋ก๋ฅผ ์ฌ์ฉํด ๋ ์ ์ฐํ๊ฒ ๋ธ๋ก ๋ดํฌ์ํค๊ธฐ
๐ 13.3.1 invoke ๊ด๋ก๋ฅผ ์ฌ์ฉํด ๋ ์ ์ฐํ๊ฒ ๋ธ๋ก ๋ดํฌ์ํค๊ธฐ
class Greeter(val greeting: String) {
operator fun invoke(name: String) {
println("$greeting, $name!")
}
}
fun main() {
val bavarianGreeter = Greeter("Servus")
bavarianGreeter("Dmitry")
}
- ํด๋์ค ์์์
invoke๋ฉ์๋ ์ ์ invoke๋ฉ์๋์ ์๊ทธ๋์ฒ์ ๋ํ ์๊ตฌ์ฌํญ์ ์๋ค.- ์ํ๋๋๋ก ํ๋ผ๋ฏธํฐ ๊ฐ์๋ ํ์ ์ ์ง์ ํ ์ ์๋ค.
๐ 13.3.2 DSL์ invoke ๊ด๋ก: ๊ทธ๋ ์ด๋ค ์์กด๊ด๊ณ ์ ์ธ
dependencies {
testImplementation(kotlin("test"))
implementation("org.jetbrains.exposed:exposed-core:0.40.1")
implementation("org.jetbrains.exposed:exposed-dao:0.40.1")
}
- ์ด ์ฝ๋์ฒ๋ผ ๋ดํฌ๋ ๋ธ๋ก ๊ตฌ์กฐ๋ฅผ ํ์ฉํ๋ ํํธ, ํํํ ํจ์ ํธ์ถ ๊ตฌ์กฐ๋ ํจ๊ป ์ ๊ณตํ๋ API๋ฅผ ๋ง๋ค๊ณ ์ถ๋ค.
dependencies.implementation("org.jetbrains.exposed:exposed-core:0.40.1")
dependencies {
implementation("org.jetbrains.exposed:exposed-core:0.40.1")
}
- ์ฒซ ๋ฒ์งธ ๊ตฌ๋ฌธ์
invoke๋ฅผ ์ฌ์ฉํ ๊ฒ์ด๋ค.
class DependencyHandler {
fun implementation(coordinate: String) {
println("Added dependency on $coordinate")
}
operator fun invoke(body: DependencyHandler.() -> Unit) {
body()
}
}
fun main() {
val dependencies = DependencyHandler()
dependencies.implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0")
}
}
- ์ ์ฐํ DSL ๋ฌธ๋ฒ์ ์ ๊ณตํ๊ธฐ ์ํด
invoke์ฌ์ฉ - ๊ฝค ์ ์ ์์ ์ฝ๋์ง๋ง ์ด๋ ๊ฒ ์ฌ์ ์ํ
invoke๋ฉ์๋๋ก ์ธํด DSL API์ ์ ์ฐ์ฑ์ด ํจ์ฌ ์ปค์ง๋ค.
๐ 13.4 ์ค์ ์ฝํ๋ฆฐ DSL
๐ 13.4.1 ์ค์ ํธ์ถ ์ฐ์์ํค๊ธฐ: ํ ์คํธ ํ๋ ์์ํฌ์ should ํจ์
class PrefixTest {
@Test
fun testKPrefix() {
val s = "kotlin".uppercase()
s should startWith("K")
}
}
- ์ผ๋ฐ ์์ด์ฒ๋ผ ์ฝ๋๋ฅผ ์ฝ์ ์ ์๋ค.
infix fun <T> T.should(matcher: Matcher<T>) = matcher.test(this)
interface Matcher<T> {
fun test(value: T)
}
fun startWith(prefix: String): Matcher<String> {
return object : Matcher<String> {
override fun test(value: String) {
if (!value.startsWith(prefix)) {
throw AssertionError("$value does not start with $prefix")
}
}
}
}
- DSL ์์ ์ฌ์ฉํ๋ ค๋ฉด infix ๋ณ๊ฒฝ์๋ฅผ ๋ถ์ฌ์ผ ํ๋ค.
- ์ค์ ํธ์ถ๊ณผ object๋ก ์ ์ํ ์ฑ๊ธํด ๊ฐ์ฒด ์ธ์คํด์ค๋ฅผ ์กฐํฉํ๋ฉด DSL์ ์๋นํ ๋ณต์กํ ๋ฌธ๋ฒ์ ๋์ ํ ์ ์๊ณ , ๊ทธ ๋ฌธ๋ฒ์ ์ฌ์ฉํ๋ฉด DSL ๊ตฌ๋ฌธ์ ๊น๋ํ๊ฒ ๋ง๋ค ์ ์๋ค.
๐ 13.4.2 ์์ ํ์ ์ ๋ํด ํ์ฅ ํจ์ ์ ์ํ๊ธฐ: ๋ ์ง ์ฒ๋ฆฌ
val Int.days: Duration
get() = this.toDuration(DurationUnit.DAYS)
val Int.hours: Duration
get() = this.toDuration(DurationUnit.HOURS)
- ์ฝํ๋ฆฐ์์๋ ์๋ฌด ํ์ ์ด๋ ํ์ฅ ํจ์์ ์์ ๊ฐ์ฒด ํ์ ์ด ๋ ์ ์๋ค.
๐ 13.4.3 ๋ฉค๋ฒ ํ์ฅ ํจ์: SQL์ ์ํ ๋ด๋ถ DSL
- ํด๋์ค ์์์ ํ์ฅ ํจ์์ ํ์ฅ ํ๋กํผํฐ๋ฅผ ์ ์ธํ๋ฉด ๊ทธ๋ค์ด ์ ์ธ๋ ํด๋์ค์ ๋ฉค๋ฒ์ธ ๋์์ ๊ทธ๋ค์ด ํ์ฅํ๋ ๋ค๋ฅธ ํ์
์ ๋ฉค๋ฒ์ด๊ธฐ๋ ํ๋ค.
- ์ด๋ฐ ํจ์๋ ํ๋กํผํฐ๋ฅผ ๋ฉค๋ฒ ํ์ฅ์ด๋ผ ๋ถ๋ฅธ๋ค.
- ๋ฉค๋ฒ ํ์ฅ๋ ์ฌ์ ํ ๋ฉค๋ฒ๋ค.
- ์๋ฅผ ๋ค์ด,
Tableํด๋์ค์integer,varchar๋ฑ์ ๋ฉ์๋๋ ์ธ๋ถ์์ ํธ์ถํ ์ ์์ง๋ง, ๋ฉค๋ฒ ํ์ฅ ํจ์๋ฅผ ํตํด ์ ํ๋ ๋ฒ์ ๋ด์์๋ง ์ฌ์ฉํ๋๋ก ํ ์ ์๋ค.
