본문 바로가기
프로그래밍/코틀린

11장. DSL 만들기

by 직장인 투자자 2022. 1. 7.

11장. DSL 만들기

개요

  • 영역 특화 언어 (DSL) 만들기
  • 수신 객체 지정 람다 사용
  • invoke 관례 사용
  • 기존 코틀린 DSL 예제

 

 

11.1 API에서 DSL로

라이브러리 개발자 뿐만 아니라 모든 개발자는 깔끔한 API 작성해야 할 책임이 있다.

 

깔끔한 API

  • 코드를 읽는 사람들이 어떤 일이 벌어질지 명확하게 이해할 수 있어야 한다.
  • 불필요한 구문이나 번잡한 준비 코드가 적고 코드가 간결해야 한다.

코틀린이 간결한 구문을 어떻게 지원하는가?

일반 구문 간결한 구문 사용한 언어 특성
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”)} 수신 객체 지정 람다

코틀린 DSL도 컴파일 시점에 타입이 정해져서 컴파일 시점 오류감지, IDE 지원 등 모든 정적 타입 지정 언어의 장점을 누를 수 있다.

11.1.1 영역 특화 언어라는 개념

프로그래밍 언어는 컴퓨터로 풀 수 있는 모든 문제를 충분히 풀 수 있는 기능을 제공하는 범용 프로그래밍 언어 와 특정 과업 또는 영역에 초점을 맞추고 그 영역에 필요하지 않은 기능을 없앤 영역 특화 언어 로 구분할 수 있다

 

범용 프로그래밍 언어

  • 명령적인 특징을 가지고 있다.
    • 연산을 완수하기 위해 필요한 각 단계를 순서대로 기술

영역 특화 언어

  • 선언적인 특징을 가지고 있다.
    • 원하는 결과를 기술하기만 하고 그 결과를 위한 세부 실행은 언어를 해석하는 엔진에 맡김
    • 특정 영역에 특화되어 자체 문법이 있기 때문에 범용 언어로 만든 애플리케이션과 조합하기가 어렵다.

ex) SQL, 정규식

이런 단점을 해결하면서 다른 이점을 살리는 방법으로 내부 DSL 이라는 개념이 유명해지고 있다.

11.1.2 내부 DSL

내부 DSL은 범용 언어로 작성된 프로그램의 일부며, 범용 언어와 동일한 문법을 사용한다. 그렇기 때문에 내부 DSL은 다른 언어가 아니라 DSL의 핵심 장점을 유지하면서 주 언어를 특별한 방법으로 사용하는 것이다

외부 DSL 은 주 언어와는 독립적인 문법 구조를 가진다

// SQL을 이용한 쿼리문 작성 (외부 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
// 코틀린 익스포즈드를 사용해 쿼리문 작성 (내부 DSL로 결과를 코틀린 객체로 넘긴다)
(Country join Customer)
    .slice(Coutry.name, Count(Customer.id))
    .selectAll()
    .groupBy(Country.name)
    .orderBy(Count(Customer.id), isAsc = false)
    .limit(1)

11.1.3 DSL의 구조

DSL과 일반 API를 구분하는 방법

  • DSL은 구조 또는 문법을 독립적으로 가진다.
  • 위의 예제에서 여러 함수 호출을 조합해서 연산을 만드는 것 또한 내부 DSL 적인 특징이다.
  • DSL 은 같은 문맥을 함수 호출 시마다 반복하지 않고도 재사용할 수 있다.
// gradle에서 디펜던시 설정 <-- 람다 중첩을 이용해 구조를 만듦
dependencies {
    compile("junit:junit:4.11")
    compile("com.google.inject:guice:4.1.0")
}

// 일반 명령-질의 API를 통해 디펜더시 설정 (코드에 중복이 많다)
project.dependencies.add("compile", "junit:junit:4.11")
project.dependencies.add("compile", "com.google.inject:guice:4.1.0")

11.1.4 내부 DSL로 HTML 만들기

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

코틀린 코드로 HTML을 만들려는 이유

  • 타입 안정성을 보장한다. (위 코드에서 td는 tr 내에서만 사용할 수 있다. 그렇지 않은 경우 컴파일이 되지 않는다.)
  • 코틀린 코드를 원하는대로 사용할 수 있다.
    • 아래 예시처럼 맵에 들어있는 원소에 따라 동적으로 표의 칸을 생성할 수 있다.
      fun createAnotherTable() = createHTML().table {
      val numbers = mapOf(1 to "one", 2 to two)
      for ((num, string) in numbers) {
      tr {
        td { +"$num" }
        td { +string }
      }
      }
      }

이제 DSL 이 왜 필요한지 알았으니 DSL 작성할 때 코틀린이 어떻게 도움이 되는지 보자..

11.2.1 수신 객체 지정 람다와 확장 함수 타입

  • 일반 람다를 받는 buildString 함수
  • 매번 it 을 사용해 StringBuilder 인스턴스를 참조해야함.
    fun buildString(buiderAction: (StringBuilder) -> Unit): String { // 함수 타입인 파라미터 정의
    val sb = StringBuilder()
    builderAction(sb) // 람다 인자로 StringBuilder 인스턴스를 넘긴다.
    return sb.toString()
    }
    

val s = buildString {
it.append("Hello, ") // it 은 StringBuilder 인스턴스를 가리킨다.
it.append("World!")
}
println(s)
// 결과: Hello, World!

 

- 수신객체지정 람다 (lambda with a receiver) 함수
- 수신객체는 이름과 마침표를 명시하지않아도 인자의 멤버를 바로 사용할 수 있다.
   ( 확장함수처럼 this 가 넘어가기 때문에 it 를 붙이지 않아도 된다)

// 수신 객체 지정 람다를 사용해 buildString() 정의
fun buildString(builderAction: StringBuilder.() -> Unit): String { // 수신 객체가 있는 함수 타입(확장 함수 타입)의 파라미터 선언
  val sb = StringBuilder()
  sb.builderAction() // StringBuilder 인스턴스를 수신 객체로 넘김.
  return sb.toString()
}

val s = buildString {
  this.append("Hello, ")
  append("World!")  // this 를 생략해도 묵시적으로 StringBuilder 인스턴스가 수신 객체로 취급
}
println(s)
// 결과: Hello, World
  • buildString 함수(수신 객체 지정 람다)의 인자는 확장 함수타입(builderAction) 의 파라미터와 대응
  • 호출된 람다 본문 안에서는 수신 객체(sb)가 묵시적 수신 객체(this)가 된다.
  • 수신 객체 지정 람다를 인자로 넘기므로 람다 내에서 it를 사용하지 않아도 된다
  • 확장 함수 타입 선언은 <수신 객체 타입>.(파라미터타입, 파라미터타입) -> <반환타입> 형태로 명시한다.

apply 함수는 인자로 받은 람다나 함수를 호출하면서 자신의 수신 객체를 람다나 함수의 묵시적 수신 객체로 사용한다

inline fun <T> T.apply(block: T.() -> Unit): T {
    block() // apply 안에 들어간 T람다가 T의 확장함수가 된다.
  return this
}

inline fun <T, R> with(receiver: T, block: T.() -> R): R = recevier.block() // 람다를 호출해 얻은 결과를 반환한다.

apply 함수

  • apply를 호출한 인스턴스 타입이 수신 객체인 확장 함수 람다를 인자로 받는다.
  • 람다의 반환은 Unit이다.
  • 실제 코드 내부에서는 해당 람다를 수행하고 수신 객체 자기자신(this)를 리턴하고 있다
fun alphabetUsingApply() = StringBuilder().apply {
  for (letter in 'A'..'Z') { 
    append(letter)
   } 
  append("\nNow I Know the alphabet!") }.toString()

with 함수

  • 첫번째 인자 : 수신객체
  • 두번째 인자: 첫번째에 받은 인자가 수신 객체인 람다
  • 람다를 호출해 얻은 결과를 반환한다
fun alphabet() = with(StringBuilder()) { 
  for (letter in 'A'..'Z') { 
    append(letter) 
  }
  append("\nNow I Know the alphabet!") toString()
 }

11.2.2 수신 객체 지정 람다를 HTML 빌더 안에서 사용

HTML 빌더란?

  • HTML을 만들기 위한 코틀린 DSL
  • 타입 안전한 빌더(type-safe builder)의 대표적인 예
  • 객체 계층 구조를 선언적으로 정의할 수 있다.
  • 코틀린 빌더는 사용하기 편하면서도 안전하다고 한다.
fun createSimpleTable() = createHTML().
  table { 
    tr { 
      td { +"cell" }
    }
  }
  • table, tr, td 는 고차 함수로 수신 객체 지정 람다를 인자로 받는 형태이다.
  • 각 수신 객체 지정 람다가 이름 결정 규칙을 바꾸는 방식으로 되어있다.
  • table 함수에 넘겨진 람다에서는 tr 함수를 사용해HTML 태그를 먼들 수 있지만 그 람다 밖에서는 tr 함수를 찾을 수 없다.
  • table에 전달된 수신 객체는 TABLE 이라는 특별한 타입이며, 그 안에 tr 메소드 정의가 있다.
  • this 참조를 쓰지 않아도돼 빌더 문법이 간단해지고 전체적인 구문이 원래의 HTML 구문과 비슷해짐

간단한 HTML 빌더 구현

  • table 함수는 TABLE 태그의 새 인스턴스를 만들고 그 인스턴스를 초기화하고 반환
    • 주어진 태그를 초기화하고 바깥쪽 태그의 자식으로 추가하는 로직을 거의 모든 태그가 공유
    • doInit 은 자식 태그에 대한 참조를 저장하는 일과 인자로 받은 람다를 호출하는 일을 한다.
    • 여러 태그는 그냥 doInit을 호출하기만 하면 된다.
    • 각 태그는 자기 이름을 태그 안에 넣고 자식 태그를 재귀적으로 문자열로 바꿔서 합친 다음 닫는 태그를 추가하는 방식으로 자기 자신을 문자열로 표현
    • 전체구현은 kotlinx.html 라이브러리 을 참고
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태그 인스턴스를 새로 만들고 초기환 후 TABLE 태그의 자식으로 등록
}
class TR : Tag("tr") {
    fun td(init: TD.() -> Unit) = doInit(TD(), init) // // TD 태그의 새 인스턴스를 만들어서 TR 태그의 자식으로 등록
}
class TD : Tag("td")

fun createTable() =
    table {
        tr {
            td {
            }
        }
    }

>>> println(createTable())
<table><tr><td></td></tr></table>

11.2.3 코틀린빌더: 추상화와 재사용을 가능하게 하는 도구

  • 내부 DSL을 사용하면 일반 코드와 마찬가지로 반복되는 내부 DSL 코드 조각을 새 함수로 묶어서 재사용할 수 있음부트스트랩 라이브러리 에서 제공하는 드롭다운 메뉴가 있는 HTML 페이지를 코틀린 빌더를 통해 생성
<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>

kotlin html 을 사용하면 위의 html 코드를 만들기 위해 아래와 같이 작성할 수 있다.

fun buildDropdown() = createHTML().div(classes = "dropdown") {
    button(classes = "btn dropdown-toggle") {
        +"Dropdown"
        span(classes = "caret")
    }
    ul(classes = "dropdown-menu") {
        li { a("#") { +"Action" } }
        li { a("#") { +"Another action" } }
        li { role = "separator"; classes = setOf("divider") }
        li { classes = setOf("dropdown-header"); +"Header" }
        li { a("#") { +"Separated link" } }
    }
}

div, button은 모두 일반 함수이기 때문에 반복되는 로직을 별도의 함수로 분리하면 코드를 더 읽기 쉽게 만들 수 있음
그렇게 개선한 결과는 다음과 같다.

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

// 모든 UL 태그 안에서 item 호출 가능하도록 확장함수 추가.  그리고 항상 li 태그를 추가해준다.
fun UL.item(href: String, name: String) = li { a(href) { +name } }

fun UL.divider() = li { role = "separator"; classes = setOf("divider") }

fun UL.dropdownHeader(text: String) = li { classes = setOf("dropdown-header"); +text }

fun DIV.dropdownMenu(block: UL.() -> Unit) = ul("dropdown-menu", block)

 

 

 

11.3 invoke 관례를 사용한 더 유연한 블록 중첩

invoke 관례를 사용하면 객체를 함수처럼 호출할 수 있음

11.3.1 invoke 관례: 함수처럼 호출할 수 있는 객체

관례 : 특별한 이름이 붙은 함수를 일반 메소드 호출 구문으로 호출하지 않고 더 간단한 다른 구문으로 호출할 수 있게 지원하는 기능

  • operator 변경자가 붙은 invoke 메소드를 정의하면, 클래스의 객체를 함수처럼 호출할 수 있음
  • 원하는대로 파라미터 개수나 타입을 지정할 수 있다.
  • 여러 파라미터 타입을 지원하기 위해 오버로딩도 가능하다
class Greeter(val greeting: String) {
    operator fun invoke(name: String) {  
        println("$greeting, $name!")
    }
}

val bavarianGreeter = Greeter("Servus")
bavarianGreeter("Dmitry")                           // 내부적으로 bavarianGreeter.invoke("Dmitry") 으로 컴파일 된다.
Servus, Dmitry!

11.3.2 invoke 관례와 함수형 타입

  • 인라인하는 람다를 제외한 모든 람다는 함수형 인터페이스(Function1 등)를 구현하는 클래스로 컴파일된다
  • 람다를 함수처럼 호출하면 이 관례에 따라 invoke 메소드 호출로 변환된다.
interface Function1<in P1> {
  operator fun invoke(p1: P1): R
}

함수타입을 확장하면서 invoke() 를 오버라이딩하기 예제

data class Issue(
  val id: String, 
  val project: String, 
  val type: String, 
  val priority: String,
  val description: String
)

class ImportantIssuesPredicate(val project: String): (Issue) -> Boolean {  //함수타입을 부모 클래스로 사용
  override fun invoke(issue: Issue): Boolean { // invoke 구현
    return issue.project == project && issue.isImportant()
  }

  private fun Issue.isImportant(): Boolean {
    return type == "Bug" && (priority == "Major" || priority == "Critical")
  }
}

val i1 = Issue("IDEA-154446", "IDEA", "Bug", "Major", "Save settings failed")
val i2 = Issue("KT-12183", "Kotlin", "Feature", "Normal", "Intention: convert serveral ~~")

val predicate = ImportantIssuesPredicate("IDEA")
for (issue in listOf(i1, i2).filter(predicate)) { // 술어를 filter에 넘김
  println(issue.id)
}

// filter 는 T타입 element를 입력으로 받아 Boolean을 반환하는 함수형 변수'를 입력으로 받고, Boolean 이 ture 인 경우 반환할 새로운 list 에 해당 값을 포함시킴.
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T>

// 결과: IDEA-154446
  • 람다를 함수 타입 인터페이스를 구현하는 클래스로 변환하고 그 클래스의 invoke 메소드를 오버라이드하면 복잡한 람다가 필요한 구문을 리팩토링할 수 있다
  • 람다 본문에서 따로 분리해낸 메소드가 영향을 끼치는 영역을 최소화할 수 있다는 장점이 있다.

11.3.3 DSL의 invoke 관례: 그레이들에서 의존관계 정의

class DependencyHandler {
  // 일반적인 명령형 API 정의
  fun compile(coordinate: String) { // 일반적인 명령형 API를 정의
    println("Added dependency on $coordinate")
  }
  // invoke를 정의해 DSL 스타일 API를 제공
  operator fun invoke(body: DependencyHandler.() -> Unit) { // invoke 를 정의해 DSL 스타일 API 를 제공
    body() // == this.body()  
  }
}

val dependencies = DependencyHandler()
dependencies.compile("org.jetbrains.kotlin:kotlin-stdlib:1.0.0")
// 결과: Added dependecy on org.jetbrains.kotlin:kotlin-stdlib:1.0.0

dependencies {
  compile("org.jetbrains.kotlin:kotlin-stdlib:1.0.0")
}
// 결과: Added dependecy on org.jetbrains.kotlin:kotlin-stdlib:1.0.0
  • 두번째 호출은 결과적으로 아래와 같이 변환되게 된다.
dependencies.invoke({
  this.compile("org.jetbrains.kotlin:kotlin-stdlib:1.0.0")
})
  • dependencies를 함수처럼 호출하면 람다를 인자로 넘기게 된다.
  • 람다의 타입은 확장 함수 타입(수신 객체를 지정한 함수 타입) 이다.
  • 지정한 수신 객체 타입은 DpendencyHandler이다.
  • invoke 메소드는 이 수신 객체 지정 람다를 호출한다.
  • invoke 메소드가 DependencyHandler의 메소드이므로 이 메소드 내부에서 this는 DependencyHandler 객체이다.
  • 따라서 invoke 안에서 DependencyHandler 타입의 객체를 따로 명시하지 않고 compile() 을 호출할 수 있다.

11.4 실전 코틀린 DSL

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

  • 깔끔한 구문은 내부 DSL 의 핵심 특징 중 하나다.
  • 깔끔하게 만드려면 코드에 쓰이는 기호의 수를 줄여야한다.

코틀린 테스트 DSL 에서 중위 호출을 어떻게 활요하는지 살펴보자.

// should 함수 구현
infix fun <T> T.should(matcher: Matcher<T>) = matcher.test(this) // infix : 두개의 변수 가운데 오는 함수, this == T

// Matcher 선언
interface Matcher<T> {
  fun test(value: T)
}

class startWith(val prefix: String): Matcher<String> { // 클래스의 첫 글자가 대문자가 아닌 케이스 (dsl 에서는 종종 명명규칙을 벗어나야 할 때가 있다)
  override fun test(value: String) {
    if (!value.startsWith(prefix)) {
      throw AssertionError("String $value does not start with $prefix")
    }
  }
}

>>> "kotlin" should startWith("kot")


// 중위호출과 object로 정의한 싱글턴 객체 인스턴스를 조합하면 dsl 에 복잡한 문법을 도입할 수 있다.
object start // 함수에 데이터를 넘기기 위해서가 아니라 dsl 문법을 정의하기 위해 사용

// start를 인자로 넘겨서 결과로 startWrapper 인스턴스를 받을 수 있다.
infix fun String.should(x: start): StartWrapper = StartWrapper(this)

// startWrapper 클래스에는 검사를 실행하기 위해 인자로 받는 with 라는 멤버를 가진다.
class StartWrapper(val value: String) {
  infix fun with(prefix: String) = if (!value.startsWith(prefix)) {
    throw AssertionError("String $value does not start with $prefix")
  } else {
    Unit
  }
}

>>> "kotlin" should start with "kot"

11.4.2 원시 타입에 대한 확장 함수 정의: 날짜 처리

자바 8의 java.time API와 코틀린을 사용해 1.days.ago , 1.days.fromNow 와 같은 API를 구현해보자

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

// days는 Int 타입의 확장 프로퍼티
val Int.days: Period 
    get() = Period.ofDays(this) // this는 상수의 값을 가리킴

// LocalDate 클래스에는 코틀린의 - 연산자 관례와 일치하는 인자가 하나뿐인 minus 메소드가 들어가 있음 (public LocalDate minus(TemporalAmount var1))
// 따라서 -, + 는 코틀린에서 제공하는 확장함수가 아닌 LocalData라는 JDK 클래스에 있는 관례에 의해 minus, plus 메소드가 호출되는 것
val Period.ago: LocalDate
    get() = LocalDate.now() - this

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

println(1.days.ago)
// 결과: 2020-04-18

println(1.days.fromNow)
// 결과: 2020-04-20

11.4.3 멤버 확장 함수: SQL을 위한 내부 DSL

  • 클래스안에서 확장함수와 확장프로퍼티를 선언한다.
  • 정의한 확장 함수나 확장프로퍼티는 그들이 선언된 클래스의 멤버인 동시에 그들이 확장하는 다른 타입의 멤버이기도 하다.
  • 이러한 함수나 프로퍼티를 멤버 확장(Member Extensions) 이라고 부른다
  • 멤버확장으로 정의해야하는 이유는 메소드가 적용되는 범위를 제한하기 위함
  • 대신 제한된 범위로 인해 멤버확장은 확장성이 떨어진다는 단점도 있다.
// 코틀린 익스포즈드에서 테이블 선언
object Country: Table() {
  val id = integer("id").autoIncrement().primaryKey()
  val name = varchar("name", 50)
}

// Table 밖에서는 이 함수들을 호출할 수 없다.
class Table {
  fun integer(name: String) : Column<Int>
  fun varchar(name: String, length: Int) : Column<String>
  fun <T> Column<T>.primaryKey() : Column<T>
  fun Column<Int>.autoIncrement() : Column<Int> // 수신객체타입을 Int 로 제한
 //...
}

11.4.4 안코: 안드로이드 UI를 동적으로 생성하기

  • 반복되는 로직을 추출해서 재사용
  • 자세한 설명은 예제 부족으로 스킵.
// alert API의 선언
fun Context.alert(message: String, title: String, init: AlertDialogBuilder.() -> Unit)

class AlertDialogBuilder {
  fun positiveButton(text: String, callback: DialogInterface.() -> Unit)
  fun negativeButton(text: String, callback: DialogInterface.() -> Unit)
}

// Anko를 사용해 안드로이드 경고창 표시하기
fun Activity.showAreYouSureAlert(process: () -> Unit) {
  alert(title = "Are you sure?", message = "Are you really sure?") {
    positiveButton("Yes") { process() } // this 생략 (AlertDialogBuilder)
    negativeButton("No") { cancel() }
  }
}

11.5 요약

  • 내부 DSL은 여러 메소드 호출로 구성된 구조를 더 쉽게 표현할 수 있게 해주는 API를 설계할 때 사용할 수 있는 패턴이다.
  • 수신 객체 지정 람다는 람다 본문 안에서 메소드를 결정하는 방식을 재정의함으로써 여러 요소를 중첩시킬 수 있는 구조를 만들 수 있다.
  • 수신 객체 지정 람다를 파라미터로 받은 경우 그 람다의 타입은 확장 함수 타입이다. 람다를 파라미터로 받아서 사용하는 함수는 람다를 호출하면서 람다에 수신 객체를 제공한다.
  • 외부 템플릿이나 마크업 언어 대신 코틀린 내부 DSL을 사용하면 코드를 추상화하고 재활용할 수 있다.
  • 중위 호출 인자로 특별히 이름을 붙인 객체를 사용하면 특수 기호를 사용하지않는 실제 영어처럼 보이는 DSL을 만들 수 있다.
  • 원시 타입에 대한 확장을 정의하면 날짜 등의 여러 종류의 상수를 더 읽기 좋게 만들 수 있다.
  • invoke 관례를 사용하면 객체를 함수처럼 다룰 수 있다.
  • kotlinx.html 라이브러리는 HTML 페이지를 생성하기 위한 내부 DSL을 제공한다. 그 내부 DSL을 확장하면 여러 프론트엔드 개발 프레임워크를 지원하게 만들 수 있다.
  • 코틀린테스트 라이브러리는 단위 테스트에서 읽기 쉬운 단언문을 지원하는 내부 DSL을 제공한다.
  • 익스포즈드 라이브러리는 데이터베이스를 다루기 위한 내부 DSL을 제공한다.
  • 안코 라이브러리는 안드로이드 개발에 필요한 여러 도구를 제공한다. 그런 도구 중에는 UI 레이아웃을 정의하기 위한 내부 DSL도 있다
반응형

댓글