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의 특징
- 특화됨: 특정 도메인만 처리
- 선언적: "무엇을" 원하는지 기술 (어떻게는 엔진이 처리)
- 간결함: 해당 영역에서는 매우 간결
외부 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 구조 생성 방법
- 람다 내포: dependencies { implementation(...) }
- 메서드 연쇄: query.select().where().orderBy()
- 수신 객체 지정: 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 |