반응형
Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

안드로이드 개발자 노트

[이펙티브 코틀린] Item35. 복잡한 객체를 생성하기 위한 DSL을 정의하라 본문

Kotlin/이펙티브 코틀린

[이펙티브 코틀린] Item35. 복잡한 객체를 생성하기 위한 DSL을 정의하라

어리둥절범고래 2023. 12. 25. 18:57
반응형

코틀린은 DSL(Domain Specific Language)을 직접 만들 수 있습니다.

DSL은 복잡한 객체, 계층 구조를 갖고 있는 객체들을 정의할 때 유용합니다.

DSL을 만드는 것은 힘든 일이지만, 한 번 만들고 나면 보일러플레이트(boilerplate)와 복잡성을 숨기면서 개발자의 의도를 명확하게 표현할 수 있습니다.

// HTML DSL
body {
    div {
        a("https://kotlinlang.org") {
            target = ATarget.blank + "Main site"
        }
    }
    +"some content"
}

// Andriod View DSL (Anko 라이브러리)
verticalLayout {
    val name = editText()
    button("Say Hello") {
        onClick { toast("Hello, ${name.text}!")}
    }
}

// Test DSL
class MyTests: StringSpec({
    "반환되는 길이는 String의 크기이어야 합니다." {
        "test string".length shouldBe 5
    }
    "startsWith 함수는 prefix를 반환해야 합니다." {
        "world" should startWith("wor")
    }
})

// Gradle DSL
plugins {
    'java-library'
}

dependencies {
    api("junit:junit:4.12")
    implementation("junit:junit:4.12")
    testImplementation("junit:junit:4.12")
}

configurations {
    implementation {
        resolutionStrategy.failOnVersionConflict()
    }
}

 

 

 

사용자 정의 DSL 만들기

 

사용자 정의 DSL을 만들려면, 리시버를 사용하는 함수 타입에 대한 개념을 이해해야 합니다.

함수 타입은 함수로 사용할 수 있는 객체를 나타내는 타입입니다.

예를 들어 filter함수를 살펴보면, predicate에 함수 타입이 활용되고 있습니다.

inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    val list = arrayListOf<T>()
    
    for (elem in this) {
        if (predicate(elem)) { list.add(elem) }
    }
    return list
}

 

함수 타입의 몇 가지 예를 살펴보면 다음과 같습니다.

 

  • ()->Unit : 인자가 없고, Unit을 리턴합니다. (반환값 없음)
  • (Int)->Unit : Int 인자를 받고, Unit을 리턴합니다. (반환값 없음)
  • (Int)->Int : Int 인자를 받고, Int를 리턴합니다.
  • (Int,Int)->Int : Int, Int 총 2개를 받고, Int를 리턴합니다
  • (Int)->()->Unit : Int 인자를 받고, ()->Unit 타입인 다른 함수를 리턴합니다.
  • (()->Unit)->Unit : ()->Unit 타입인 함수를 인자로 받고, Unit을 리턴합니다. (반환값 없음)

함수 타입을 만드는 기본적인 방법은 다음과 같습니다.

 

  • 람다 표현식
  • 익명 함수
  • 함수 레퍼런스
// val name: type = ...
val plusLambda:    (Int, Int)->Int = { a, b -> a + b }
val plusAnonymous: (Int, Int)->Int = fun(a, b) = a + b
val plusReference: (Int, Int)->Int = ::plus

// 타입 추론
val plusLambda2 = { a: Int, b: Int -> a + b }
val plusAnonymous2 = fun(a: Int, b: Int) = a + b

 

코틀린에는 확장함수가 존재합니다. 확장함수는 아래와 같이 만들 수 있습니다.

fun Int.myPlus(other: Int) = this + other

확장 함수도 익명 함수로 만들 수 있으며, 이를 리시버를 가진 함수 타입이라고 부릅니다.

val myPlus: Int.(Int)->Int = fun Int.(other: Int) = thie + other
val myPlus = fun Int.(other: Int) = thie + other

확장 함수에서 리시버는 this 키워드로 참조할 수 있습니다.

val myPlus: Int.(Int)->Int = { this + it }

리시버를 가진 익명 확장 함수와 람다 표현식은 다음과 같은 방법으로 호출할 수 있습니다.

 

  • 일반적인 객체처름 invoke 메서드를 사용
  • 확장 함수가 아닌 함수처럼 사용
  • 일반적인 확장 함수처럼 사용
myPlus.invoke(1, 2)
myPlus(1, 2)
1.myPlus(2)

 

리시버를 가진 함수 타입의 가장 중요한 특징은 this의 참조 대상을 변경할 수 있다는 것입니다.

this는 apply 함수에서 리시버 객체의 메서드와 프로퍼티를 간단하게 참조할 수 있게 해 주기도 합니다.

inline fun <T> T.apply(block: T.() -> Unit): T {
    this.block()
    return this
}

class User {
    var name: String = ""
    var surname: String = ""
}

val user = User().apply {
    name = "Marcin"
    surname = "jiwon"
}

 

 

 

언제 사용해야 할까?

 

DSL을 정의한다는 것은 개발자의 인지적 혼란과 성능이라는 비용이 모두 발생할 수 있습니다.

DSL은 다음과 같은 것을 표현하는 것에 유용합니다.

 

  • 복잡한 자료 구조
  • 계층적인 구조
  • 거대한 양의 데이터

 


정리

 

  • DSL은 언어 내부에서 사용할 수 있는 특별한 언어다.
  • 복잡한 객체는 물론이고 HTML 코드, 복잡한 설정 등 계층 구조를 갖는 객체를 간단하게 표현할 수 있게 해준다.
  • DSL이 익숙하지 않은 개발자에게 혼란과 어려움을 줄 수 있다.
  • 하지만 잘 정의된 DSL은 프로젝트에 큰 도움을 준다.
반응형