Kotlin-In-Action | #11. DSL 만들기

보람·2022년 6월 7일
0

Kotlin-In-Action

목록 보기
12/12

DSL, Domain Specific Language

  • 영역 특화 언어란 특정 도메인(분야, 해결하려는 문제)을 적용하는데 특화된 언어를 의미한다.
  • 코틀린에서 DSL 형식의 API를 DB 접근, HTML 생성, 테스트 등에 사용할 수 있다.
  • 명확하고 간결한 코드의 API를 작성할 수 있다.
    • set.add(2) 보다는 set +=2 처럼 코틀린은 간결한 구문을 지원한다.

내부 DSL

  • 내부 DSL은 여러 메서드 호출로 구성된 구조를 더 쉽게 표현할 수 있게 해주는 API를 설계할 때 사용할 수 있는 패턴
  • 외부 템플릿이나 마크업 언어 대신 코틀린 내부 DSL을 사용하면 코드를 추상화하고 재활용할 수 있다.
    • 예제를 봤을 때 느낀 것은 중복되는 태그를 함수화 하면 재활용이 가능하고 작은 함수가 모여서 큰 개념의 함수가 되는 느낌. (더 깊이 생각하지 말자. 머리 아프다)
  • HTML 페이지 생성을 위한 내부 DSL을 확장하면 여러 프론트엔드 개발 프레임워크 지원 가능
    • 타입 안전성 보장: td를 tr안에서만 사용
    • 동적으로 사용: 표의 칸을 원하는 만큼 추가 가능
fun createSimpleTable() = createHTML().
	table {
    	tr {
        	td { +"cell" } 
        }
    }
        
  • 위 예제에서
    • 각 함수는 고차 함수로 수신 객체 지정 람다를 인자로 받는다.
    • table 함수에 넘겨진 람다에서는 tr 함수를 사용해 <tr> 태그를 만들 수 있고 tr 함수에 넘겨진 람다에서는 td 함수를 사용해 <td> 태그를 만들 수 있다. (타입 안전성)
  • 내부 DSL 지원 라이브러리
    • 코틀린 테스트 라이브러리 : 단위테스트에서 읽기 쉬운 단언문 지원하는 내부 DSL 지원
    • 익스포즈드 라이브러리 : DB를 다루기 위한 내부 DSL 제공
    • 안코 라이브러리 : 안드로이드 개발에 필요한 여러 도구를 제공

수신 객체 지정 람다

      String.(Int, Int) -> Unit
   <수신객체타입><파라미터타입><반환타입>
fun buildString(
        builderAction: StringBuilder.() -> Unit
) : String {
    val sb = StringBuilder()
    sb.builderAction()
    return sb.toString()
}

//main 함수
val s = buildString {
        this.append("Hello, ") //this 키워드는 StringBuilder 객체를 가리킴
        append("World!") //this 를 생략해도 묵시적으로 StringBuilder 객체가 수신 객체로 취급
    }
println(s) //Hello, World!
  • 위 예제에서
    • StringBuilder : 수신객체타입
    • () : 파라미터는 없음
    • Unit : 반환 타입
  • 수신 객체 지정 람다는 람다 본문 안에서 메서드를 결정하는 방식을 재정의함으로써 여러 요소를 중첩시킬 수 있는 구조를 만들 수 있다.
  • 수신 객체 지정 람다를 파라미터로 받은 경우 그 람다의 타입은 확장 함수 타입이다.
  • 람다를 파라미터로 받아서 사용하는 함수는 람다를 호출하면서 람다에 수신 객체를 제공한다.

중위 호출 인자

  • 중위 호출 인자로 특별히 이름을 붙인 객체를 사용하면 특수 기호를 사용하지 않는 실제 영어처럼 보이는 DSL을 만들 수 있다.

확장

val Int.days: Period //반환 타입:Period
    get() = Period.ofDays(this)

val Period.ago: LocalDate //위 days 의 반환타입인 Period.ago 확장함수 
    get() = LocalDate.now() - this //어제 

val Period.fromNow: LocalDate //위 days 의 반환타입인 Period.ago 확장함수 
    get() = LocalDate.now() + this //내일
    
println(1.days.ago) //2022-06-06
println(1.days.fromNow) //2022-06-08
  • 원시 타입에 대한 확장을 정의하면 날짜 등의 여러 종류의 상수를 더 읽기 좋게 만들 수 있다.

invoke 관례: 더 유연한 블록 중첩

  • invoke 관례를 사용하면 객체를 함수처럼 다룰 수 있다.
class Greeter(val greeting: String) {
    operator fun invoke(name: String) {
        println("$greeting, $name!") //Servus, Dmitry!
    }
}

//main
val bavarianGreeter = Greeter("Servus")
bavarianGreeter("Dmitry")
  • bavarianGreeter.invoke("Dmitry") 로 컴파일 됨
  • invoke는 오버라이딩 가능

느낀 점

음..어렵다🥸 깊게 이해하기 보다는 나중에 HTML DSL, DB 쿼리 빌더 등을 사용할 때 이것이 DSL이구나! 라는 거라도 생각하면서 사용하는 개발자가 되자. 일단은 그래보자.

profile
백엔드 개발자

0개의 댓글