DSL

์ด๋ฒˆ ์ฑ•ํ„ฐ์—์„œ๋Š” ์˜์—ญ ํŠนํ™” ์–ธ์–ด(Domain-Specific Language)๋ฅผ ํ†ตํ•ด ๊ด€์šฉ์ ์ธ API๋ฅผ ๋””์ž์ธํ•˜๋Š” ๋ฒ•์„ ๋ฐฐ์šธ ๊ฒƒ์ด๋‹ค. ์ „ํ†ต์ ์ธ API์™€ DSL ์Šคํƒ€์ผ API์˜ ์ฐจ์ด์ ์„ ๊ณต๋ถ€ํ•˜๊ณ , DSL ์Šคํƒ€์ผ API๊ฐ€ ๋‹ค์–‘ํ•œ ์‹ค์šฉ์ ์ธ ๋ฌธ์ œ, ์ฆ‰ DB ์ ‘๊ทผ, HTML ์ƒ์„ฑ, ํ…Œ์ŠคํŒ… ๋“ฑ์— ์‚ฌ์šฉ๋˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šธ ๊ฒƒ์ด๋‹ค.

์ฝ”ํ‹€๋ฆฐ DSL ์„ค๊ณ„๋Š” ์ฝ”ํ‹€๋ฆฐ ์–ธ์–ด์˜ ์—ฌ๋Ÿฌ ํŠน์„ฑ์„ ํ™œ์šฉํ•˜๋Š”๋ฐ ๊ทธ์ค‘ ํ•˜๋‚˜๊ฐ€ 5์žฅ์—์„œ ์‚ดํŽด๋ณธ ์ˆ˜์‹  ๊ฐ์ฒด ์ง€์ • ๋žŒ๋‹ค์ด๋‹ค. ๋˜๋‹ค๋ฅธ ์ƒˆ๋กœ์šด ํŠน์ง•์€ invoke๋กœ, ์ด๋Š” 10์žฅ์—์„œ KFunction์„ call์ด๋‚˜ invoke๋กœ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์— ๊ด€ํ•ด์„œ 11์žฅ์— ์„ค๋ช…ํ•  ๊ฒƒ์ด๋ผ๊ณ  ํ•˜๊ณ  ๋„˜์–ด๊ฐ”๋‹ค. invoke๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด DSL ์ฝ”๋“œ ์•ˆ์—์„œ ๋žŒ๋‹ค์™€ ํ”„๋กœํผํ‹ฐ ๋Œ€์ž…์„ ๋” ์œ ์—ฐํ•˜๊ฒŒ ์กฐํ•ฉํ•  ์ˆ˜ ์žˆ๋‹ค.

APIs์—์„œ DSLs๋กœ

์šฐ๋ฆฌ์˜ ๋ชฉํ‘œ๋Š” ์ฝ๊ธฐ ์‰ฝ๊ณ  ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ ์‰ฌ์šด ์ฝ”๋“œ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด๋‹ค. ์ด ๋ชฉํ‘œ๋ฅผ ์œ„ํ•ด์„œ๋Š” ๊ฐ๊ฐ์˜ ํด๋ž˜์Šค์—๋งŒ ์ง‘์ค‘ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ๊ฐ ํด๋ž˜์Šค๊ฐ€ ์„œ๋กœ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ๋ฐ”๋ฅผ ์‚ดํŽด๋ด์•ผ ํ•œ๋‹ค. ์ฆ‰, ํด๋ž˜์Šค APIs์— ์ง‘์ค‘ํ•ด์•ผ ํ•œ๋‹ค.

์ด ์ฑ…์„ ํ†ตํ•ด์„œ ์šฐ๋ฆฌ๋Š” clean APIs๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ์ฝ”ํ‹€๋ฆฐ์˜ ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ๋“ค์„ ์‚ดํŽด๋ณด์•˜๋‹ค. clean APIs๋ž€ ๋ฌด์—‡์ผ๊นŒ? ์ด๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ง์„ ๋œปํ•œ๋‹ค.

  • ์ฝ”๋“œ๋ฅผ ์ฝ๋Š” ๋…์ž๊ฐ€ ์–ด๋–ค ์ผ์ด ๋ฒŒ์–ด์งˆ์ง€ ๋ช…ํ™•ํ•˜๊ฒŒ ์ดํ•ดํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค. ์–ด๋–ค ์–ธ์–ด๋ฅผ ์‚ฌ์šฉํ•˜๊ฑด ์ด๋ฆ„์„ ์ž˜ ๋ถ™์ด๊ณ  ์ ์ ˆํ•œ ๊ฐœ๋…์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ๋งค์šฐ ์ค‘์š”ํ•˜๋‹ค.

  • ์ฝ”๋“œ๊ฐ€ ๊ฐ„๊ฒฐํ•ด์•ผ ํ•œ๋‹ค. ๋ถˆํ•„์š”ํ•œ ๊ตฌ๋ฌธ์ด๋‚˜ ๋ฒˆ์žกํ•œ ์ค€๋น„ ์ฝ”๋“œ๊ฐ€ ๊ฐ€๋Šฅํ•œ ํ•œ ์ ์–ด์•ผ ํ•œ๋‹ค. ์ด ์กฐ๊ฑด์„ ๋งŒ์กฑ์‹œํ‚ค๋Š” ๊ฒƒ์ด ์ด๋ฒˆ ์ฑ•ํ„ฐ์˜ ์ฃผ์š” ๋ชฉํ‘œ์ด๋‹ค.

์ฝ”ํ‹€๋ฆฐ์—์„œ ๊น”๋”ํ•œ API๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์œ„ํ•ด ์ง€์›ํ•˜๋Š” ๊ธฐ๋Šฅ๋“ค์„ ํ‘œ๋กœ ์‚ดํŽด๋ณด์ž.

์ผ๋ฐ˜ ๊ตฌ๋ฌธ ๊ฐ„๊ฒฐํ•œ ๊ตฌ๋ฌธ ์‚ฌ์šฉํ•œ ์–ธ์–ด ํŠน์„ฑ

StringUtil.capitalize(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โ€)}

์ˆ˜์‹  ๊ฐ์ฒด ์ง€์ • ๋žŒ๋‹ค

์ฝ”ํ‹€๋ฆฐ DSL์€ clean-syntax๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ๊ณผ ๊ทธ๋Ÿฐ ๊ตฌ๋ฌธ์„ ํ™•์žฅํ•ด ์—ฌ๋Ÿฌ ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ์„ ์กฐํ•ฉํ•จ์œผ๋กœ์จ ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค์–ด๋‚ด๋Š” ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•œ๋‹ค.

๊ทธ ๊ฒฐ๊ณผ๋กœ DSL์€ ๊ฐ๊ฐ์˜ ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ๋งŒ์„ ์ œ๊ณตํ•˜๋Š” API์— ๋น„ํ•ด ๋” ํ‘œํ˜„๋ ฅ์ด ํ’๋ถ€ํ•ด์ง€๊ณ  ์‚ฌ์šฉํ•˜๊ธฐ ํŽธํ•ด์ง„๋‹ค.

Kotlin DSLs๋Š” ์ฝ”ํ‹€๋ฆฐ๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ, ์ปดํŒŒ์ผ ์‹œ์ ์— ํƒ€์ž…์ด ์ •ํ•ด์ง„๋‹ค(fully statically typed). ๋”ฐ๋ผ์„œ DSL ํŒจํ„ด์„ ์‚ฌ์šฉํ•  ๋•Œ๋„ ์ฝ”ํ‹€๋ฆฐ์˜ ์žฅ์ ์„ ๋ˆ„๋ฆด ์ˆ˜ ์žˆ๋‹ค.

๋‹ค์Œ์€ DSLs์ด ํ•  ์ˆ˜ ์žˆ๋Š” ์ผ์˜ ์˜ˆ์‹œ์ด๋‹ค.

val yesterday = 1.days.ago

//์œ„์˜ ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ HTMl ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑํ•จ.
fun createSimpleTable() = createHTML().
	table {
		tr {
			td { +"cell" }
	}
}

์ž์„ธํ•œ ์ด์•ผ๊ธฐ๋ฅผ ํ•˜๊ธฐ ์ „์—, ๋จผ์ € DSLs์ด ๋ฌด์—‡์ธ์ง€ ์‚ดํŽด๋ณด์ž.

์˜์—ญ ํŠนํ™” ์–ธ์–ด(DSL)๋ž€?

์˜์—ญ ํŠนํ™” ์–ธ์–ด๋ผ๋Š” ๊ฒƒ ์ž์ฒด๋Š” ์˜ค๋ž˜๋œ ๊ฐœ๋…์ด๋‹ค. ๋ฒ”์šฉ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด(general-purpose programming language)๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜์—ฌ ํ•„์š”ํ•˜์ง€ ์•Š์€ ๊ธฐ๋Šฅ์„ ์—†์•ค ์˜์—ญ ํŠนํ™” ์–ธ์–ด๋ฅผ DSL์ด๋ผ๊ณ  ๋ถ€๋ฅธ๋‹ค. ์ด๋Š” ํŠน์ • ๋„๋ฉ”์ธ๊ณผ ๊ทธ ๋„๋ฉ”์ธ์— ์—ฐ๊ด€๋œ ๊ธฐ๋Šฅ์—๋งŒ ์ง‘์ค‘ํ•˜๋Š” ์–ธ์–ด๋ฅผ ์˜๋ฏธํ•˜๋ฉฐ, SQL์ด๋‚˜ ์ •๊ทœ์‹(regular expressions) ๋“ฑ์ด ์—ฌ๊ธฐ์— ์†ํ•œ๋‹ค. ์ด ์–ธ์–ด๋“ค์€ ํŠน์ • task๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐ๋Š” ํœผ๋ฅญํ•˜๋‚˜, ์ด ์–ธ์–ด๋งŒ์œผ๋กœ ์ „์ฒด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋ชจ๋‘ ์ž‘์„ฑํ•  ์ˆ˜๋Š” ์—†๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ์ด ์–ธ์–ด๋“ค์€ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ถ•์†Œํ•จ์œผ๋กœ์จ ์ž์‹ ์˜ ๋ชฉํ‘œ๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ์„ฑ์ทจํ•  ์ˆ˜ ์žˆ๋‹ค. SQL์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ํ•จ์ˆ˜๋‚˜ ํด๋ž˜์Šค๋ฅผ ์ž‘์„ฑํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค. ์ด์™€ ๊ฐ™์ด DSL์€ ๋ฒ”์šฉ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด์™€ ๋‹ฌ๋ฆฌ declarative(์„ ์–ธ์ )ํ•˜๋‹ค. ์ด์™€ ๋‹ฌ๋ฆฌ ๋ฒ”์šฉ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด๋Š” imperativeํ•˜๋‹ค.

declarative๋Š” ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ์ˆ ํ•˜๊ธฐ๋งŒ ํ•˜๊ณ  ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋‹ฌ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ์„ธ๋ถ€ ์‹คํ–‰์€ ์–ธ์–ด๋ฅผ ํ•ด์„ํ•˜๋Š” ์—”์ง„์— ๋งก๊ธฐ๋Š”๋ฐ, ์‹คํ–‰ ์—”์ง„์ด ๊ฒฐ๊ณผ๋ฅผ ์–ป๋Š” ๊ณผ์ •์„ ์ตœ์ ํ™”ํ•˜๊ธฐ ๋•Œ๋ฌธ์— declarative ์–ธ์–ด๊ฐ€ ๋” ํšจ์œจ์ ์ธ ๊ฒฝ์šฐ๊ฐ€ ์ข…์ข… ์žˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ DSLs์€ ๋˜ํ•œ ๋‹จ์ ๋„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”๋ฐ, ๊ทธ๊ฑด ๋ฐ”๋กœ ๋ฒ”์šฉ ์–ธ์–ด๋กœ ๋งŒ๋“  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ ์กฐํ•ฉํ•˜๊ธฐ ์–ด๋ ต๋‹ค๋Š” ์ ์ด๋‹ค. DSLs์€ ๊ณ ์œ ํ•œ ๋ฌธ๋ฒ•์„ ๊ฐ€์ง€๊ณ  ์žˆ์–ด ๋‹ค๋ฅธ ์–ธ์–ด๋กœ ๋งŒ๋“  ํ”„๋กœ๊ทธ๋žจ๊ณผ ํ†ตํ•ฉํ•˜๊ธฐ ์–ด๋ ต๋‹ค. ๋”ฐ๋ผ์„œ DSL๋กœ ์ž‘์„ฑํ•œ ํ”„๋กœ๊ทธ๋žจ์„ ๋‹ค๋ฅธ ์–ธ์–ด์—์„œ ํ˜ธ์ถœํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” DSL ํ”„๋กœ๊ทธ๋žจ์„ ๋ณ„๋„์˜ ํŒŒ์ผ์ด๋‚˜ ๋ฌธ์ž์—ด ๋ฆฌํ„ฐ๋Ÿด๋กœ ์ €์žฅํ•ด์•ผ ํ•œ๋‹ค.

ํ•˜์ง€๋งŒ ์ด๋Ÿฐ ์‹์œผ๋กœ DSL์„ ์ €์žฅํ•  ๊ฒฝ์šฐ ํ˜ธ์ŠคํŠธ ํ”„๋กœ๊ทธ๋žจ๊ณผ DSL์˜ ์ƒํ˜ธ์ž‘์šฉ์„ ์ปดํŒŒ์ผ ์‹œ์ ์— ๊ฒ€์ฆํ•˜๊ฑฐ๋‚˜ DSL ํ”„๋กœ๊ทธ๋žจ์„ ๋””๋ฒ„๊น…ํ•˜๊ฑฐ๋‚˜ DSL ์ฝ”๋“œ ์ž‘์„ฑ์„ ๋•๋Š” IDE ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๊ธฐ ์–ด๋ ค์›Œ์ง€๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ๋‹ค. ์ด๋Ÿฐ ๋ฌธ์ œ๋ฅผ ๊ทน๋ณตํ•˜๊ธฐ ์œ„ํ•ด internal DSL์ด๋ผ๋Š” ๊ฐœ๋…์ด ์œ ๋ช…ํ•ด์ง€๊ณ  ์žˆ๋‹ค.

internal DSL

๋…๋ฆฝ์ ์ธ ๋ฌธ๋ฒ• ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง„ external DSL๊ณผ ๋‹ฌ๋ฆฌ internal DSL์€ ๋ฒ”์šฉ ์–ธ์–ด๋กœ ์ž‘์„ฑ๋œ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์˜ ์ผ๋ถ€์ด๋ฉฐ ๋ฒ”์šฉ ์–ธ์–ด์™€ ๋™์ผํ•œ ๋ฌธ๋ฒ•์„ ์‚ฌ์šฉํ•œ๋‹ค. ์ฆ‰, internal DSL์€ ๋…๋ฆฝ์ ์ธ ์–ธ์–ด๋ผ๊ธฐ๋ณด๋‹ค DSL์˜ ํ•ต์‹ฌ ์žฅ์ ์„ ์œ ์ง€ํ•˜๋ฉด์„œ ์ฃผ ์–ธ์–ด๋ฅผ ํŠน๋ณ„ํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค. ์ด ๋‘ ๊ฐ€์ง€ ์ ‘๊ทผ๋ฒ•์„ ๋น„๊ตํ•˜๊ธฐ ์œ„ํ•ด external DSL๊ณผ internal DSL์—์„œ ํ…Œ์Šคํฌ๊ฐ€ ์–ด๋–ป๊ฒŒ ์™„๋ฃŒ๋˜๋Š”์ง€ ํ™•์ธํ•ด๋ณด์ž.

SELECT Country.name, COUNT(Customer.id)
    FROM Country
    JOIN Customer
        ON Country.id = Customer.country_id
GROUP BY Country.name
ORDER BY COUNT(Customer.id) DESC
    LIMIT 1

์œ„ SQL์€ ๊ฐ€์žฅ ๋งŽ์€ ๊ณ ๊ฐ์ด ์‚ด๊ณ  ์žˆ๋Š” ๋‚˜๋ผ๋ฅผ ์•Œ์•„๋‚ด๋Š” ์งˆ์˜๋ฌธ์œผ๋กœ, ์งˆ์˜ ์–ธ์–ด๊ฐ€ ์ฃผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์–ธ์–ด ์‚ฌ์ด์— ์ƒํ˜ธ ์ž‘์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ถˆํŽธํ•  ์ˆ˜๋„ ์žˆ๋‹ค. ์•„๋ž˜๋Š” ์œ„์™€ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ํ•˜๋Š” ์ฝ”๋“œ์ง€๋งŒ ์ฝ”ํ‹€๋ฆฐ์œผ๋กœ ์ž‘์„ฑ๋œ ๊ฒƒ์ด๋‹ค.

(Country join Customer)
	.slice(Country.name, Count(Customer.id))
	.selectAll()
	.groupBy(Country.name)
	.orderBy(Count(Customer.id), isAsc = false)
	.limit(1)

์ด ๋‘ ์ฝ”๋“œ๋Š” ๋™์ผํ•œ ํ”„๋กœ๊ทธ๋žจ์„ ์ƒ์„ฑํ•˜๊ณ  ์‹คํ–‰ํ•˜์ง€๋งŒ, ๋‘ ๋ฒˆ์งธ ์ฝ”๋“œ๋Š” ์ผ๋ฐ˜ ์ฝ”ํ‹€๋ฆฐ ์ฝ”๋“œ๋กœ ์ผ๋ฐ˜ ์ฝ”ํ‹€๋ฆฐ ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ์ด๋ฅผ internal DSL์ด๋ผ๊ณ  ๋ถ€๋ฅธ๋‹ค.

DSL์˜ ๊ตฌ์กฐ

์ผ๋ฐ˜์ ์ธ API์™€ DSL ๊ฐ„์˜ ๋ช…ํ™•ํ•œ ๊ตฌ๋ถ„์ด๋ผ๋Š” ๊ฒƒ์€ ์—†๋‹ค. โ€œDSL์€ ๋ณด๋ฉด ์•Œ ์ˆ˜ ์žˆ๋‹คโ€๋ผ๊ณ  ๊ตฌ๋ถ„ํ•˜๊ณ ๋Š” ํ•œ๋‹ค. DSLs์€ ์ข…์ข… ์–ธ์–ด์˜ ํŠน์ง•์— ์˜์กดํ•˜๋ฉฐ, ๋‹ค๋ฅธ ๋งฅ๋ฝ์—์„œ๋„ ํญ๋„“๊ฒŒ ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ์ž์ฃผ ๊ฑฐ๋ก ๋˜๋Š” ํ•˜๋‚˜์˜ ํŠน์„ฑ์€ ๋ฐ”๋กœ APIs์— ์กด์žฌํ•˜์ง€ ์•Š๋Š” ํŠน์ง•, ์ฆ‰ ๊ตฌ์กฐ(structure)์™€ ๋ฌธ๋ฒ•(grammar)์ด๋‹ค.

์ผ๋ฐ˜์ ์œผ๋กœ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ๋งŽ์€ ๋ฉ”์†Œ๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฉฐ, ํด๋ผ์ด์–ธํŠธ๋Š” ์ด๋Ÿฌํ•œ ๋ฉ”์†Œ๋“œ๋ฅผ ํ•˜๋‚˜์”ฉ ํ˜ธ์ถœํ•จ์œผ๋กœ์จ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋Ÿฌํ•œ ์ผ๋ จ์˜ ํ˜ธ์ถœ์— ๋Œ€ํ•œ ๋‚ด๋ถ€์ ์ธ ๊ตฌ์กฐ๋‚˜ ๋งฅ๋ฝ์€ ์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค. ์ด๋Ÿฌํ•œ API๋ฅผ command-query API๋ผ๊ณ  ๋ถ€๋ฅธ๋‹ค. ์ด์™€๋Š” ๋ฐ˜๋Œ€๋กœ, DSL์—์„œ์˜ ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ์€ ์ข€ ๋” ํฐ ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”๋ฐ, ์ด๋Š” DSL์˜ grammar๋กœ ์ •์˜๋œ๋‹ค.

์ฝ”ํ‹€๋ฆฐ DSL์—์„œ๋Š” ๋ณดํ†ต ๋žŒ๋‹ค๋ฅผ ์ค‘์ฒฉ์‹œํ‚ค๊ฑฐ๋‚˜ ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ์„ ์—ฐ์‡„์‹œํ‚ค๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ ๋‹ค. ๊ทธ๋Ÿฐ ๊ตฌ์กฐ๋Š” ์œ„์—์„œ ์‚ดํŽด๋ณธ SQL ์˜ˆ์ œ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

DSL์—์„œ๋Š” ์งˆ์˜๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•œ ๋ฉ”์†Œ๋“œ๋“ค์„ ์กฐํ•ฉํ•ด์•ผํ•˜๋ฉฐ, ๊ทธ๋ ‡๊ฒŒ ๋ฉ”์†Œ๋“œ๋ฅผ ์กฐํ•ฉํ•ด์„œ ๋งŒ๋“  ์งˆ์˜๋Š” ์งˆ์˜์— ํ•„์š”ํ•œ ์ธ์ž๋ฅผ ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ ํ•˜๋‚˜์— ๋ชจ๋‘ ๋„˜๊ธฐ๋Š” ๊ฒƒ๋ณด๋‹ค ํ›จ์”ฌ ๋” ๊ฐ€๋…์„ฑ์ด ๋†’๋‹ค. DSL ๊ตฌ์กฐ์˜ ์žฅ์ ์€ ๋งค๋ฒˆ ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋  ๋•Œ๋งˆ๋‹ค ๋ฐ˜๋ณตํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹Œ, ์—ฌ๋Ÿฌ ๋ฒˆ์˜ ํ•จ์ˆ˜ ํ˜ธ์ถœ์— ๋Œ€ํ•ด ๊ฐ™์€ context๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ๋‹ค์Œ ์˜ˆ์ œ๋ฅผ ์‚ดํŽด๋ณด์ž.

//Kotlin DSL
dependencies {
	compile("junit:junit:4.11")
	compile("com.google.inject:guice:4.1.0")
}
//command-query API
project.dependencies.add("compile", "junit:junit:4.11")
project.dependencies.add("compile", "com.google.inject:guice:4.1.0")

DSLs์— ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์€ Chained method calls(์—ฐ์†์ ์ธ ์ฝ”๋“œ ์ค„์—์„œ ๊ฐœ์ฒด์˜ Method๋ฅผ ๋ฐ˜๋ณต์ ์œผ๋กœ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ)์ด๋‹ค.

//kotlintest 
str should startWith("kot")
//JUnit APIs
assertTrue(str.startsWith("kot"))

internal DSL๋กœ HTML ๊ตฌ์ถ•

์ด ์„น์…˜์—์„œ๋Š” internal DSL๋กœ HTML ๊ตฌ์ถ•ํ•˜๋Š” ๋ฒ•์„ ์ข€ ๋” ์ž์„ธํ•˜๊ฒŒ ๋ฐฐ์šด๋‹ค.

//kotlinx.html library
fun createSimpleTable() = createHTML().
table {
	tr {
		td { +"cell" }
	}
}
//HTML 
<table>
	<tr>
		<td>cell</td>
	</tr>
</table>

createSimpleTable ํ•จ์ˆ˜๋Š” HTML ์กฐ๊ฐ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” string์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์™œ ์ง์ ‘ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์ฝ”ํ‹€๋ฆฐ์„ ํ†ตํ•ด HTML์„ ์ž‘์„ฑํ•ด์•ผ ํ• ๊นŒ? ๊ทธ ์ด์œ ๋Š” ์ฝ”ํ‹€๋ฆฐ์€ type-safeํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์ฝ”ํ‹€๋ฆฐ์—์„œ๋Š” tr ํƒœ๊ทธ ์•ˆ์—๋งŒ td ํƒœ๊ทธ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. ๋˜ ๋‹ค๋ฅธ ์ด์œ ๋Š” ์ฝ”ํ‹€๋ฆฐ์€ ๋‹ค๋ฅธ ์–ด๋–ค ์–ธ์–ด๋„ ๋‚ด๋ถ€์— ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์ด๋‹ค. ์ด ๋ง์€ ๋ชฉํ‘œํ•˜๋˜ ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•˜๊ณ  ์žˆ๋Š” HTML ์กฐ๊ฐ์„ ๋™์ ์œผ๋กœ ์ƒ์„ฑ ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ๋œป์ด๋‹ค.

//kotlinx.html library
fun createAnotherTable() = createHTML().table {
	val numbers = mapOf(1 to "one", 2 to "two")
	for ((num, string) in numbers) {
		tr {
			td { +"$num" }
			td { +string }
		}
	}
}
//HTML 
<table>
	<tr>
		<td>1</td>
		<td>one</td>
	</tr>
	<tr>
		<td>2</td>
		<td>two</td>
	</tr>
</table>

์ด์ œ DSL์ด ๋ฌด์—‡์ธ์ง€, ๊ทธ๋ฆฌ๊ณ  ์™œ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์€ ์ง€๋ฅผ ์•Œ์•˜์œผ๋‹ˆ ์ฝ”ํ‹€๋ฆฐ์ด ์ด๋ฅผ ์–ด๋–ป๊ฒŒ ์ง€์›ํ•˜๋Š” ์ง€๋ฅผ ์‚ดํŽด๋ณด์ž. ๋จผ์ € ์ˆ˜์‹  ๊ฐ์ฒด ์ง€์ • ๋žŒ๋‹ค๋ฅผ ๋ด์•ผ ํ•œ๋‹ค.

๊ตฌ์กฐํ™”๋œ API ๊ตฌ์ถ• : DSL์—์„œ ์ˆ˜์‹  ๊ฐ์ฒด ์ง€์ • ๋žŒ๋‹ค ์‚ฌ์šฉ

์ˆ˜์‹  ๊ฐ์ฒด ์ง€์ • ๋žŒ๋‹ค๋Š” ๊ตฌ์กฐํ™”๋œ API๋ฅผ ๋งŒ๋“ค ๋•Œ ๋„์›€์ด ๋˜๋Š” ๊ฐ•๋ ฅํ•œ ์ฝ”ํ‹€๋ฆฐ ๊ธฐ๋Šฅ์ด๋‹ค.

์ˆ˜์‹  ๊ฐ์ฒด ์ง€์ • ๋žŒ๋‹ค์™€ ํ™•์žฅ ํ•จ์ˆ˜ ํƒ€์ž…

์šฐ๋ฆฌ๋Š” 5.5์—์„œ with๋‚˜ apply๊ฐ™์€ scope function๋ฅผ ์†Œ๊ฐœํ•  ๋•Œ ์ˆ˜์‹  ๊ฐ์ฒด ์ง€์ • ๋žŒ๋‹ค์— ๋Œ€ํ•ด ์–ธ๊ธ‰ํ–ˆ์—ˆ๋‹ค. ์ด์ œ buildString ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ์ฝ”ํ‹€๋ฆฐ์ด ์ˆ˜์‹  ๊ฐ์ฒด ์ง€์ • ๋žŒ๋‹ค๋ฅผ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ์‚ดํŽด๋ณด์ž.

buildString์€ ํ•œ StringBuilder ๊ฐ์ฒด์— ์—ฌ๋Ÿฌ ๋‚ด์šฉ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋žŒ๋‹ค๋ฅผ ์ธ์ž๋กœ ๋ฐ›๋Š” buildString()์„ ์ •์˜ํ•ด๋ณด์ž.

fun buildString(
		builderAction: (StringBuilder) -> Unit
): String {
	val sb = StringBuilder()
	builderAction(sb)
	return sb.toString()
}

>>> val s = buildString {
... it.append("Hello, ")
... it.append("World!")
... }
>>> println(s)
Hello, World!

์ด ์ฝ”๋“œ๋Š” ์ดํ•ดํ•˜๊ธฐ ์‰ฝ์ง€๋งŒ ์‚ฌ์šฉํ•˜๊ธฐ ํŽธ๋ฆฌํ•˜์ง€๋Š” ์•Š๋Š”๋ฐ, ๊ทธ ์ด์œ ๋Š” ๋žŒ๋‹ค ๋ณธ๋ฌธ์—์„œ ๋งค๋ฒˆ it์„ ์‚ฌ์šฉํ•ด StringBuilder๋ฅผ ์ฐธ์กฐํ•ด์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์ˆ˜์‹  ๊ฐ์ฒด ์ง€์ • ๋žŒ๋‹ค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ it์ด๋ผ๋Š” ์ด๋ฆ„์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๋žŒ๋‹ค๋ฅผ ์ธ์ž๋กœ ๋„˜๊ฒจ๋ณด์ž.

fun buildString(
		builderAction: StringBuilder.() -> Unit
) : String {
	val sb = StringBuilder()
	sb.builderAction()
	return sb.toString()
}
>>> val s = buildString {
... 	this.append("Hello, ")
...	 append("World!")
... }
>>> println(s)
Hello, World!

๋‘ ์ฝ”๋“œ๋ฅผ ๋น„๊ตํ•ด ๋ณด๋ฉด, ์šฐ์„  builderAction๊ฐ€ ํ™•์žฅ ํ•จ์ˆ˜ ํƒ€์ž…์„ ์‚ฌ์šฉํ–ˆ๋‹ค.

//ํ™•์žฅ ํ•จ์ˆ˜ ํƒ€์ž… ์„ ์–ธ ๊ณผ์ •
(StringBuilder) -> StringBuilder() -> StringBuilder.()

์ด์ œ ๋žŒ๋‹ค๋ฅผ ์ˆ˜์‹  ์ธ์ž๋กœ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, it์„ ์ œ๊ฑฐํ•ด๋„ ๋œ๋‹ค. ๋”ฐ๋ผ์„œ this.append()์˜ ํ˜•ํƒœ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค.

๋‘ ๋ฒˆ์งธ๋กœ๋Š” buildString ํ•จ์ˆ˜์˜ ์„ ์–ธ์ด ๋‹ฌ๋ผ์กŒ๋‹ค. (StringBuilder) -> Unit๋ฅผ StringBuilder.() -> Unit๋กœ ๋Œ€์ฒดํ•˜์˜€๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ์™œ ๊ตณ์ด ํ™•์žฅ ํ•จ์ˆ˜ ํƒ€์ž…์„ ์‚ฌ์šฉํ• ๊นŒ? ํ™•์žฅ ํ•จ์ˆ˜์˜ ๋ณธ๋ฌธ์—์„œ๋Š” ํ™•์žฅ ๋Œ€์ƒ ํด๋ž˜์Šค์— ์ •์˜๋œ ๋ฉ”์†Œ๋“œ๋ฅผ ๋งˆ์น˜ ๊ทธ ํด๋ž˜์Šค ๋‚ด๋ถ€์—์„œ ํ˜ธ์ถœํ•˜๋“ฏ์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

์ฆ‰, sb.builderAction()์—์„œ builderAction์€ StringBuilder ํด๋ž˜์Šค ์•ˆ์— ์ •์˜๋œ ํ•จ์ˆ˜๊ฐ€ ์•„๋‹ˆ๋ฉฐ StringBuilder ์ธ์Šคํ„ด์Šค์ธ sb๋Š” ํ™•์žฅ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ์™€ ๋™์ผํ•œ ๊ตฌ๋ฌธ์œผ๋กœ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋Š” ํ•จ์ˆ˜ ํƒ€์ž…์˜ ์ธ์ž์ผ ๋ฟ์ด๋‹ค.

HTML builder์—์„œ ์ˆ˜์‹  ๊ฐ์ฒด ์ง€์ • ๋žŒ๋‹ค ์‚ฌ์šฉ

HTML์„ ์œ„ํ•œ Kotlin DSL์€ ๋ณดํ†ต HTML builder๋ผ๊ณ  ๋ถˆ๋ฆฐ๋‹ค. ์ด๋Š” type-safe builders๋ผ๋Š” ์ปจ์…‰์„ ๋Œ€ํ‘œํ•œ๋‹ค. Builder๋Š” ๊ฐ์ฒด ๊ณ„์ธต์„ ๋ช…์‹œ์ ์œผ๋กœ ํ‘œํ˜„ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋˜๋Š”๋ฐ, ์ด๋Š” XML ๋˜๋Š” UI ์š”์†Œ๋ฅผ ํŽธ๋ฆฌํ•˜๊ฒŒ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

์ฝ”ํ‹€๋ฆฐ๋„ ์œ„์™€ ๊ฐ™์€ ์•„์ด๋””์–ด๋ฅผ ์‚ฌ์šฉํ•˜์ง€๋งŒ, ์ฝ”ํ‹€๋ฆฐ builder๋Š” type-safeํ•˜๋‹ค.

fun createSimpleTable() = createHTML().
	table {
		tr {
			td { +"cell" }
		}
}

์œ„์˜ ์ฝ”ํ‹€๋ฆฐ ์ฝ”๋“œ์—์„œ table, tr, td๋Š” ๊ฐ๊ฐ ํ•จ์ˆ˜์ด๋‹ค. ์ด๋“ค์€ ๋ชจ๋‘ ๊ณ ์ฐจ ํ•จ์ˆ˜๋กœ, ๋žŒ๋‹ค๋ฅผ ์ˆ˜์‹  ๊ฐ์ฒด๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค. ์ด๋Ÿฌํ•œ ๋žŒ๋‹ค๋Š” *์ด๋ฆ„ ํ’€์ด ๊ทœ์น™(name-resolution rules)*์„ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋‹ค. table ํ•จ์ˆ˜์— ์ „๋‹ฌ๋œ ๋žŒ๋‹ค ์•ˆ์—์„œ๋Š” tr ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ๋žŒ๋‹ค ๋ฐ–์—์„œ๋Š” tr ํ•จ์ˆ˜๋Š” unresolved๋œ๋‹ค.

์•„๋ž˜๋Š” HTML builder๋ฅผ ์™„์ „ํ•˜๊ฒŒ ๊ตฌํ˜„ํ•œ ๊ฒƒ์ด๋‹ค.

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) //TR ํƒœ๊ทธ์˜ ์ƒˆ๋กœ์šด ์ธ์Šคํ„ด์Šค ์ถ”๊ฐ€
}
class TR : Tag("tr") {
	fun td(init: TD.() -> Unit) = doInit(TD(), init) //TD ํƒœ๊ทธ์˜ ์ƒˆ๋กœ์šด ์ธ์Šคํ„ด์Šค ์ถ”๊ฐ€
}
class TD : Tag("td")
fun createTable() =
	table {
		tr {
			td {
			}
		}
	}
>>> println(createTable())
<table><tr><td></td></tr></table>

๋ชจ๋“  ํƒœ๊ทธ๋Š” ์ค‘์ฒฉ ํƒœ๊ทธ์˜ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ , ์ด๋ฅผ ๋”ฐ๋ผ ์ˆœ์ฐจ์ ์œผ๋กœ ํƒœ๊ทธ๋ฅผ ๋งŒ๋“ค์–ด๊ฐ„๋‹ค.

Kotlin builders: ์ถ”์ƒํ™”์™€ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ

๋งŒ์•ฝ ํ”„๋กœ๊ทธ๋žจ์—์„œ ์ผ๋ฐ˜์ ์ธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ณ ์ž ํ•œ๋‹ค๋ฉด, ์ค‘๋ณต์„ ํ”ผํ•˜๊ณ  ์ฝ”๋“œ๋ฅผ ๋ณด๊ธฐ ์ข‹๊ฒŒ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ๋‹ค์–‘ํ•œ ๋„๊ตฌ๋“ค์ด ์žˆ๋‹ค. ๊ทธ ์ค‘ ํ•˜๋‚˜๋Š” ๋ฐ˜๋ณต์ ์ธ ์ฝ”๋“œ๋ฅผ ์ƒˆ๋กœ์šด ํ•จ์ˆ˜๋กœ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ด๋Ÿฌํ•œ ์ผ์€ SQL์ด๋‚˜ HTML์—์„œ ๊ต‰์žฅํžˆ ์–ด๋ ต๊ฑฐ๋‚˜ ์‹ฌ์ง€์–ด ๋ถˆ๊ฐ€๋Šฅํ•  ์ˆ˜๋„ ์žˆ๋‹ค. ์ด๋•Œ internal DSLs์„ ์‚ฌ์šฉํ•˜๋ฉด ์ถ”์ƒํ™”๋œ ๊ธฐ๋Šฅ์„ ๊ฐ€์ง€๋Š” ์ƒˆ๋กœ์šด ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

<div class="dropdown">
		<button class="btn dropdown-toggle">
			Dropdown
		<span class="caret"></span>
	</button>
	<ul class="dropdown-menu">
		<li><a href="#">Action</a></li>
		<li><a href="#">Another action</a></li>
		<li role="separator" class="divider"></li>
		<li class="dropdown-header">Header</li>
		<li><a href="#">Separated link</a></li>
	</ul>
</div>

์œ„์˜ ์ฝ”๋“œ์—์„œ๋Š” div, button, ul, li์™€ ๊ฐ™์€ ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ๊ฐ™์€ ๊ตฌ์กฐ๋ฅผ ๋ฐ˜๋ณต์ ์œผ๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๊ฒƒ๋ณด๋‹ค ๋” ์ž˜ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•๋„ ์กด์žฌํ•œ๋‹ค.

fun dropdownExample() = createHTML().dropdown {
	dropdownButton { +"Dropdown" }
	dropdownMenu {
		item("#", "Action")
		item("#", "Another action")
		divider()
		dropdownHeader("Header")
		item("#", "Separated link")
	}
}

์œ„์˜ ์ฝ”๋“œ๋Š” ํ•จ์ˆ˜ํ™”๋ฅผ ํ–ˆ์œผ๋ฉด์„œ๋„ ๋ถˆํ•„์š”ํ•œ ๋””ํ…Œ์ผ์„ ์ˆจ๊ธฐ๊ณ  ์žˆ์–ด ๋” ๋ณด๊ธฐ ์ข‹์€ ์ฝ”๋“œ๋ฅผ ๋งŒ๋“ ๋‹ค.

invoke convention์„ ์‚ฌ์šฉํ•œ ๋” ์œ ์—ฐํ•œ ๋ธ”๋ก ์ค‘์ฒฉ

invoke convention์„ ์‚ฌ์šฉํ•˜๋ฉด ํ•จ์ˆ˜์ฒ˜๋Ÿผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ์ฒด๋ฅผ ๋งŒ๋“œ๋Š” ํด๋ž˜์Šค๋ฅผ ์ •์˜ํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด ๊ธฐ๋Šฅ์€ ์ผ์ƒ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ ๋งŒ๋“ค์–ด์ง„ ๊ธฐ๋Šฅ์€ ์•„๋‹ˆ๋‹ค. invoke convention์„ ๋‚จ์šฉํ•˜๋ฉด 1()๊ณผ ๊ฐ™์ด ์ดํ•ดํ•˜๊ธฐ ์–ด๋ ค์šด ์ฝ”๋“œ๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ DSL์—์„œ๋Š” invoke convention์ด ์œ ์šฉํ•œ ๊ฒฝ์šฐ๊ฐ€ ์ž์ฃผ ์žˆ๋‹ค.

invoke convention : ํ•จ์ˆ˜์ฒ˜๋Ÿผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ์ฒด

์šฐ๋ฆฌ๋Š” ์ด๋ฏธ 7์žฅ์—์„œ ์ฝ”ํ‹€๋ฆฐ์˜ convention์— ๋Œ€ํ•˜์—ฌ ํ•™์Šตํ•˜์˜€๋‹ค. ๊ฐ€๋ น foo๋ผ๋Š” ๋ณ€์ˆ˜๊ฐ€ ์žˆ๊ณ  foo[bar]๋ผ๋Š” ์‹์„ ์‚ฌ์šฉํ•˜๋ฉด ์ด๋Š” foo.get(bar)๋กœ ๋ณ€ํ™˜๋œ๋‹ค. ์ด๋•Œ get์€ Foo๋ผ๋Š” ํด๋ž˜์Šค ์•ˆ์— ์ •์˜๋œ ํ•จ์ˆ˜์ด๊ฑฐ๋‚˜ ํ™•์žฅ ํ•จ์ˆ˜์—ฌ์•ผ ํ•œ๋‹ค.

invoke convention ์—ญ์‹œ ๊ฐ™์€ ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•˜๋Š”๋ฐ, get๊ณผ๋Š” ๋‹ค๋ฅด๊ฒŒ ๊ด„ํ˜ธ()๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. operator ๋ณ€๊ฒฝ์ž๊ฐ€ ๋ถ™์€ invoke ๋ฉ”์†Œ๋“œ ์ •์˜๊ฐ€ ๋“ค์–ด์žˆ๋Š” ํด๋ž˜์Šค์˜ ๊ฐ์ฒด๋Š” ํ•จ์ˆ˜์ฒ˜๋Ÿผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋‹ค. ๋‹ค์Œ์€ ์ด์— ๋Œ€ํ•œ ์˜ˆ์‹œ์ด๋‹ค.

class Greeter(val greeting: String) {
	operator fun invoke(name: String) { //invoke ๋ฉ”์†Œ๋“œ ์ •์˜
		println("$greeting, $name!")
	}
}
>>> val bavarianGreeter = Greeter("Servus") //ํ•จ์ˆ˜๋กœ์จ Greeter ์ธ์Šคํ„ด์Šค ํ˜ธ์ถœ
>>> bavarianGreeter("Dmitry")
Servus, Dmitry!

์œ„์— ์ฝ”๋“œ์—์„œ๋Š” bavarianGreeter ๊ฐ์ฒด๊ฐ€ ๋งˆ์น˜ ํ•จ์ˆ˜์ฒ˜๋Ÿผ ํ˜ธ์ถœ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด ๋•Œ bavarianGreeter(โ€œDmitryโ€)๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ bavarianGreeter.invoke(โ€œDmitryโ€)๋กœ ์ปดํŒŒ์ผ๋œ๋‹ค.

invoke convention : convention๊ณผ ํ•จ์ˆ˜ํ˜• ํƒ€์ž…

invoke convention์—์„œ ๋ฐฐ์› ๊ธฐ ๋•Œ๋ฌธ์—, ์šฐ๋ฆฌ๋Š” ์ผ๋ฐ˜์ ์ธ ๋žŒ๋‹ค ํ˜ธ์ถœ ๋ฐฉ์‹(๋žŒ๋‹ค ๋’ค์— ๊ด„ํ˜ธ๋ฅผ ๋ถ™์ด๋Š” ๋ฐฉ์‹: lambda())์ด ์‹ค์ œ๋กœ๋Š” invoke convention์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์— ์ง€๋‚˜์ง€ ์•Š์Œ์„ ์ถฉ๋ถ„ํžˆ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

์ธ๋ผ์ธ๋œ ๋žŒ๋‹ค๋ฅผ ์ œ์™ธํ•œ ๋ชจ๋“  ๋žŒ๋‹ค๋Š” ํ•จ์ˆ˜ํ˜• ์ธํ„ฐํŽ˜์ด์Šค(Function1 ๋“ฑ)์„ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค๋กœ ์ปดํŒŒ์ผ๋œ๋‹ค. ๊ฐ ํ•จ์ˆ˜ํ˜• ์ธํ„ฐํŽ˜์ด์Šค ์•ˆ์—๋Š” ๊ทธ ์ธํ„ฐํŽ˜์ด์Šค ์ด๋ฆ„์ด ๊ฐ€๋ฆฌํ‚ค๋Š” ๊ฐœ์ˆ˜๋งŒํผ(์˜ˆ๋ฅผ ๋“ค์–ด, Function1์ด๋ผ๋ฉด 1๊ฐœ**) ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ›๋Š” invoke ๋ฉ”์†Œ๋“œ**๊ฐ€ ๋“ค์–ด์žˆ๋‹ค.

๋žŒ๋‹ค๋ฅผ ํ•จ์ˆ˜์ฒ˜๋Ÿผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์œผ๋ฉด ์–ด๋–ค ์ ์ด ์ข‹์„๊นŒ? ์šฐ์„  ๋ณต์žกํ•œ ๋žŒ๋‹ค๋ฅผ ์—ฌ๋Ÿฌ ๋ฉ”์†Œ๋“œ๋กœ ๋ถ„๋ฆฌํ•˜๋ฉด์„œ๋„ ์—ฌ์ „ํžˆ ๋ถ„๋ฆฌ ์ „์˜ ๋žŒ๋‹ค์ฒ˜๋Ÿผ ์™ธ๋ถ€์—์„œ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

DSL์˜ invoke convention : Gradle์—์„œ ์˜์กด๊ด€๊ณ„ ์ •์˜

dependencies.compile("junit:junit:4.11")// ์ฒซ ๋ฒˆ์งธ๋ฐฉ์‹

dependencies {// ๋‘ ๋ฒˆ์งธ ๋ฐฉ์‹
	compile("junit:junit:4.11")
}

์œ„ ์ฝ”๋“œ์—์„œ, ์ฒซ ๋ฒˆ์งธ ๊ฒฝ์šฐ๋Š” dependenices ๋ณ€์ˆ˜์— ๋Œ€ํ•ด compile ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๊ณ , ๋‘ ๋ฒˆ์งธ ๊ฒฝ์šฐ์—๋Š” dependenices ์•ˆ์— ๋žŒ๋‹ค๋ฅผ ๋ฐ›๋Š” invoke ๋ฉ”์†Œ๋“œ๋ฅผ ์ •์˜ํ•˜๋ฉด ๋‘ ๋ฒˆ์งธ ๋ฐฉ์‹์˜ ํ˜ธ์ถœ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด๋Ÿฌํ•œ invoke convention์œผ๋กœ ์ธํ•ด DSL API์˜ ์œ ์—ฐ์„ฑ์ด ์ปค์ง€๊ฒŒ ๋œ๋‹ค.

์‹ค์ „ ์ฝ”ํ‹€๋ฆฐ DSL

์ด์ œ๋ถ€ํ„ฐ๋Š” ์‹ค์šฉ์ ์ธ DSL ์˜ˆ์ œ, ์ฆ‰ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ, ๋‚ ์งœ ๋ฆฌํ„ฐ๋Ÿด, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์งˆ์˜ ๋“ฑ์— ๋Œ€ํ•ด ์‚ดํŽด๋ณด๋„๋ก ํ•˜์ž.

์ค‘์œ„ ํ˜ธ์ถœ ์—ฐ์‡„ : ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ์˜ should

์•ž์„œ ์‚ดํŽด๋ณธ kotlintest DSL์—์„œ ์ค‘์œ„ ํ˜ธ์ถœ์„ ์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•˜๋Š”์ง€ ์‚ดํŽด๋ณด์ž.

s should startWith("kot")์™€ ๊ฐ™์€ ์ฝ”๋“œ์—์„œ, s์— ๋“ค์–ด๊ฐ„ ๊ฐ’์ด kot๋กœ ์‹œ์ž‘ํ•˜์ง€ ์•Š์œผ๋ฉด ์ด ๋‹จ์–ธ์€ ์—๋Ÿฌ๊ฐ€ ๋‚˜๊ฒŒ ๋œ๋‹ค. ์ด ์ฝ”๋“œ๋Š” ๋งˆ์น˜ The s string should start with this constant์ฒ˜๋Ÿผ ์ฝํžˆ๊ฒŒ ๋œ๋‹ค. ์ด ๋ชฉ์ ์„ ๋‹ฌ์„ฑํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” should ํ•จ์ˆ˜ ์„ ์–ธ ์•ž์— infix ๋ณ€๊ฒฝ์ž๋ฅผ ๋ถ™์—ฌ์•ผ ํ•œ๋‹ค(๊ทธ๋ž˜์•ผ ์ค‘์œ„ ํ•จ์ˆ˜๊ฐ€ ๋˜๋ฏ€๋กœ).

infix fun <T> T.should(matcher: Matcher<T>) = matcher.test(this)

์œ„์™€ ๊ฐ™์ด should ํ•จ์ˆ˜๋ฅผ ๊ตฌํ˜„ํ•  ๋•Œ, should ํ•จ์ˆ˜๋Š” Matcher์˜ ์ธ์Šคํ„ด์Šค๋ฅผ ์š”๊ตฌํ•œ๋‹ค. Matcher๋Š” ๊ฐ’์— ๋Œ€ํ•œ ๋‹จ์–ธ๋ฌธ์„ ํ‘œํ˜„ํ•˜๋Š” ์ œ๋„ค๋ฆญ ์ธํ„ฐํŽ˜์ด์Šค๋กœ, startWith๋กœ ๊ตฌํ˜„๋˜๋ฉฐ, ์ด๋Š” ์–ด๋–ค ๋ฌธ์ž์—ด์ด ์ฃผ์–ด์ง„ ๋ฌธ์ž์—ด๋กœ ์‹œ์ž‘ํ•˜๋Š”์ง€ ๊ฒ€์‚ฌํ•œ๋‹ค.

kotlintest DSL์—์„œ ์—ฐ์‡„์ ์ธ ํ˜ธ์ถœ์„ ์‚ฌ์šฉํ•˜๋ฉด, ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋งŒ๋“ค ์ˆ˜๋„ ์žˆ๋‹ค.

"kotlin" should start with "kot"

์ด ๋ฌธ์žฅ์€ ์ฝ”ํ‹€๋ฆฐ์ฒ˜๋Ÿผ ๋ณด์ด์ง€ ์•Š๋Š”๋‹ค. ์ด ๋ฌธ์žฅ์ด ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ๋ณด๊ธฐ ์œ„ํ•ด์„œ๋Š” ์ค‘์œ„ ํ˜ธ์ถœ์„ ์ผ๋ฐ˜์ ์ธ ๊ฒƒ์œผ๋กœ ๊ณ ์ณ์•ผ ํ•œ๋‹ค.

"kotlin".should(start).with("kot")

์œ„์˜ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด์„œ ์šฐ๋ฆฌ๋Š” should์™€ with๊ฐ€ ์—ฐ์‡„์ ์œผ๋กœ ์ค‘์œ„ ํ˜ธ์ถœ์„ ํ•œ๋‹ค๋Š” ์‚ฌ์‹ค์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

์›์‹œ ํƒ€์ž…์— ๋Œ€ํ•œ ํ™•์žฅ ํ•จ์ˆ˜ ์ •์˜: ๋‚ ์งœ ์ฒ˜๋ฆฌ

์ฑ•ํ„ฐ๊ฐ€ ์‹œ์ž‘ํ•  ๋•Œ ๋ณด์•˜๋˜ ์˜ˆ์ œ ์ค‘ ๋‚ ์งœ์— ๋Œ€ํ•ด์„œ ์‚ดํŽด๋ณด์ž.

**val** yesterday = 1.days.ago
**val** tomorrow = 1.days.fromNow

์ด DSL์„ java.time API์™€ ์ฝ”ํ‹€๋ฆฐ์„ ํ†ตํ•ด ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋ช‡ ์ค„์˜ ์ฝ”๋“œ๋งŒ ์žˆ์œผ๋ฉด ๋œ๋‹ค.

import java.time.Period
import java.time.LocalDate

val Int.days: Period
    get() = Period.ofDays(this)

val Period.ago: LocalDate
    get() = LocalDate.now() - this

val Period.fromNow: LocalDate
    get() = LocalDate.now() + this

println(1.days.ago)
// 2020-05-15
println(1.days.fromNow)
// 2020-05-17

๋ฉค๋ฒ„ ํ™•์žฅ ํ•จ์ˆ˜ : SQL์„ ์œ„ํ•œ ๋‚ด๋ถ€ DSL

์ด์ œ ๋ฉค๋ฒ„ ํ™•์žฅ์„ ์‚ฌ์šฉํ•˜๋Š” ์˜ˆ์ œ๋ฅผ ์‚ดํŽด๋ณด์ž. ๋‹ค์Œ์€ ์ต์Šคํฌ์ฆˆ๋“œ ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ์ œ๊ณตํ•œ SQL์„ ์œ„ํ•œ internal DSL์—์„œ ๊ฐ€์ ธ์˜จ ์˜ˆ์ œ์ด๋‹ค. ์ต์Šคํฌ์ฆˆ๋“œ ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ SQL๋กœ ํ…Œ์ด๋ธ”์„ ๋‹ค๋ฃจ๊ธฐ ์œ„ํ•ด์„œ๋Š” Table ํด๋ž˜์Šค๋ฅผ ํ™•์žฅํ•œ ๊ฐ์ฒด๋กœ ๋Œ€์ƒ ํ…Œ์ด๋ธ”์„ ์ •์˜ํ•ด์•ผ ํ•œ๋‹ค.

object Country: Table() {
    val id = integer("id").autoIncrement().primaryKey()
    val name = varchar("name", 50)
}

์œ„์˜ ์„ ์–ธ์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ํ…Œ์ด๋ธ”๊ณผ ์ผ์น˜ํ•œ๋‹ค. ์ด ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” SchemaUtils.create(Country) ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ SQL์„ ๊ตฌํ˜„ํ•ด์•ผ ํ•œ๋‹ค.

CREATE TABLE IF NOT EXISTS Country (
	id INT AUTO_INCREMENT NOT NULL,
	name VARCHAR(50) NOT NULL,
	CONSTRAINT pk_Country PRIMARY KEY (id)
)

Last updated