반응형
Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |
Tags
- 테스트
- 책
- g 단위테스트
- ReactiveProgramming
- 회고
- 글또
- 안드로이드스튜디오
- Rxjava
- 안드로이드강좌
- Compose
- Gradle
- 자바
- 코틀린
- 병럴프로그래밍
- k8s
- Kotlin
- kotlin강좌
- viewmodel
- 알고리즘
- 알게되는
- 커스텀상태
- Coroutine
- android
- 디자인패턴
- 스레드
- theming
- mockito
- 병렬프로그래밍
- 안드로이드
- 코루틴
Archives
- Today
- Total
선생님, 개발을 잘하고 싶어요.
Kotlin in Action - 2부 11장 - DSL 만들기 본문
다루는 거
- 영역 특화 언어, DSL, Domain-Specific Language
- 수신 객체 지정 람다,
- invoke 관례
API에서 DSL로
- 클래스간 상호작용을 이해하기 쉽고 명확하게 표현할 수 있게 만드는 것이 목표
- 2부에서 살펴본 내용은 API를 깔끔하게 작성할 수 있게 돕는 코틀린 특성
- 깔끔하다는 것은?
- 코드를 읽을 때, 어떤 일이 벌어질지 명확하게 이해할 수 있어야함. 이를 위해서 이름과 개념의 선택을 잘 해야한다.
- 코드가 간결해야함, 불필요한 구문이나 보일러플레이트가 가능한 적어야한다.
- 깔끔하다는 것은?
개념적 DSL?
- 범용 프로그래밍 언어: 모든 문제를 충분히 풀 수 있는 기능을 제공
- 영역 특화 언어: 특정 과업 또는 영역에 초점을 맞추고 그 영역에 필요하지 않은 기능을 제거, (e.g. SQL, 정규식)
- DSL이 더 선언적(declarative)이다.
- 선언적 언어는 원하는 결과를 기술만 하고, 그 결과를 달성하기 위한 세부 실행은 언어를 해석하는 엔진에 맡긴다. 최적화는 엔진이 한다.
- DSL의 단점으로 DSL과 범용 언어로 만든 애플리케이션의 조합이 어렵는 점이 있다.
- 이러한 DSL은 IDE의 지원도 빈약하고 컴파일 시점 검증도 약하다.
- 이러한 문제를 해결하는 internal DSL이라는 개념이 유명해지고 있다.
내부 DSL
- 내부 DSL은 범용 언어로 작성된 프로그램의 일부다.
- 익스포즈드 라이브러리는 구체적인 과업(SQL)을 달성하기 위한 것이지만, 범용 언어(코틀린)의 라이브러리로 구현된다.
DSL의 구조
- DSL과 API 사이 잘 정의된 일반적 경계는 없다.
- DSL은 구조 또는 문법이 존재한다.
- 명령-질의 API: 함수 호출 시퀀스에 아무런 구조가 없음, 클라이언트가 하나씩 호출
- DSL: DSL 문법에 의해 정해지는 더 커다란 구조에 속함.
- 문법이 있기에 내부 DSL을 일종의 언어라고 부를 수 있다.
- DSL에서는 여러 함수 호출을 조합해서 연산한다.
- 타입 검사기는 여러 함수 호출이 바르게 조합됐는지 검사한다.
- 함수 이름은 보통 동사 역할을 한다.
- 함수 인자는 명사 역할을 한다.
- DSL 구조의 장점으로는 같은 문맥을 재사용할 수 있다는 점이다. → 문맥의 재사용, 문맥의 추상화
- 람다 중첩 혹은 메서드 호출 연쇄를 통해서 DSL 구조를 만든다.
HTML DSL에서 보는 인사이트
- 왜 내부 DSL로 만들까?
- 코틀린이 타입 안정성 보장
- DSL 안에서 코틀린 코드를 원하는 대로 사용 가능 (for, if …)
구조화된 API 구축: DSL에서 수신 객체 지정 DSL 사용
수신 객체 지정 람다
- 수신 객체 지정 람다 (lambda with a receiver)
fun buildString(
builderAction: StringBuilder.() -> Unit // 수신 객체가 있는 함수 타입의 파라미터 선언
): String {
val sb = StringBuilder()
sb.builderAction() // StringBuilder 인스턴스를 람다의 수신 객체로 넘김
return sb.toString()
}
val s = buildString {
this.append("Hello, ") // this 키워드는 StringBuilder를 지칭
append("World") // this는 묵시적으로 StringBuilder 지칭
}
- 확장 함수 타입 (extension function type)HTML을 만들기 위한 코틀린 DSL → HTML 빌더 (type-safe builder)
- 객체 계층 구조를 선언적으로 정의할 수 있다.
- 코틀린 빌더는 타입 안정성을 보장한다.
- ⭐️ 수신 객체 지정 람다가 이름 결정 규칙을 바꾼다.
- @DslMarker 애노테이션을 사용해 중첩된 람다에서 외부 람다의 수신 객체를 접근하지 못하게 제한할 수 있다.
- 수신 객체 지정 람다를 사용하면 코드 블록 내부에서 이름 결정 규칙을 바꿀 수 있으므로 이를 이용해 API에 구조를 추가할 수 있다.
- 코드 블록 내부에서는 receiver의 함수를 호출할 수 있다. 즉 수신 객체 지정 람다를 사용하면 호출할 수 있는 함수(이름)을 원하는 형태로 조정할 수 있다는 것
- 외부 DSL인 SQL이나 HTML을 별도 함수로 분리해 이름 부여하기는 어렵다. 하지만 내부 DSL은 일반 코드와 마찬가지로 반복되는 DSL 코드 조각을 새 함수로 묶어서 재사용할 수 있다.
invoke 관례
- invoke 관례를 활용해 객체를 호출가능하게 하는 것은 일상적인 기능은 아니라는 점에 유의하자.
- 복잡한 람다식에 이름을 부여하고 싶을 때, 코드 블럭을 나누고 싶을 때, invoke 관례를 이용할 수 있다.
class ImprtanctIssuePredicate(val project: String)
: (Issue) -> Boolean { // 함수 타입을 부모 클래스로 활용한다.
// invoke 관례를 구현한다.
override fun invoke(issue: Issue): Boolean {
return issue.project == project && issue.isImportant()
}
// 복잡한 술어 로직을 별도의 함수로 분리해서 관리할 수 있게되었다.
private fun Issue.isImportant(): Boolean {
return type == "Bug" && (priority == "Major" || priority == "Critical")
}
}
- invoke 관례를 활용하면 DSL를 다양한 방법으로 사용할 수 있다. 이럴 때 invoke 관례의 파라미터로 수신 객체 지정 람다를 받는 패턴을 활용할 수 있다.
dependencies.compile("junit:junit:4.11") // 일반 함수 처럼 표현하고 싶거나
dependencies { // 중첩 람다로 표현하고 싶을 수도 있다.
compile("junit:junit:4.11")
}
class DependencyHandler {
// 일반 함수 호출
fun compile(coordinate: String) { .. }
// 중첩 람다 표현 가능
operator fun invoke(body: DependencyHandler.() -> Unit) { body() }
}
중위 호출 연쇄
- 중위 호출 함수, infix: 메서드 호출에 따른 잡음을 줄여주는 기능
- 문맥을 결정하기 위한 용도의 object를 선언해서 사용할 수 있다.
- 하나의 함수 이름으로 적합한 오버로딩 함수를 선택하기 위함
- 적절한 반환을 받을 수 있음
멤버 확장 함수
- 클래스 안에서 확장 함수와 확장 프로퍼티를 선언하는 것
class Table {
fun <T> Column<T>.primaryKey(): Column<T>
fun Column<Int>.autoIncrement(): Column<Int>
}
- 다음과 같은 제약을 할 수 있다.
- Table 클래스 밖에서 이 둘을 호출할 수 없다.
- 수신 객체 타입을 제한한다. autoIncrement 같은 경우에는 Int 타입 칼럼에만 가능하다.
- 멤버 확장은 Table 원본 소스코드를 수정하지 않고는 확장할 수 없다.
- 맥락을 지정하기 위한 object를 내부적으로 활용하기도 한다.
fun Table.select(where: SqlEXpressionBuilder.() -> Op<Boolean>) : Query
object SqlExpressionBuilder {
infix fun<T> Column<T>.eq(t: T) : Op<Boolean>
}
- eq와 같은 함수는 SqlExpressionBuilder 통해 제공되어서 select의 조건을 지정하는 경우에만 사용할 수 있는 맥락을 제공한다.
'일상 > 책 리뷰' 카테고리의 다른 글
자바 병렬 프로그래밍 - 1부 2장 - 스레드 안정성 (0) | 2022.06.09 |
---|---|
자바 병렬 프로그래밍 - 0부 1장 - 개요 (0) | 2022.06.09 |
Kotlin in Action - 2부 10장 - 애노테이션과 리플렉션 (0) | 2022.06.06 |
Kotlin in Action - 2부 9장 - 제네릭스 (0) | 2022.05.29 |
Kotlin in Action - 2부 8장 - 고차 함수: 파라미터와 반환 값으로 람다 사용 (0) | 2022.05.28 |
Comments