수신 객체 지정 람다 (Function with Receiver)
수신 객체 지정 람다는 람다 안에서 특정 객체의 멤버에 this. 없이 접근할 수 있게 합니다. DSL과 빌더 패턴의 기반이 됩니다.
일반 람다 vs 수신 객체 지정 람다
// 일반 람다 — it으로 접근
val action: (StringBuilder) -> Unit = { sb ->
sb.append("Hello")
sb.append(", World!")
}
// 수신 객체 지정 람다 — this로 (생략 가능)
val action2: StringBuilder.() -> Unit = {
append("Hello") // this.append(...) 생략
append(", World!")
}StringBuilder.() -> Unit은 “StringBuilder를 수신 객체로 받아 Unit을 반환하는 람다”입니다.
함수 타입 표기
// 수신 객체 지정 함수 타입
TypeName.() -> ReturnType
TypeName.(Param) -> ReturnType
TypeName.(Param1, Param2) -> ReturnType예시:
val greet: String.() -> String = { "Hello, $this!" }
val add: Int.(Int) -> Int = { other -> this + other }
"Kotlin".greet() // "Hello, Kotlin!"
3.add(4) // 7고차 함수에서 활용
fun buildString(action: StringBuilder.() -> Unit): String {
val sb = StringBuilder()
sb.action() // sb가 수신 객체가 되어 action 실행
return sb.toString()
}
val result = buildString {
append("Hello") // this = StringBuilder
append(", ")
append("World!")
insert(0, ">>> ")
}
// ">>> Hello, World!"이것이 Kotlin 표준 라이브러리의 buildString과 동일한 구조입니다.
apply — 수신 객체 지정 람다의 대표 예
스코프 함수 apply가 수신 객체 지정 람다를 사용합니다.
// apply의 시그니처 (간략화)
fun <T> T.apply(block: T.() -> Unit): T {
block() // this(= T 인스턴스)에서 block 실행
return this
}
data class User(var name: String = "", var age: Int = 0, var email: String = "")
val user = User().apply {
name = "홍길동" // this.name = ...
age = 30
email = "hong@example.com"
}DSL 구축
수신 객체 지정 람다의 가장 강력한 활용입니다.
간단한 HTML DSL
class Tag(val name: String) {
private val children = mutableListOf<Tag>()
private var content = ""
fun tag(name: String, block: Tag.() -> Unit): Tag {
val child = Tag(name).apply(block)
children.add(child)
return child
}
operator fun String.unaryPlus() {
content = this
}
fun render(indent: Int = 0): String {
val prefix = " ".repeat(indent)
return if (children.isEmpty()) {
"$prefix<$name>$content</$name>"
} else {
"$prefix<$name>\n" +
children.joinToString("\n") { it.render(indent + 1) } +
"\n$prefix</$name>"
}
}
}
fun html(block: Tag.() -> Unit) = Tag("html").apply(block)
val page = html {
tag("body") {
tag("h1") { +"제목" }
tag("p") { +"본문 내용" }
}
}
println(page.render())
// <html>
// <body>
// <h1>제목</h1>
// <p>본문 내용</p>
// </body>
// </html>설정 DSL
class ServerConfig {
var host: String = "localhost"
var port: Int = 8080
var timeout: Int = 3000
inner class DatabaseConfig {
var url: String = ""
var maxPool: Int = 10
}
val database = DatabaseConfig()
fun database(block: DatabaseConfig.() -> Unit) = database.apply(block)
}
fun server(block: ServerConfig.() -> Unit): ServerConfig =
ServerConfig().apply(block)
val config = server {
host = "prod.example.com"
port = 443
timeout = 5000
database {
url = "jdbc:postgresql://localhost/mydb"
maxPool = 20
}
}this가 두 개인 경우
멤버로 정의된 확장 함수에서는 수신 객체가 두 개입니다.
class A {
val valueA = "A"
fun String.process() {
println(this) // String 수신 객체
println(this@A.valueA) // A 수신 객체 (레이블로 구분)
}
fun test() {
"hello".process()
}
}@DslMarker — DSL 스코프 제한
중첩 DSL에서 바깥 스코프의 함수를 실수로 호출하는 것을 막습니다.
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class BodyTag {
fun p(block: PTag.() -> Unit) { /* ... */ }
}
@HtmlDsl
class PTag {
operator fun String.unaryPlus() { /* ... */ }
}
fun body(block: BodyTag.() -> Unit) = BodyTag().apply(block)
body {
p {
+"내용"
// p { } // @DslMarker로 인해 컴파일 에러 — p 안에서 p 호출 불가
}
}표준 라이브러리에서의 수신 객체 지정 람다
// buildString
val s = buildString {
repeat(3) { append("ha") }
} // "hahaha"
// buildList
val list = buildList {
add(1)
addAll(listOf(2, 3, 4))
add(5)
}
// with
val result = with(StringBuilder()) {
append("Hello")
append(", World!")
toString()
}
// apply, run, also, let 모두 수신 객체 지정 람다 활용정리
- 수신 객체 지정 람다:
Type.() -> Unit— 람다 안에서this없이 수신 객체 멤버 접근 - DSL의 기반 — 선언적이고 읽기 쉬운 API 구축
apply,run,with등 스코프 함수가 이 방식으로 구현됨@DslMarker— 중첩 스코프에서 잘못된 함수 호출 방지