스코프 함수·위임 — let·run·with·apply·also·by·lazy
코틀린 코드, 특히 안드로이드 코드를 읽으면 apply { }·?.let { }·by lazy { }가 끝없이 나온다. 스코프 함수(let·run·with·apply·also)는 객체를 임시 스코프 안에서 다루게 해 코드를 묶고 간결하게 만들고, **위임(by)**은 프로퍼티·구현을 다른 객체에 맡겨 보일러플레이트를 없앤다. 이 둘은 문법은 작지만 사용 빈도가 가장 높은 도구다. (출처: Kotlin Docs — Scope Functions / Delegation / Effective Kotlin Item 48·49.)
스코프 함수 5형제 — 두 축으로 구분한다
다섯이 헷갈리는 이유는 비슷해서다. 두 가지 축만 잡으면 정리된다.
- 컨텍스트 객체를 무엇으로 참조하나 —
this(run·with·apply) vsit(let·also) - 무엇을 반환하나 — 람다 결과(let·run·with) vs 객체 자신(apply·also)
| 함수 | 참조 | 반환 | 전형적 용도 |
|---|---|---|---|
let | it | 람다 결과 | null 처리·변환 (?.let) |
run | this | 람다 결과 | 객체 설정 후 값 계산 |
with | this | 람다 결과 | 한 객체에 여러 호출 묶기 |
apply | this | 객체 자신 | 설정(configuration) 후 그 객체 반환 |
also | it | 객체 자신 | 부수효과(로깅·검증)·체이닝 |
각각을 코드로 — 안드로이드 관용구
// apply — 객체를 만들고 '설정'한 뒤 그 객체를 반환 (가장 흔함)
val intent = Intent(this, DetailActivity::class.java).apply {
putExtra("id", 42)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
} // intent 반환
// let — null이 아닐 때만 + 변환
val upper: String? = name?.let { it.trim().uppercase() }
// also — 부수효과(로깅·검증)를 끼우고 원본을 그대로 흘림
val user = createUser().also { Log.d("TAG", "생성: $it") }
// run — 객체 컨텍스트에서 '값을 계산'해 반환
val area = rect.run { width * height }
// with — 한 객체에 여러 작업을 묶음 (확장 아님, 인자로 받음)
with(binding) {
title.text = "제목"
subtitle.text = "부제"
button.isEnabled = true
}
고르는 법 — 설정 후 객체가 필요하면
apply, null 처리·변환이면?.let, 로깅 같은 곁다리면also, 값을 계산해 받으면run, 한 객체에 여러 호출이면with. 헷갈리면 위 표의 두 축으로 되짚는다. 남용해 중첩이 깊어지면 오히려 가독성이 나빠지니 한두 단계로.
takeIf·takeUnless — 조건부로 자신/ null
val valid = input.takeIf { it.isNotBlank() } // 조건 맞으면 자신, 아니면 null
?: return // → Elvis와 자연스럽게 결합
위임 — by 로 일을 떠넘긴다
**클래스 위임(by)**은 인터페이스 구현을 다른 객체에 맡긴다 — *상속 대신 합성(composition)*으로 기능을 재사용하는 코틀린의 언어 차원 지원이다.
interface Repository { fun load(): List<Member> }
class CachingRepository(
private val delegate: Repository
) : Repository by delegate { // load()는 delegate에 자동 위임
// 필요한 메서드만 오버라이드, 나머진 delegate가 처리
}
프로퍼티 위임 — lazy·observable·맵
프로퍼티 위임은 getter/setter 로직을 위임 객체에 맡긴다. 안드로이드에서 매일 쓰는 셋:
// lazy — 첫 접근 때 한 번만 초기화 (스레드 안전 기본), 이후 캐시
val config: Config by lazy { loadConfig() }
// observable — 값이 바뀔 때마다 콜백
var name: String by Delegates.observable("") { _, old, new ->
Log.d("TAG", "$old → $new")
}
// 안드로이드 KTX — ViewModel·arguments 위임
private val viewModel: MyViewModel by viewModels() // Fragment KTX
private val userId: Long by lazy { requireArguments().getLong("id") }
lazy는 무거운 초기화(파일·DB·계산)를 실제 쓸 때까지 미루는 데 쓴다. lateinit(02장)과 비교하면 — lazy는 val·자동 초기화·스레드 안전, lateinit은 var·수동 초기화(안드로이드 뷰 바인딩처럼 나중에 외부가 주입).
정리 — 작지만 가장 자주 쓰는 도구
스코프 함수는 객체를 임시 스코프에서 다뤄 코드를 묶고, 위임은 프로퍼티·구현을 떠넘겨 보일러플레이트를 없앤다. 둘 다 문법은 사소하지만 안드로이드 코드 어디에나 나오니 — *두 축(this/it·반환)*과 lazy/by viewModels 관용구를 손에 익히는 게 실전의 출발점이다.
한 줄 요약 — 스코프 함수는 참조(this/it) × 반환(람다결과/객체자신) 두 축: 설정→
apply, null·변환→?.let, 곁다리→also, 값계산→run, 여러호출→with. 위임by는 합성으로 구현을 떠넘김, 프로퍼티 위임by lazy(지연·캐시)·observable·by viewModels().
(출처: Kotlin Docs — Scope Functions / Delegated Properties / Effective Kotlin Item 48 / Android KTX — by viewModels.)