[Effective Kotlin] 아이템 35.복잡한 객체를 생성하기 위한 DSL을 정의하라

0
post-thumbnail

DSL은 복잡한 객체, 계층구조를 갖고있는 객체를 정의할 때 굉장히 유용하다.

코틀린 테스트를 활용해 테스트 케이스를 정의했다.

class MyTest : StringSpec({
	"length sould return size of string" {
   		"hello".length shouldBe 5
    }
    "strtsWith should test for a prefix" {
     	"world" should startWith("wor")
    }

})

Gradle 설정을 정의할때에도 Gradle Dsl이 사용된다.

DSL을 활용하면 복잡하고 계층적인 자료구조를 쉽게 만들수 있다.
이미 존해나는것도 좋지만, 사용자 정의 DSL을 만드는것도 알아두는것이 좋다.

사용자 정의 DSL 만들기

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


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)
    	}
    }
}

함수타입의 몇가지 예를 살펴보자.

  • () -> Unit : No argument Unit(no) retrun
  • (Int)-> Unit : Int argument Unit(no) Return
  • (Int)-> () -> Unit : Int를 아규먼트로 받고, 다른 함수를 리턴한다.
    이때의 다른함수는 Unit을 리턴한다.
  • (()->Unit) -> Unit : 다른함수를 아규먼트로 받고 Unit를 리턴한다.
    이때의 다른함수는 아규먼트로 아무것도 받지않고 Unit을 리턴한다.

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

  • 람다 식
  • 익명함수
  • 함수 레퍼런스

fun plus(a: Int, b: Int) = a+b 같은 함수가 있다고 생각해보자.

val plus1 : (Int, Int) -> Int = {a , b -> a+b}
val plus2 : (Int, Int) -> Int = fun(a, b) = a+b
val plus3 : (Int, Int) -> Int = ::plus

프로퍼티 타입이 지정되어있기때문에 람다와 익명함수의 아규먼트 타입을 추론할수 있다.
반대로 아규먼트 타입을 지정해서 함수의 형태를 추론할수 있다.

val plus4 = {a : Int , b : Int -> a+b}
val plus5 = fun(a: Int, b : Int) = a+b

함수 타입은 '함수를 나타내는 객체'를 표현하는 타입이다.
익명은 이름을 갖고있지 않은 함수이며, 람다는 익명함수를 짧게 작성할 수 있는 표기 방법입니다.

함수를 나타내는 타입이 있다면 확장함수는 어떻게 표현할 수 있을까?

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

익명확장 함수 또한 가능하다.

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

이 함수의 타입은 학장함수를 나타내는 특별한 타입, 리시버를 가진 함수 타입이라고 부른다.

일반저긴 함수 타입과 비슷하지만, 파라티머 앞에 리시버 타입이 추가되어 있다.

val myPlus : Int. (Int)-> Int = fun Int.(other : Int) = this + 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 = "Moskala"
}

리시버를 가진 함수타입은 DSL을 구성하는 가장 기본적인 블록이다.

fun createTable() : TableDsl = table{
	tr{
    	for(i in 1..2){
        	td{
            	+"This is column $i"
            }
        }
    }
}

함수가 텝레벨에 위치하고 별도의 리시버를 갖지 않으므로 table 함수도 톱레벨에 있어야 한다.

tr 함수는 table 내부에서만 허용되어야 하며, 따라서 table 함수의 아규먼트는 tr함수를 갖는 리시버를 가져야한다.

fun table(init : TableBuilder.() -> Unit) : TableBuilder{

}

class TableBuilder{
 fun tr(init : TrBuilder.() -> Unit) {}
}

class TrBuilder{
 fun td(init : TdBuilder.() -> Unit) {}
}

"This is column" 은 단순하게 문자열에 적용된 단항 +연산자이기 때문에 아래와 같이 정의한다.

class TdBuildr{
	var text = ""
    
    operater fun Stirng.unaryPlus(){
    	text += this
    }

}

이렇게 DSL을 모두 정의했고, 파라미터를 활용하여 적절하게 값들을 초기화 하면 된다.

fun table(init : TableBuilder.() -> Unit) : TableBuilder{
 val TaleBuilder = TableBulder()
 init.invoke(tableBuilder)
 return tableBuilder
}

이전에 언급한것 처럼 apply 함수를 사용하면 더욱 간단하게 만들 수 있다.

fun table(init : TableBuidler.() -> Unit) = TableBuilder().apply(init)

언제 사용하면 좋을까?

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

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

DSL없이 빌더나 단순하게 생성자만 활용해도 원하는 모든것을 표현할수 있으나,
코드를 읽는 사람에게 필요없는 반복적인 코드가 있을경우 DSL을 고려해보자.

정리

DSL은 복잡한 객체를 만들거나 복잡한 계층 구조를 갖는 객체를 만들때만 활용하는것이 좋다.

profile
쉽게 가르칠수 있도록 노력하자

0개의 댓글