본문 바로가기
공부/Kotlin

11장 DSL 만들기

by JERO__ 2022. 7. 18.

1. API에서 DSL로

깔끔한 API란 어떤 의미일까?

  • 코드 읽는 독자가 명확하게 이해할 수 있어야 한다(이름, 개념)
  • 코드가 간결해야 한다.

코틀린의 깔끔한 API를 다시 복습해보자

자바 코틀린 사용한 언어 특성

1-1. DSL : 영역 특화(declarative) 언어

영역 특화 언어 : 범용 프로그래밍 언어를 기반으로 하여, 필요하지 않은 기능을 없앰

→ 범용(imperative) 프로그래밍 언어와 달리 declarative 이다. 하지만, 범용 언어와 조합하기 어렵다. 이를 극복하기 위해 internal DSL 개념이 유명해졌다!

  • 예) SQL, 정규식

1-2. 내부 DSL (internal DSL)

  • external DSL : 독립적인 문법구조
  • internal DSL : 범용 언어로 작성된 프로그램
    • 예시(기존SQL)
    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
    
    • 예시(코틀린 internal DSL)
    (Country join Customer)
        .slice(Country.name, Count(Customer.id))
        .selectAll()
        .groupBy(Country.name)
        .orderBy(Count(Customer.id),isAsc = false)
        .limit(1)
    

1-3 DSL 구조

  • 보통 람다를 중첩시키거나
  • dependencies { complie("junit:junit:4.11") complie("com.google.inject:guice:4.1.0") }
  • 메소드 호출을 연쇄시키는 방식으로 구조를 만든다.
  • str should startWith("kotlin")

2. 구조화된 API 구축 : DSL에서 수신 객체 지정 DSL 사용

수신 객체 지정 람다는 구조화된 API를 만들 때 도움이 되는 강력한 코틀린 기능

→ 구조가 있다는 것은 일반적인 API와 DSL을 구분하는 아주 중요한 특성

예시

  • 람다를 인자로 받는 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)
  • 수신 객체 지정 람다를 사용 - it 사용X
  • fun buildString( builderAction: StringBuilder.() -> Unit ) : String { val sb = StringBuilder() sb.builderAction() return sb.toString() } val s = buildString { this.append("Hello, ") append("World!") } println(s)

확장 함수 타입을 함수의 매개변수 타입으로 지정함으로써 수신 객체 지정 람다를 인자로 받을 수 있게 되었다. 왜 확장함수나 수신객체지정람다 타입일까?

  • 정의된 메소드를 마치 그 클래스 내부에서 호출하듯이 사용할 수 있다.
  • invoke convention을 사용하면 객체를 함수처럼 호출할 수 있다.
  • class Greeter(val greeting: String) { operator fun invoke(name: String) { println("$greeting, $name!") } } val bavarianGreeter = Greeter("Servus") bavarianGreeter("Dmitry")

실전 코틀린 DSL

1. kotest 맛보기

s should startWith("kot")
  • should
    • Matcher 인스턴스를 요구한다.
  • infix fun <T> T.should(matcher: Matcher<T>) = matcher.test(this)
  • startWith : Matcher를 구현
  • interface Matcher<T> { fun test(value: T) } class startWith(val prefix: String) : Matcher<String> { override fun test(value: String) { if(!value.startsWith(prefix)) throw AssertionError("String $value does not start with $prefix") } }

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

  • 클래스 안 확장 함수와 확장 프로퍼티 선언

댓글