본문 바로가기
language/Kotilin In Action

6주차 (13장)

by abstract.jiin 2025. 9. 1.

13.1 API에서 DSL로

목표: 깔끔한 API 만들기

깔끔한 API의 2가지 조건

*// 1. 명확성 - 무슨 일이 일어날지 명확하게 이해 가능*
val user = User.Builder()
    .name("김철수")
    .email("chulsu@email.com")
    .build()

*// 2. 간결성 - 불필요한 구문 최소화*
val user = user {
    name = "김철수"
    email = "chulsu@email.com"
}

코틀린의 간결한 구문들

Before vs After 비교

*// 일반 구문 (번잡함)*
StringUtil.capitalize(s)
1.to("one")
set.add(2)
map.get("key")
file.use({ f -> f.read() })

val m = mutableListOf<Int>()
m.add(1)
m.add(2)
return m.toList()

*// 간결한 구문 (깔끔함)*
s.capitalize()          *// 확장 함수*
1 to "one"              *// 중위 호출*
set += 2                *// 연산자 오버로딩*
map["key"]              *// get 메서드 관례*
file.use { it.read() }  *// 람다 괄호 밖으로*

with(sb) {              *// 수신 객체 지정 람다*
    append("yes")
    append("no")
}

return buildList {      *// 람다를 받는 빌더 함수*
    add(1)
    add(2)
}

DSL 맛보기

1. 날짜 연산 DSL

*// 마치 자연어처럼!*
val yesterday = Clock.System.now() - 1.days
val nextWeek = Clock.System.now() + 7.days
val twoMonthsAgo = Clock.System.now() - 2.months

2. HTML 생성 DSL

*// 마치 HTML을 직접 작성하는 것처럼!*
fun createSimpleTable() = createHTML().table {
    tr {
        td { +"cell" }
    }
}

*// 생성되는 HTML:
// <table>
//   <tr>
//     <td>cell</td>
//   </tr>
// </table>*

DSL이 뭔가요?

DSL vs 범용 프로그래밍 언어

*// DSL (Domain Specific Language) - 특정 영역에 특화
// 예시: SQL, 정규식, HTML
// SQL - 데이터베이스 전용*
SELECT name, age FROM users WHERE age > 18

*// 정규식 - 문자열 패턴 매칭 전용*
\\d{4}-\\d{2}-\\d{2}  
*// 날짜 형식
// 범용 언어 - 모든 문제 해결 가능
// 예시: Java, Kotlin, Python*
class User(val name: String, val age: Int)
fun processUsers(users: List<User>) { ... }

DSL의 특징

  1. 특화됨: 특정 도메인만 처리
  2. 선언적: "무엇을" 원하는지 기술 (어떻게는 엔진이 처리)
  3. 간결함: 해당 영역에서는 매우 간결

외부 DSL vs 내부 DSL

외부 DSL - 별도 언어

*// SQL (외부 DSL)*
val sqlQuery = """
    SELECT Country.name, COUNT(Customer.id)
    FROM Country
    INNER JOIN Customer
    ON Country.id = Customer.country_id
    GROUP BY Country.name
    ORDER BY COUNT(Customer.id) DESC
    LIMIT 1
"""

*// 문제점:
// 1. 문자열로 저장 → 컴파일 시점 검증 불가
// 2. IDE 지원 제한적
// 3. 호스트 언어와 연동 어려움*

내부 DSL - 코틀린 코드

*// Exposed 프레임워크 (내부 DSL)*
(Country innerJoin Customer)
    .slice(Country.name, Count(Customer.id))
    .selectAll()
    .groupBy(Country.name)
    .orderBy(Count(Customer.id), order = SortOrder.DESC)
    .limit(1)

*// 장점:
// 1. 일반 코틀린 코드 → 컴파일 시점 검증
// 2. 완전한 IDE 지원
// 3. 다른 코틀린 코드와 자유롭게 조합*

DSL의 구조

일반 API vs DSL

*// 명령-질의 API (Command-Query API)
// 한 번에 하나씩 호출, 맥락 없음*
project.dependencies.add("testImplementation", kotlin("test"))
project.dependencies.add("implementation", "org.jetbrains.exposed:exposed-core:0.40.1")
project.dependencies.add("implementation", "org.jetbrains.exposed:exposed-dao:0.40.1")

*// DSL - 구조와 맥락 있음*
dependencies {  
*// 람다 내포로 구조 생성*
    testImplementation(kotlin("test"))
    implementation("org.jetbrains.exposed:exposed-core:0.40.1")
    implementation("org.jetbrains.exposed:exposed-dao:0.40.1")
}

테스트 프레임워크 비교

*// 일반 API*
assertTrue(str.startsWith("kot"))

*// DSL (코테스트)*
str should startWith("kot")  *// 메서드 연쇄로 구조 생성*

HTML DSL 자세히 보기

기본 테이블 생성

import kotlinx.html.stream.createHTML
import kotlinx.html.*

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

*// 결과:
// <table>
//   <tr>
//     <td>cell</td>
//   </tr>
// </table>*

동적 테이블 생성 - 진짜 파워!

kotlin

fun createDynamicTable() = createHTML().table {
    val numbers = mapOf(1 to "one", 2 to "two", 3 to "three")
    
    for ((num, string) in numbers) {
        tr {
            td { +"$num" }
            td { +string }
        }
    }
}

*// 결과:
// <table>
//   <tr><td>1</td><td>one</td></tr>
//   <tr><td>2</td><td>two</td></tr>
//   <tr><td>3</td><td>three</td></tr>
// </table>*

HTML DSL의 장점

*// 타입 안전성*
table {
    tr {
        td { +"내용" }  *// 올바른 중첩*
    }
    *// td { +"내용" }   // 컴파일 에러 td는 tr 안에만*
}

*// 코틀린 코드 자유롭게 사용*
table {
    *// 조건문 사용*
    if (showHeader) {
        tr {
            th { +"이름" }
            th { +"나이" }
        }
    }
    
    *// 반복문 사용*
    users.forEach { user ->
        tr {
            td { +user.name }
            td { +"${user.age}" }
        }
    }
}

DSL 구조 만드는 방법들

1. 람다 내포 (Lambda Nesting)

dependencies {           *// 바깥 람다*
    implementation {     *// 안쪽 람다*
        group = "com.example"
        name = "library"
        version = "1.0"
    }
}

2. 메서드 연쇄 (Method Chaining)

query
    .select("name", "age")
    .from("users")
    .where("age > 18")
    .orderBy("name")
    .limit(10)

3. 수신 객체 지정 람다

buildString {        *// StringBuilder가 수신 객체*
    append("Hello")  *// this.append("Hello")*
    append(" ")
    append("World")
}

실제 DSL 사용 예시들

1. Gradle 빌드 스크립트

plugins {
    kotlin("jvm") version "1.8.0"
    application
}

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
    testImplementation(kotlin("test"))
}

tasks.test {
    useJUnitPlatform()
}

2. 안드로이드 UI (Compose)

@Composable
fun Greeting(name: String) {
    Column {
        Text(
            text = "Hello $name!",
            fontSize = 24.sp
        )
        Button(
            onClick = { */* 클릭 처리 */* }
        ) {
            Text("클릭하세요")
        }
    }
}

3. 스프링 설정

@Configuration
class DatabaseConfig {
    
    @Bean
    fun dataSource() = HikariDataSource().apply {
        jdbcUrl = "jdbc:postgresql://localhost/mydb"
        username = "user"
        password = "password"
    }
}

핵심 정리

DSL의 목표

  • 가독성: 도메인 전문가도 읽을 수 있는 코드
  • 표현력: 해당 영역의 개념을 자연스럽게 표현
  • 안전성: 컴파일 시점 검증
  • 생산성: 적은 코드로 많은 일

내부 DSL의 장점

*// 1. 타입 안전성
// 2. IDE 지원 (자동완성, 리팩토링)
// 3. 호스트 언어와 매끄러운 통합
// 4. 컴파일 시점 검증
// 5. 디버깅 가능*

DSL vs API 구분법

  • API: 개별 함수들의 조합
  • DSL: 문법과 구조가 있는 언어

13.2 구조화된 API 구축: DSL에서 수신 객체 지정 람다 사용


수신 객체 지정 람다 - DSL의 핵심!

일반 람다 vs 수신 객체 지정 람다

*// 일반 람다 - 번잡함*
fun buildString(builderAction: (StringBuilder) -> Unit): String {
    val sb = StringBuilder()
    builderAction(sb)  *// sb를 인자로 전달*
    return sb.toString()
}

val s = buildString {
    it.append("Hello, ")  *// 매번 it. 필요* 
    it.append("World!")
}

*// 수신 객체 지정 람다 - 깔끔함*
fun buildString(builderAction: StringBuilder.() -> Unit): String {
    val sb = StringBuilder()
    sb.builderAction()  *// sb를 수신 객체로 전달*
    return sb.toString()
}

val s = buildString {
    append("Hello, ")  *// this. 생략 가능* 
    append("World!")
}

확장 함수 타입 이해하기

문법 분해

*// 일반 함수 타입*
(StringBuilder) -> Unit

*// 확장 함수 타입 (수신 객체 지정)*
StringBuilder.() -> Unit
*//     ↑         ↑
// 수신 객체    반환 타입
// 타입*

복잡한 예시

*// String.(Int, Int) -> Unit
// ↑      ↑     ↑      ↑
// 수신   파라미터1  파라미터2  반환타입
// 객체   타입      타입      
// 타입*

val complexLambda: String.(Int, Int) -> Unit = { start, end ->
    println("$this: ${start}부터 ${end}까지")  *// this = String 수신 객체*
}

"Hello".complexLambda(0, 3)  *// Hello: 0부터 3까지*

HTML 빌더에서 수신 객체 지정 람다

각 블록의 수신 객체 타입

table {           *// 수신 객체: TABLE*
    tr {          *// 수신 객체: TR*
        td {      *// 수신 객체: TD*
            +"cell"  *// unaryPlus 연산자 오버로딩*
        }
    }
}

HTML 빌더 구현

기본 태그 클래스들

@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>"
}

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 table(init: TABLE.() -> Unit) = TABLE().apply(init)

동작 과정 시뮬레이션

fun createTable() = table {  *// TABLE() 생성 → apply(init)*
    tr {                     *// TR() 생성 → doInit 호출*
        td {                 *// TD() 생성 → doInit 호출*
        }
    }
}

*// 내부 동작:
// 1. TABLE() 인스턴스 생성
// 2. tr 호출 → TR() 생성하고 TABLE의 children에 추가
// 3. td 호출 → TD() 생성하고 TR의 children에 추가
// 4. toString() 호출하여 HTML 문자열 생성*

@DslMarker - 혼동 방지

문제 상황

*// @DslMarker 없으면 이런 혼동 발생 가능*
createHTML().body {
    a {              *// a 태그 (href 프로퍼티 있음)*
        img {        *// img 태그 내부*
            href = "https://..."  *// 😱 어떤 href? a의 href?*
        }
    }
}

@DslMarker로 해결

@DslMarker
annotation class HtmlTagMarker

@HtmlTagMarker
open class Tag  *// 모든 태그에 마커 적용
// 이제 img 블록 안에서 a의 href에 접근하려 하면:
// "암시적 수신 객체로는 호출할 수 없음. 명시적으로 지정하세요" 컴파일 에러*

apply와 with 구현

apply - 수신 객체 반환

inline fun <T> T.apply(block: T.() -> Unit): T {
    block()        *// this.block()와 같음*
    return this    *// 수신 객체 자신을 반환*
}

*// 사용*
val sb = StringBuilder().apply {
    append("Hello")
    append(" World")
}  *// sb는 StringBuilder*

with - 람다 결과 반환

inline fun <T, R> with(receiver: T, block: T.() -> R): R =
    receiver.block()  *// 람다 실행 결과 반환// 사용*
val result = with(StringBuilder()) {
    append("Hello")
    append(" World")
    toString()  *// 이 값이 반환됨*
}  *// result는 String*

빌더 패턴의 장점

1. 타입 안전성

table {
    tr {
        td { +"내용" }     *// 올바른 중첩*
    }
    *// td { +"내용" }     // 컴파일 에러. td는 tr 안에만*
}

2. 코틀린 코드와 자유로운 조합

fun createDynamicTable() = createHTML().table {
    val items = listOf("사과", "바나나", "오렌지")
    
    *// 조건문 사용 가능*
    if (items.isNotEmpty()) {
        tr {
            th { +"과일 목록" }
        }
    }
    
    *// 반복문 사용 가능*
    items.forEachIndexed { index, item ->
        tr {
            td { +"${index + 1}" }
            td { +item }
        }
    }
}

3. 추상화와 재사용

*// HTML 생성 로직을 함수로 분리*
fun BODY.listWithToc(block: LISTWITHTOC.() -> Unit) {
    val listWithToc = LISTWITHTOC().apply(block)
    
    *// 목차 생성*
    ul {
        listWithToc.entries.forEachIndexed { index, (title, _) ->
            li { a("#$index") { +title } }
        }
    }
    
    *// 내용 생성*
    listWithToc.entries.forEachIndexed { index, (title, body) ->
        h2 { id = "$index"; +title }
        p { +body }
    }
}

@HtmlTagMarker
class LISTWITHTOC {
    val entries = mutableListOf<Pair<String, String>>()
    
    fun item(title: String, body: String) {
        entries += title to body
    }
}

*// 사용*
fun buildBookList() = createHTML().body {
    listWithToc {
        item("코틀린 기초", "변수와 함수에 대해 배웁니다")
        item("클래스와 객체", "객체지향 프로그래밍을 학습합니다")
        item("함수형 프로그래밍", "람다와 고차함수를 다룹니다")
    }
}

핵심 정리

수신 객체 지정 람다의 특징

*// 1. 문법*
val lambda: StringBuilder.() -> Unit = {
    append("Hello")  *// this.append("Hello")*
}

*// 2. 호출 방법*
val sb = StringBuilder()
sb.lambda()  *// 확장 함수처럼 호출// 3. 변수 저장 가능*
val appendExcl: StringBuilder.() -> Unit = { append("!") }

DSL 구조 생성 방법

  1. 람다 내포: dependencies { implementation(...) }
  2. 메서드 연쇄: query.select().where().orderBy()
  3. 수신 객체 지정: buildString { append(...) }

HTML 빌더의 핵심 아이디어

  • 구조: 태그의 중첩 관계를 코드 블록으로 표현
  • 타입 안전성: 잘못된 중첩은 컴파일 에러
  • 재사용성: 일반 코틀린 함수로 HTML 로직 분리 가능
  • 동적 생성: 반복문, 조건문으로 동적 HTML 생성

13.3 invoke 관례를 사용해 더 유연하게 블록 내포시키기


invoke 관례 - 객체를 함수처럼!

기본 개념

class Greeter(val greeting: String) {
    operator fun invoke(name: String) {
        println("$greeting, $name!")
    }
}

fun main() {
    val greeter = Greeter("안녕하세요")
    
    *// 일반 호출*
    greeter.invoke("철수")  *// 안녕하세요, 철수!*
    
    *// invoke 관례 사용*
    greeter("영희")        *// 안녕하세요, 영희!//  ↑ 객체를 함수처럼 호출!*
}

람다도 실제로는 invoke!

val lambda: (String) -> Unit = { println("Hello, $it") }

*// 이 두 호출은 같음*
lambda("World")        *// invoke 관례*
lambda.invoke("World") *// 명시적 호출
// 함수형 인터페이스에 invoke가 정의되어 있음*
interface Function1<in P1, out R> {
    operator fun invoke(p1: P1): R
}

실용적인 invoke 예시들

1. 계산기 객체

class Calculator {
    operator fun invoke(expression: String): Double {
        *// 간단한 계산 로직*
        return when {
            "+" in expression -> {
                val parts = expression.split("+")
                parts[0].trim().toDouble() + parts[1].trim().toDouble()
            }
            "-" in expression -> {
                val parts = expression.split("-")
                parts[0].trim().toDouble() - parts[1].trim().toDouble()
            }
            else -> 0.0
        }
    }
}

fun main() {
    val calc = Calculator()
    
    println(calc("10 + 5"))  *// 15.0*
    println(calc("20 - 3"))  *// 17.0*
}

2. 설정 빌더

class ConfigBuilder {
    private val properties = mutableMapOf<String, String>()
    
    operator fun invoke(key: String, value: String) {
        properties[key] = value
    }
    
    operator fun invoke(block: ConfigBuilder.() -> Unit) {
        this.block()
    }
    
    fun build() = properties.toMap()
}

fun main() {
    val config = ConfigBuilder()
    
    *// 방법 1: 직접 호출*
    config("host", "localhost")
    config("port", "8080")
    
    *// 방법 2: 블록 사용*
    config {
        this("database", "mydb")
        this("timeout", "30")
    }
    
    println(config.build())
    *// {host=localhost, port=8080, database=mydb, timeout=30}*
}

DSL에서 invoke 활용 - Gradle 스타일

문제: 두 가지 문법 모두 지원하고 싶음

*// 1. 단순한 함수 호출 스타일*
dependencies.implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")

*// 2. 블록 스타일*
dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
    implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0")
}

해결: invoke로 유연한 API 제공

class DependencyHandler {
    *// 일반 메서드 - 명령형 API*
    fun implementation(coordinate: String) {
        println("Added dependency on $coordinate")
    }
    
    fun testImplementation(coordinate: String) {
        println("Added test dependency on $coordinate")
    }
    
    *// invoke 관례 - DSL 스타일 API*
    operator fun invoke(body: DependencyHandler.() -> Unit) {
        body()  *// this.body()와 같음*
    }
}

fun main() {
    val dependencies = DependencyHandler()
    
    *// 방법 1: 직접 호출*
    dependencies.implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
    
    *// 방법 2: invoke 관례 사용*
    dependencies {
        implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0")
        testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.0")
    }
}

내부 동작 과정

*// dependencies { ... } 는 이렇게 변환됨:*
dependencies.invoke({
    this.implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0")
    this.testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.0")
})

*// invoke 메서드에서:
// - this = DependencyHandler 인스턴스
// - body = 수신 객체 지정 람다
// - body() 호출하면 람다 안의 코드 실행*

13.4 실전 코틀린 DSL

13.4.1 중위 호출 연쇄: 테스트 프레임워크의 should

코테스트 DSL - 영어처럼 읽히는 테스트

import io.kotest.matchers.should
import io.kotest.matchers.string.startWith
import org.junit.jupiter.api.Test

class PrefixTest {
    @Test
    fun testKPrefix() {
        val s = "kotlin".uppercase()
        s should startWith("K")  *// 영어처럼 읽힘!*
    }
}

should 함수 구현

*// should는 중위 함수*
infix fun <T> T.should(matcher: Matcher<T>) = matcher.test(this)

*// Matcher 인터페이스*
interface Matcher<T> {
    fun test(value: T)
}

*// startWith 매처 구현*
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")
            }
        }
    }
}

더 많은 매처들

*// 다양한 매처들*
fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)
    val text = "Hello World"
    
    *// 문자열 매처*
    text should startWith("Hello")
    text should endWith("World")
    text should contain("llo")
    
    *// 숫자 매처*  
    42 should beGreaterThan(30)
    42 should beLessThan(50)
    42 should beInRange(40..45)
    
    *// 컬렉션 매처*
    numbers should haveSize(5)
    numbers should contain(3)
    numbers should containAll(1, 2, 3)
}

*// 매처 구현 예시*
fun beGreaterThan(expected: Int): Matcher<Int> = object : Matcher<Int> {
    override fun test(value: Int) {
        if (value <= expected) {
            throw AssertionError("$value should be greater than $expected")
        }
    }
}

fun haveSize(expected: Int): Matcher<Collection<*>> = object : Matcher<Collection<*>> {
    override fun test(value: Collection<*>) {
        if (value.size != expected) {
            throw AssertionError("Collection size ${value.size} should be $expected")
        }
    }
}

13.4.2 원시 타입 확장: 날짜 처리 DSL

자연스러운 날짜 표현

import kotlin.time.Duration
import kotlin.time.DurationUnit

*// 현재 시간 기준 계산*
val now = Clock.System.now()
val yesterday = now - 1.days
val nextWeek = now + 7.days
val twoHoursLater = now + 2.hours
val thirtyMinutesAgo = now - 30.minutes

확장 프로퍼티로 구현

*// Int에 대한 확장 프로퍼티*
val Int.days: Duration
    get() = this.toDuration(DurationUnit.DAYS)

val Int.hours: Duration  
    get() = this.toDuration(DurationUnit.HOURS)

val Int.minutes: Duration
    get() = this.toDuration(DurationUnit.MINUTES)

val Int.seconds: Duration
    get() = this.toDuration(DurationUnit.SECONDS)

*// 커스텀 단위도 추가 가능*
val Int.weeks: Duration
    get() = (this * 7).toDuration(DurationUnit.DAYS)

val Int.fortnights: Duration  *// 2주*
    get() = (this * 14).toDuration(DurationUnit.DAYS)

실제 사용 예시

fun scheduleEvents() {
    val now = Clock.System.now()
    
    val events = listOf(
        "회의" to now + 1.hours,
        "점심" to now + 3.hours,
        "프레젠테이션" to now + 1.days,
        "휴가" to now + 2.weeks
    )
    
    events.forEach { (event, time) ->
        println("$event: $time")
    }
}

*// 날짜 계산 유틸리티*
fun calculateDeadlines() {
    val projectStart = Clock.System.now()
    
    val milestones = mapOf(
        "설계 완료" to projectStart + 1.weeks,
        "개발 완료" to projectStart + 6.weeks,  
        "테스트 완료" to projectStart + 8.weeks,
        "배포" to projectStart + 10.weeks
    )
    
    milestones.forEach { (milestone, deadline) ->
        println("$milestone 마감: $deadline")
    }
}

13.4.3 멤버 확장 함수: SQL DSL

Exposed 프레임워크 - 테이블 정의

*// 테이블 정의*
object Country : Table() {
    val id = integer("id").autoIncrement()
    val name = varchar("name", 50)
    override val primaryKey = PrimaryKey(id)
}

object Customer : Table() {
    val id = integer("id").autoIncrement()
    val name = varchar("name", 50)
    val countryId = reference("country_id", Country.id)
    override val primaryKey = PrimaryKey(id)
}

멤버 확장 함수의 힘

class Table {
    *// 일반 메서드*
    fun integer(name: String): Column<Int> = Column(name)
    fun varchar(name: String, length: Int): Column<String> = Column(name)
    
    *// 멤버 확장 함수 - 특정 맥락에서만 사용 가능*
    fun Column<Int>.autoIncrement(): Column<Int> {
        *// 정수 컬럼만 자동 증가 가능*
        return this.apply { */* 자동증가 설정 */* }
    }
    
    fun <T> Column<T>.primaryKey(): Column<T> {
        *// 모든 타입의 컬럼이 기본키 가능*
        return this.apply { */* 기본키 설정 */* }
    }
    
    *// 문자열 컬럼 전용 확장*
    fun Column<String>.uniqueIndex(): Column<String> {
        return this.apply { */* 유니크 인덱스 설정 */* }
    }
}

*// 사용*
object User : Table() {
    val id = integer("id").autoIncrement().primaryKey()  *// 가능*
    val email = varchar("email", 100).uniqueIndex()      *// 가능
    // val name = varchar("name", 50).autoIncrement()    // 컴파일 에러*
}

SQL 쿼리 DSL

*// 쿼리 작성*
fun findUsersInCountry() {
    val result = (Country innerJoin Customer)
        .select { Country.name eq "USA" }
        .orderBy(Customer.name)
        .limit(10)
    
    result.forEach { row ->
        println("고객: ${row[Customer.name]}, 국가: ${row[Country.name]}")
    }
}

*// SqlExpressionBuilder에서 제공하는 연산자들*
object SqlExpressionBuilder {
    *// 비교 연산자들*
    infix fun <T> Column<T>.eq(value: T): Op<Boolean> = EqOp(this, value)
    infix fun <T> Column<T>.neq(value: T): Op<Boolean> = NeqOp(this, value)
    infix fun <T : Comparable<T>> Column<T>.greater(value: T): Op<Boolean> = GreaterOp(this, value)
    infix fun <T : Comparable<T>> Column<T>.less(value: T): Op<Boolean> = LessOp(this, value)
    
    *// 문자열 연산자들*
    infix fun Column<String>.like(pattern: String): Op<Boolean> = LikeOp(this, pattern)
    
    *// NULL 체크*
    fun <T> Column<T>.isNull(): Op<Boolean> = IsNullOp(this)
    fun <T> Column<T>.isNotNull(): Op<Boolean> = IsNotNullOp(this)
    
    *// IN 연산자*
    infix fun <T> Column<T>.inList(values: List<T>): Op<Boolean> = InListOp(this, values)
}

*// 복잡한 쿼리 예시*
fun complexQuery() {
    val result = Customer
        .select { 
            (Customer.name like "%Kim%") and 
            (Customer.countryId inList listOf(1, 2, 3)) and
            Customer.name.isNotNull()
        }
        .orderBy(Customer.name)
    
    *// 생성되는 SQL:
    // SELECT * FROM Customer 
    // WHERE Customer.name LIKE '%Kim%' 
    //   AND Customer.country_id IN (1, 2, 3)
    //   AND Customer.name IS NOT NULL
    // ORDER BY Customer.name*
}

멤버 확장의 장점과 단점

*// 장점: 맥락 제한*
class DatabaseContext {
    fun Column<String>.encrypt(): Column<String> {
        *// 데이터베이스 맥락에서만 사용 가능*
        return this
    }
}

class ApiContext {
    fun Column<String>.validate(): Column<String> {
        *// API 맥락에서만 사용 가능*
        return this
    }
}

*// 단점: 확장성 제한
// - 기존 클래스 수정 없이는 새 멤버 확장 추가 불가
// - 서드파티 라이브러리 확장 어려움*
context(Table)
fun Column<Int>.autoIncrement(): Column<Int> {
    *// Table과 Column<Int> 양쪽 모두에 접근 가능*
    return this
}

DSL 설계 패턴 정리

1. 구조 생성 방법

*// 람다 내포*
dependencies {
    implementation("...")
}

*// 메서드 연쇄*  
query.select().where().orderBy()

*// 중위 호출*
value should be greater than 10

2. 유연성 제공

*// invoke로 두 가지 스타일 지원*
api.method("param")  *// 직접 호출*
api {                *// 블록 스타일*
    method("param")
}

3. 타입 안전성

*// 멤버 확장으로 맥락 제한*
class TableContext {
    fun Column<Int>.autoIncrement()  *// 정수 컬럼만*
    fun Column<String>.index()       *// 문자열 컬럼만*
}

4. 자연어 스타일

*// 확장 프로퍼티로 자연스러운 표현*
1.days
2.weeks  
30.minutes

*// 중위 호출로 영어다운 표현*
"text" should startWith "t"
number should be greaterThan 5

'language > Kotilin In Action' 카테고리의 다른 글

8주차 (16~18장)  (3) 2025.09.01
7주차(14,15)  (1) 2025.09.01
5주차(11~12장)  (1) 2025.09.01
4주차(9~10장)  (1) 2025.09.01
3주차 (6~8장)  (1) 2025.09.01