Skip to content

중급: 리시버를 가진 람다 표현식

이 장에서는 다른 유형의 함수인 람다 표현식과 함께 리시버를 사용하는 방법과, 이들이 도메인 특화 언어(DSL)를 만드는 데 어떻게 도움이 되는지 배웁니다.

리시버를 가진 람다 표현식

초급 투어에서 람다 표현식을 사용하는 방법을 배웠습니다. 람다 표현식은 리시버를 가질 수도 있습니다. 이 경우 람다 표현식은 리시버를 매번 명시적으로 지정할 필요 없이 리시버의 모든 멤버 함수나 프로퍼티에 접근할 수 있습니다. 이러한 추가적인 참조가 없으면 코드를 더 쉽게 읽고 유지보수할 수 있습니다.

리시버를 가진 람다 표현식은 리시버를 가진 함수 리터럴로도 알려져 있습니다.

리시버를 가진 람다 표현식의 구문은 함수 타입을 정의할 때 다릅니다. 먼저 확장하려는 리시버를 작성합니다. 다음으로 .을 붙인 다음 나머지 함수 타입 정의를 완료합니다. 예를 들어:

kotlin
MutableList<Int>.() -> Unit

이 함수 타입은 다음을 가집니다:

  • MutableList<Int>를 리시버로 가집니다.
  • 괄호 () 안에 함수 파라미터가 없습니다.
  • 반환 값 Unit이 없습니다.

캔버스에 도형을 그리는 다음 예시를 고려해 보세요:

kotlin
class Canvas {
    fun drawCircle() = println("🟠 Drawing a circle")
    fun drawSquare() = println("🟥 Drawing a square")
}

// 리시버를 가진 람다 표현식 정의
fun render(block: Canvas.() -> Unit): Canvas {
    val canvas = Canvas()
    // 리시버를 가진 람다 표현식 사용
    canvas.block()
    return canvas
}

fun main() {
    render {
        drawCircle()
        // 🟠 Drawing a circle
        drawSquare()
        // 🟥 Drawing a square
    }
}

이 예시에서:

  • Canvas 클래스에는 원이나 사각형을 그리는 것을 시뮬레이션하는 두 개의 함수가 있습니다.
  • render() 함수는 block 파라미터를 받고 Canvas 클래스의 인스턴스를 반환합니다.
  • block 파라미터는 Canvas 클래스를 리시버로 가지는 리시버를 가진 람다 표현식입니다.
  • render() 함수는 Canvas 클래스의 인스턴스를 생성하고, canvas 인스턴스에 block() 람다 표현식을 리시버로 사용하여 호출합니다.
  • main() 함수는 block 파라미터에 전달되는 람다 표현식과 함께 render() 함수를 호출합니다.
  • render() 함수에 전달된 람다 내부에서 프로그램은 Canvas 클래스의 인스턴스에 대해 drawCircle()drawSquare() 함수를 호출합니다.

리시버를 가진 람다 표현식에서 drawCircle()drawSquare() 함수가 호출되기 때문에, 이 함수들은 Canvas 클래스 내부에 있는 것처럼 직접 호출될 수 있습니다.

리시버를 가진 람다 표현식은 도메인 특화 언어(DSL)를 만들고자 할 때 유용합니다. 리시버를 명시적으로 참조하지 않고도 리시버의 멤버 함수 및 프로퍼티에 접근할 수 있으므로 코드가 더욱 간결해집니다.

이를 시연하기 위해, 메뉴의 항목을 구성하는 예시를 고려해 봅시다. MenuItem 클래스와 메뉴에 항목을 추가하는 item() 함수 및 모든 메뉴 항목의 리스트 items를 포함하는 Menu 클래스부터 시작하겠습니다:

kotlin
class MenuItem(val name: String)

class Menu(val name: String) {
    val items = mutableListOf<MenuItem>()

    fun item(name: String) {
        items.add(MenuItem(name))
    }
}

메뉴를 빌드하는 menu() 함수에 함수 파라미터 (init)로 전달된 리시버를 가진 람다 표현식을 시작점으로 사용해 봅시다:

kotlin
fun menu(name: String, init: Menu.() -> Unit): Menu {
    // Menu 클래스의 인스턴스를 생성합니다.
    val menu = Menu(name)
    // 클래스 인스턴스에 대해 리시버 init()을 가진 람다 표현식을 호출합니다.
    menu.init()
    return menu
}

이제 DSL을 사용하여 메뉴를 구성하고 메뉴 구조를 콘솔에 출력하는 printMenu() 함수를 만들 수 있습니다:

kotlin
class MenuItem(val name: String)

class Menu(val name: String) {
    val items = mutableListOf<MenuItem>()

    fun item(name: String) {
        items.add(MenuItem(name))
    }
}

fun menu(name: String, init: Menu.() -> Unit): Menu {
    val menu = Menu(name)
    menu.init()
    return menu
}

fun printMenu(menu: Menu) {
    println("Menu: ${menu.name}")
    menu.items.forEach { println("  Item: ${it.name}") }
}

// DSL 사용
fun main() {
    // 메뉴 생성
    val mainMenu = menu("Main Menu") {
        // 메뉴에 항목 추가
        item("Home")
        item("Settings")
        item("Exit")
    }

    // 메뉴 출력
    printMenu(mainMenu)
    // Menu: Main Menu
    //   Item: Home
    //   Item: Settings
    //   Item: Exit
}

보시다시피, 리시버를 가진 람다 표현식을 사용하면 메뉴를 만드는 데 필요한 코드가 크게 단순화됩니다. 람다 표현식은 설정 및 생성뿐만 아니라 구성에도 유용합니다. API, UI 프레임워크 및 구성 빌더를 위한 DSL을 구축하는 데 일반적으로 사용되어 간결한 코드를 생성하므로 기본 코드 구조 및 논리에 더 쉽게 집중할 수 있습니다.

코틀린의 생태계에는 표준 라이브러리buildList()buildString() 함수와 같은 이 설계 패턴의 많은 예시가 있습니다.

리시버를 가진 람다 표현식은 코틀린의 **타입 안전 빌더 (Type-safe builders)**와 결합하여 런타임이 아닌 컴파일 시간에 타입 관련 문제를 감지하는 DSL을 만들 수 있습니다. 더 자세히 알아보려면 타입 안전 빌더를 참조하세요.

연습 문제

연습 문제 1

fetchData() 함수는 리시버를 가진 람다 표현식을 받습니다. 람다 표현식을 업데이트하여 append() 함수를 사용하도록 하여 코드의 출력이 Data received - Processed가 되도록 하세요.

kotlin
fun fetchData(callback: StringBuilder.() -> Unit) {
    val builder = StringBuilder("Data received")
    builder.callback()
}

fun main() {
    fetchData {
        // 여기에 코드를 작성하세요
        // Data received - Processed
    }
}
예시 솔루션
kotlin
fun fetchData(callback: StringBuilder.() -> Unit) {
    val builder = StringBuilder("Data received")
    builder.callback()
}

fun main() {
    fetchData {
        append(" - Processed")
        println(this.toString())
        // Data received - Processed
    }
}
연습 문제 2

Button 클래스와 ButtonEvent, Position 데이터 클래스가 있습니다. Button 클래스의 onEvent() 멤버 함수를 트리거하여 더블 클릭 이벤트를 발생시키는 코드를 작성하세요. 코드는 "Double click!"을 출력해야 합니다.

kotlin
class Button {
    fun onEvent(action: ButtonEvent.() -> Unit) {
        // 더블 클릭 이벤트를 시뮬레이션합니다 (우클릭 아님)
        val event = ButtonEvent(isRightClick = false, amount = 2, position = Position(100, 200))
        event.action() // 이벤트 콜백 트리거
    }
}

data class ButtonEvent(
    val isRightClick: Boolean,
    val amount: Int,
    val position: Position
)

data class Position(
    val x: Int,
    val y: Int
)

fun main() {
    val button = Button()

    button.onEvent {
        // 여기에 코드를 작성하세요
        // Double click!
    }
}
kotlin
class Button {
    fun onEvent(action: ButtonEvent.() -> Unit) {
        // 더블 클릭 이벤트를 시뮬레이션합니다 (우클릭 아님)
        val event = ButtonEvent(isRightClick = false, amount = 2, position = Position(100, 200))
        event.action() // 이벤트 콜백 트리거
    }
}

data class ButtonEvent(
    val isRightClick: Boolean,
    val amount: Int,
    val position: Position
)

data class Position(
    val x: Int,
    val y: Int
)

fun main() {
    val button = Button()
    
    button.onEvent {
        if (!isRightClick && amount == 2) {
            println("Double click!")
            // Double click!
        }
    }
}
연습 문제 3

각 요소가 1씩 증가된 정수 리스트의 사본을 생성하는 함수를 작성하세요. List<Int>incremented 함수로 확장하는 제공된 함수 스켈레톤을 사용하세요.

kotlin
fun List<Int>.incremented(): List<Int> {
    val originalList = this
    return buildList {
        // 여기에 코드를 작성하세요
    }
}

fun main() {
    val originalList = listOf(1, 2, 3)
    val newList = originalList.incremented()
    println(newList)
    // [2, 3, 4]
}
kotlin
fun List<Int>.incremented(): List<Int> {
    val originalList = this
    return buildList {
        for (n in originalList) add(n + 1)
    }
}

fun main() {
    val originalList = listOf(1, 2, 3)
    val newList = originalList.incremented()
    println(newList)
    // [2, 3, 4]
}