리시버를 가진 람다 식
리시버를 가진 람다 식 (Lambda Expressions with Receiver)
이 장에서는 또 다른 종류의 함수인 람다 식에서 리시버를 쓰는 법과, 이를 통해 도메인 특화 언어(DSL)를 만들 수 있다는 걸 배워 볼게요.
출처: Kotlin 공식문서
본문
리시버를 가진 람다 식 (Lambda expressions with receiver)
초급 투어에서 람다 식을 쓰는 방법을 배웠어요. 람다 식도 리시버를 가질 수 있어요. 이 경우 람다 식은 리시버의 멤버 함수나 프로퍼티를 매번 리시버를 명시하지 않고 바로 접근할 수 있어요. 이런 추가 참조가 없어지면 코드가 더 읽기 쉽고 유지보수하기 쉬워져요.
TIP: 리시버를 가진 람다 식은 '리시버를 가진 함수 리터럴(function literals with receiver)'이라고도 불러요.
리시버를 가진 람다 식의 문법은 함수 타입을 정의할 때 달라져요. 먼저 확장하고 싶은 리시버를 쓰고, 다음에 .을 붙인 뒤 함수 타입 정의의 나머지를 완성하면 돼요. 예를 들어:
MutableList<Int>.() -> Unit
이 함수 타입은:
MutableList<Int>를 리시버로 가져요.- 괄호
()안에 함수 파라미터가 없어요. - 반환 값이 없어요:
Unit.
캔버스에 도형을 그리는 이 예시를 살펴볼게요:
class Canvas {
fun drawCircle() = println("🟠 Drawing a circle")
fun drawSquare() = println("🟥 Drawing a square")
}
// Lambda expression with receiver definition
fun render(block: Canvas.() -> Unit): Canvas {
val canvas = Canvas()
// Use the lambda expression with receiver
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()함수를 호출해요.drawCircle()과drawSquare()함수가 리시버를 가진 람다 식 안에서 호출되므로, 마치Canvas클래스 안에 있는 것처럼 직접 호출돼요.
리시버를 가진 람다 식은 **도메인 특화 언어(DSL)**를 만들고 싶을 때 특히 유용해요. 리시버를 따로 참조하지 않고도 리시버의 멤버 함수와 프로퍼티에 접근할 수 있으니, 코드가 더 간결해지거든요.
이걸 보여 주기 위해, 메뉴에 항목을 구성하는 예시를 살펴볼게요. 먼저 MenuItem 클래스와, 메뉴에 항목을 추가하는 item() 함수와 모든 메뉴 항목의 리스트 items를 가진 Menu 클래스를 만들어 볼게요:
class MenuItem(val name: String)
class Menu(val name: String) {
val items = mutableListOf<MenuItem>()
fun item(name: String) {
items.add(MenuItem(name))
}
}
시작점으로, 메뉴를 만드는 menu() 함수에 함수 파라미터(init)로 전달되는 리시버를 가진 람다 식을 사용해 볼게요:
fun menu(name: String, init: Menu.() -> Unit): Menu {
// Creates an instance of the Menu class
val menu = Menu(name)
// Calls the lambda expression with receiver init() on the class instance
menu.init()
return menu
}
이제 DSL로 메뉴를 구성하고, 메뉴 구조를 콘솔에 출력하는 printMenu() 함수를 만들 수 있어요:
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}") }
}
// Use the DSL
fun main() {
// Create the menu
val mainMenu = menu("Main Menu") {
// Add items to the menu
item("Home")
item("Settings")
item("Exit")
}
// Print the menu
printMenu(mainMenu)
// Menu: Main Menu
// Item: Home
// Item: Settings
// Item: Exit
}
보시다시피 리시버를 가진 람다 식을 쓰면 메뉴를 만드는 데 필요한 코드가 훨씬 간단해져요. 람다 식은 초기 설정과 생성뿐 아니라 구성(configuration)에도 유용해요. API, UI 프레임워크, 설정 빌더에서 DSL을 만들 때 흔히 사용되며, 간결한 코드를 만들어 내복적인 코드 구조와 로직에 더 쉽게 집중할 수 있게 해 줘요.
Kotlin 생태계에는 표준 라이브러리의 buildList()와 buildString() 함수처럼 이 디자인 패턴을 쓰는 예시가 많이 있어요.
TIP: 리시버를 가진 람다 식은 Kotlin의 타입 안전 빌더(type-safe builders)와 결합해, 런타임이 아니라 컴파일 타임에 타입 문제를 잡아내는 DSL을 만들 수 있어요. 자세한 내용은 타입 안전 빌더(Type-safe builders) 문서를 참고하세요.
연습 (Practice)
연습 1
리시버를 가진 람다 식을 받는 fetchData() 함수가 있어요. 코드의 출력이 Data received - Processed가 되도록 람다 식을 append() 함수를 쓰도록 수정해 보세요.
fun fetchData(callback: StringBuilder.() -> Unit) {
val builder = StringBuilder("Data received")
builder.callback()
}
fun main() {
fetchData {
// Write your code here
// Data received - Processed
}
}
연습 2
Button 클래스와 ButtonEvent, Position 데이터 클래스가 있어요. Button 클래스의 onEvent() 멤버 함수를 트리거해서 더블 클릭 이벤트를 발생시키는 코드를 작성해 보세요. 코드는 "Double click!"을 출력해야 해요.
class Button {
fun onEvent(action: ButtonEvent.() -> Unit) {
// Simulate a double-click event (not a right-click)
val event = ButtonEvent(isRightClick = false, amount = 2, position = Position(100, 200))
event.action() // Trigger the event callback
}
}
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 {
// Write your code here
// Double click!
}
}
연습 3
모든 요소가 1씩 증가한 정수 리스트의 복사본을 만드는 함수를 작성해 보세요. List<Int>를 incremented 함수로 확장하는 주어진 함수 뼈대를 사용하세요.
fun List<Int>.incremented(): List<Int> {
val originalList = this
return buildList {
// Write your code here
}
}
fun main() {
val originalList = listOf(1, 2, 3)
val newList = originalList.incremented()
println(newList)
// [2, 3, 4]
}