본문으로 건너뛰기

🎨 DSL 만들기

📖 DSL이란?

**DSL(Domain-Specific Language)**은 특정 도메인에 특화된 언어입니다. Kotlin으로 읽기 쉽고 직관적인 API를 만들 수 있습니다!

💡 기본 개념

람다와 수신자

// 일반 람다
fun buildString1(action: () -> String): String {
return action()
}

// 수신자가 있는 람다
fun buildString2(action: StringBuilder.() -> Unit): String {
val builder = StringBuilder()
builder.action()
return builder.toString()
}

fun main() {
val result = buildString2 {
append("Hello ") // this.append("Hello ")
append("World!")
}
println(result) // Hello World!
}

apply와 with

class Person {
var name: String = ""
var age: Int = 0
}

fun main() {
// apply - 객체 반환
val person1 = Person().apply {
name = "홍길동"
age = 25
}

// with - 결과 반환
val greeting = with(person1) {
"안녕하세요, ${name}님! (${age}세)"
}

println(greeting)
}

🎯 HTML DSL

간단한 HTML 빌더

class HtmlBuilder {
private val content = StringBuilder()

fun h1(text: String) {
content.append("<h1>$text</h1>\n")
}

fun p(text: String) {
content.append("<p>$text</p>\n")
}

fun div(block: HtmlBuilder.() -> Unit) {
content.append("<div>\n")
block()
content.append("</div>\n")
}

override fun toString() = content.toString()
}

fun html(block: HtmlBuilder.() -> Unit): String {
val builder = HtmlBuilder()
builder.block()
return builder.toString()
}

fun main() {
val page = html {
h1("Welcome!")
p("This is a paragraph.")
div {
h1("Section")
p("Content here.")
}
}

println(page)
}

속성 추가

class Tag(val name: String) {
private val attributes = mutableMapOf<String, String>()
private val children = mutableListOf<Tag>()
var text: String = ""

fun attribute(key: String, value: String) {
attributes[key] = value
}

operator fun String.unaryPlus() {
text = this
}

override fun toString(): String {
val attrs = attributes.entries.joinToString(" ") { "${it.key}=\"${it.value}\"" }
val attrsString = if (attrs.isNotEmpty()) " $attrs" else ""

return if (children.isEmpty() && text.isEmpty()) {
"<$name$attrsString />"
} else {
val childrenString = children.joinToString("\n") { it.toString() }
val content = if (text.isNotEmpty()) text else childrenString
"<$name$attrsString>$content</$name>"
}
}
}

fun tag(name: String, block: Tag.() -> Unit): Tag {
val tag = Tag(name)
tag.block()
return tag
}

fun main() {
val div = tag("div") {
attribute("class", "container")
+"Hello World"
}

println(div)
// <div class="container">Hello World</div>
}

🔧 SQL DSL

쿼리 빌더

class Query {
private var table: String = ""
private val columns = mutableListOf<String>()
private var whereClause: String = ""

fun from(table: String) {
this.table = table
}

fun select(vararg columns: String) {
this.columns.addAll(columns)
}

fun where(condition: String) {
whereClause = condition
}

fun build(): String {
val cols = if (columns.isEmpty()) "*" else columns.joinToString(", ")
var sql = "SELECT $cols FROM $table"
if (whereClause.isNotEmpty()) {
sql += " WHERE $whereClause"
}
return sql
}
}

fun query(block: Query.() -> Unit): String {
val query = Query()
query.block()
return query.build()
}

fun main() {
val sql = query {
select("id", "name", "email")
from("users")
where("age > 20")
}

println(sql)
// SELECT id, name, email FROM users WHERE age > 20
}

🎨 설정 DSL

앱 설정

class AppConfig {
var host: String = "localhost"
var port: Int = 8080
var debug: Boolean = false

private val routes = mutableListOf<Route>()

fun route(path: String, block: Route.() -> Unit) {
val route = Route(path)
route.block()
routes.add(route)
}

fun showConfig() {
println("=== Config ===")
println("Host: $host")
println("Port: $port")
println("Debug: $debug")
println("\n=== Routes ===")
routes.forEach { println(it) }
}
}

class Route(val path: String) {
var method: String = "GET"
var handler: String = ""

override fun toString() = "$method $path -> $handler"
}

fun configure(block: AppConfig.() -> Unit): AppConfig {
val config = AppConfig()
config.block()
return config
}

fun main() {
val config = configure {
host = "example.com"
port = 3000
debug = true

route("/api/users") {
method = "GET"
handler = "listUsers"
}

route("/api/users") {
method = "POST"
handler = "createUser"
}
}

config.showConfig()
}

🎯 테스트 DSL

간단한 테스트 프레임워크

class TestSuite(val name: String) {
private val tests = mutableListOf<Test>()

fun test(name: String, block: TestContext.() -> Unit) {
tests.add(Test(name, block))
}

fun run() {
println("=== $name ===\n")
var passed = 0
var failed = 0

for (test in tests) {
val context = TestContext()
try {
test.block(context)
println("✓ ${test.name}")
passed++
} catch (e: AssertionError) {
println("✗ ${test.name}: ${e.message}")
failed++
}
}

println("\n결과: $passed passed, $failed failed")
}
}

class Test(val name: String, val block: TestContext.() -> Unit)

class TestContext {
infix fun <T> T.shouldBe(expected: T) {
if (this != expected) {
throw AssertionError("Expected $expected but was $this")
}
}
}

fun describe(name: String, block: TestSuite.() -> Unit) {
val suite = TestSuite(name)
suite.block()
suite.run()
}

fun main() {
describe("Calculator") {
test("addition") {
val result = 2 + 3
result shouldBe 5
}

test("subtraction") {
val result = 5 - 2
result shouldBe 3
}

test("multiplication") {
val result = 3 * 4
result shouldBe 12
}
}
}

🏗️ 빌더 패턴

사용자 빌더

class User private constructor(
val name: String,
val email: String?,
val age: Int?,
val address: String?
) {
class Builder {
private var name: String = ""
private var email: String? = null
private var age: Int? = null
private var address: String? = null

fun name(name: String) = apply { this.name = name }
fun email(email: String) = apply { this.email = email }
fun age(age: Int) = apply { this.age = age }
fun address(address: String) = apply { this.address = address }

fun build(): User {
require(name.isNotBlank()) { "Name is required" }
return User(name, email, age, address)
}
}

override fun toString() = "User(name=$name, email=$email, age=$age, address=$address)"
}

fun user(block: User.Builder.() -> Unit): User {
return User.Builder().apply(block).build()
}

fun main() {
val user = user {
name("홍길동")
email("hong@example.com")
age(25)
address("서울")
}

println(user)
}

🤔 자주 묻는 질문

Q1. DSL은 언제 쓰나요?

A: 반복적인 작업을 깔끔하게!

// ❌ 장황한 코드
val config = Config()
config.setHost("localhost")
config.setPort(8080)
config.addRoute("/api", "GET", "handler")

// ✅ DSL
val config = configure {
host = "localhost"
port = 8080
route("/api") {
method = "GET"
handler = "handler"
}
}

Q2. @DslMarker는 뭔가요?

A: 중첩 DSL 제한!

@DslMarker
annotation class HtmlDsl

@HtmlDsl
class HtmlTag {
fun div(block: HtmlTag.() -> Unit) { }
}

fun html(block: HtmlTag.() -> Unit) {
val tag = HtmlTag()
tag.block()
}

Q3. 성능은?

A: 일반 코드와 거의 같음!

// 컴파일 후 거의 같은 바이트코드 생성
val result1 = buildString {
append("Hello")
}

val builder = StringBuilder()
builder.append("Hello")
val result2 = builder.toString()

🎬 마치며

DSL로 아름다운 API를 만드세요!

핵심 정리:
✅ 수신자 람다 활용
✅ apply, with로 빌더 패턴
✅ 직관적인 문법
✅ 타입 안전성 유지
✅ 도메인 특화 언어

축하합니다! Practical 시리즈를 완료했습니다! 🎉

다음 단계: 코루틴에서 비동기 프로그래밍을 배워보세요!