
코틀린 let, run, apply, also, with 차이는 표만 보면 더 헷갈립니다. 실제로는 이름보다 this와 it, 반환값이 객체인지 계산 결과인지부터 보면 훨씬 빨리 정리됩니다.
이번 글은 문법 나열보다 선택 기준에 집중합니다. 객체 설정, null 처리, 부수 효과, 짧은 계산이라는 실전 장면으로 나눠 보겠습니다.
헷갈릴 때는 반환값부터 보세요. 객체를 계속 쓰려면 apply와 also, 계산 결과가 필요하면 let과 run, with 쪽입니다.
코틀린 let, run, apply, also, with 차이부터 먼저 보자
run, apply, with는 블록 안에서 객체를 receiver처럼 다룹니다. 그래서 멤버를 바로 부르기 편하고 설정 코드에서 특히 읽기 쉽습니다.
val user = User().apply {
name = "Bora"
age = 29
}반대로 let과 also는 객체를 람다 인자로 받습니다. 그래서 이 값을 가지고 무엇을 한다는 느낌이 더 분명합니다.
val length = name.let {
println(it)
it.length
}멤버를 많이 만지면 this 계열이 편하고, 값을 인자로 넘기며 다루면 it 계열이 더 읽기 쉽습니다.
반환값이 진짜 선택 기준이다
apply와 also는 객체 자신을 반환합니다. 그래서 체이닝을 이어가기 좋고, 설정이나 부가 작업을 붙인 뒤 원래 객체를 계속 쓸 수 있습니다.
val config = mutableMapOf<String, String>()
.also { println("기본 설정 생성") }
.apply {
put("mode", "debug")
put("region", "kr")
}let, run, with는 람다의 마지막 결과를 반환합니다. 그래서 객체를 바탕으로 새 문자열이나 계산 결과를 만들 때 더 자연스럽습니다.
val message = user.run {
if (age >= 20) "성인: $name" else "미성년자: $name"
}let은 왜 nullable 처리에서 자주 보일까
let이 유명한 이유 중 하나가 ?.let 패턴입니다. null이 아닐 때만 블록을 실행하고, 그 안에서는 값을 non-null처럼 다루기 쉽기 때문입니다.
val email: String? = getEmail()
val normalized = email?.let {
it.trim().lowercase()
}그래서 nullable 값을 잠깐 안전하게 꺼내 계산할 때 let이 잘 맞습니다. 다만 nullable이라고 무조건 let이 정답은 아닙니다. 기본값 하나만 넣고 끝날 때는 Elvis가 더 직접적일 수 있습니다.
val displayName = userName?.trim() ?: "Guest"apply는 객체 설정용일 때 가장 자연스럽다
apply는 생성 직후 설정하거나, 설정 블록을 묶고 싶을 때 가장 읽기 좋습니다. 블록 끝에서 객체 자신을 다시 돌려주기 때문입니다.
data class User(
var name: String = "",
var age: Int = 0,
var city: String = ""
)
val user = User().apply {
name = "Mina"
age = 31
city = "Seoul"
}run은 계산 결과가 필요할 때 좋다
run은 this 스타일인데 반환값은 람다 결과입니다. 그래서 객체 문맥 안에서 멤버를 여러 개 쓰고, 마지막에 다른 값을 계산할 때 편합니다.
val label = user.run {
if (age >= 20) "$name is adult" else "$name is minor"
}객체를 다듬고 그대로 반환하면 apply, 객체 문맥에서 계산 결과를 반환하면 run이라고 기억하면 빠릅니다.
also는 부수 효과를 붙일 때 잘 맞는다
also는 객체 자신을 그대로 반환합니다. 대신 블록 안에서는 it로 받습니다. 이 조합 때문에 중심 작업을 바꾸기보다 로그, 추적, 검증처럼 옆에서 한 번 끼어드는 작업에 잘 맞습니다.
val savedUser = user
.also { println("저장 전 검증: $it") }
.also { audit("save-user", it.id) }with는 왜 아직도 쓰일까
with는 extension function이 아니라, 객체를 인자로 받는 함수입니다. 그래도 이미 손에 있는 객체를 기준으로 여러 멤버를 묶어 읽고, 마지막에 결과 하나를 만들 때 깔끔할 수 있습니다.
val summary = with(user) {
"$name / $age / $city"
}최근 코드에서는 obj.run으로 비슷한 장면을 많이 처리하기도 합니다. 그래서 with가 틀렸다기보다 덜 보이는 경우가 많다고 보는 편이 맞습니다.
가장 빠른 선택 규칙
- 반환값이 객체 자신인가, 계산 결과인가
- 블록 안에서 멤버를 바로 만질 건가, 값을 인자로 다룰 건가
- 목적이 설정인가, 계산인가, 부수 효과인가
- nullable 값 잠깐 처리: let
- 객체 초기화와 설정: apply
- 객체 문맥에서 결과 계산: run
- 로그, 디버깅, audit 같은 옆작업: also
- 이미 있는 객체를 묶어 읽고 결과 계산: with
반환값을 먼저 보고, 그다음 this인지 it인지 보면 대부분 바로 고를 수 있습니다.
자주 하는 실수
let이 익숙해지면 모든 체인 끝에 붙이고 싶어집니다. 그런데 멤버 설정이 중심인 코드까지 let으로 밀어붙이면 오히려 it 반복이 늘어나 가독성이 떨어질 수 있습니다.
user?.let {
it.address?.run {
also { println(it) }
}
}scope function 중첩은 문법상 가능하지만 지금의 this나 it가 누구인지 놓치기 쉽습니다. also 안에서 본 작업까지 다 처리하는 것도 의도를 흐릴 수 있습니다.
정리
코틀린 let, run, apply, also, with 차이는 이름 다섯 개를 외우는 문제가 아닙니다. 객체를 어떻게 가리키는지와 무엇을 반환하는지부터 보면 거의 끝납니다.
관련해서 함께 보면 좋은 글은 코틀린 data class: 실무에서 어디까지 쓸까, 코틀린 sealed class vs enum: 상태 모델링 기준입니다. 공식 참고 자료로는 Kotlin scope functions 문서와 Kotlin null safety 문서를 같이 읽어보면 좋습니다.