Kotlin 스코프 함수 완전 정리 - let, run, apply, also, with
Java 개발자가 Kotlin을 처음 접하면 let, apply, run 같은 코드를 보고 당황한다.
이 글에서는 이런 함수들이 왜 나왔는지, 어떤 원리로 동작하는지 기초부터 정리한다.
왜 이런 함수가 나왔는가?
Java 시절의 불편함
// 객체 생성 후 설정할 때 반복이 많다
User user = new User();
user.
setName("Kim");
user.
setAge(30);
user.
setEmail("kim@test.com");
user.
setPhone("010-1234-5678");
// user. user. user. user. 계속 반복...
// null 체크도 지저분하다
String name = getName();
if(name !=null){
String upper = name.toUpperCase();
System.out.
println(upper);
}
코틀린 설계자들이 이런 반복과 장황함을 줄이고 싶었다.
이해를 위한 기초 개념
함수는 값이다
val greet: (String) -> String = { name -> "안녕 $name" }
greet("Kim") // "안녕 Kim"
람다(Lambda) = 이름 없는 함수
// 일반 함수
fun double(x: Int): Int {
return x * 2
}
// 같은 걸 람다로
val double = { x: Int -> x * 2 }
마지막 파라미터가 람다면 괄호 밖으로 뺄 수 있다
// 원래 문법
listOf(1, 2, 3).filter({ it > 1 })
// 코틀린 관례: 람다를 밖으로
listOf(1, 2, 3).filter { it > 1 }
확장 함수 (Extension Function)
기존 클래스에 함수를 추가할 수 있다.
fun String.shout(): String {
return this.uppercase() + "!!!"
// 여기서 this = 이 함수를 호출한 String 객체
}
"hello".shout() // "HELLO!!!"
수신 객체가 있는 람다 (Lambda with Receiver)
확장 함수를 람다 버전으로 만든 것이다. 이것이 스코프 함수의 핵심 원리다.
// 일반 람다: 파라미터로 받음
val greet1: (String) -> String = { name -> "안녕 $name" }
// 수신 객체 람다: this로 접근
val greet2: String.() -> String = { "안녕 $this" }
// 호출
greet1("Kim") // "안녕 Kim"
"Kim".greet2() // "안녕 Kim"
직접 만들어보며 이해하기
apply를 직접 구현
fun <T> T.myApply(block: T.() -> Unit): T {
this.block() // 자기 자신(this)에서 람다 실행
return this // 자기 자신 반환
}
분해하면:
T.myApply→ 아무 타입에나 붙일 수 있는 확장 함수block: T.() -> Unit→ 수신 객체 람다 (안에서 this = T)return this→ 자기 자신 반환
val user = User().myApply {
// 여기서 this = User 객체
name = "Kim" // this.name = "Kim"
age = 30 // this.age = 30
}
// user = 설정된 User 객체
let을 직접 구현
fun <T, R> T.myLet(block: (T) -> R): R {
return block(this) // 자기 자신을 파라미터로 넘김
}
apply와 차이:
block: (T) -> R→ 일반 람다 (파라미터it으로 받음)return block(this)→ 람다 결과 반환 (자기 자신이 아님)
val length = "Hello".myLet {
// 여기서 it = "Hello"
println(it)
it.length // 이것이 반환됨
}
// length = 5
두 가지 축으로 정리
스코프 함수는 딱 두 가지 선택의 조합이다.
축 1: 객체를 어떻게 참조하나?
| this (수신 객체) | it (파라미터) |
|---|---|
| 멤버에 바로 접근 | 명시적으로 접근 |
name = "Kim" |
it.name = "Kim" |
| apply, run, with | let, also |
축 2: 무엇을 반환하나?
| 객체 자신 반환 | 람다 결과 반환 |
|---|---|
| 체이닝에 유리 | 변환에 유리 |
| apply, also | let, run, with |
조합표
| 객체 자신 반환 | 람다 결과 반환 | |
|---|---|---|
| this로 참조 | apply | run, with |
| it로 참조 | also | let |
이 표에서 5개 함수가 전부 나온다.
전체 비교
| 함수 | 객체 참조 | 반환값 | 주 용도 |
|---|---|---|---|
let |
it |
람다 결과 | null 체크, 변환 |
run |
this |
람다 결과 | 객체 설정 + 결과 계산 |
apply |
this |
객체 자신 | 객체 초기화/설정 |
also |
it |
객체 자신 | 부수 효과 (로깅 등) |
with |
this |
람다 결과 | 이미 있는 객체에 여러 작업 |
하나씩 실전 예제
apply — “이 객체를 설정하고, 그 객체를 돌려줘”
// Before (Java 스타일)
val paint = Paint()
paint.color = Color.RED
paint.style = Paint.Style.FILL
paint.textSize = 16f
// After (apply)
val paint = Paint().apply {
color = Color.RED
style = Paint.Style.FILL
textSize = 16f
}
let — “null 아니면 이걸로 뭔가 해줘”
// Before
val order = getOrder()
if (order != null) {
processOrder(order)
}
// After
getOrder()?.let { order ->
processOrder(order)
}
// 변환에도 유용
val display = user.name?.let { "이름: $it" } ?: "이름 없음"
run — “설정도 하고, 결과도 계산해줘”
val isAdult = user.run {
println("검사 대상: $name")
age >= 18 // 이것이 반환됨
}
also — “원래 하던 거 하고, 추가로 이것도 해줘”
val sorted = numbers
.also { println("정렬 전: $it") }
.sorted()
.also { println("정렬 후: $it") }
with — “이 객체 가지고 여러 작업 할게”
// 유일하게 확장 함수가 아님: with(객체) { ... }
with(textView) {
text = "Hello"
textSize = 16f
visibility = View.VISIBLE
}
체이닝 예제
fun createUser(email: String?): User? {
return email?.let { validEmail -> // null 체크
User().apply { // 객체 초기화
this.email = validEmail
this.name = validEmail.split("@")[0]
}.also { // 로깅
println("유저 생성: ${it.name}")
}
}
}
흔한 실수와 주의점
it 중첩 시 헷갈림
// 나쁜 예: 중첩되면 it이 뭔지 헷갈림
user?.let {
it.orders?.let {
it.first() // 이 it은 orders임, user가 아님!
}
}
// 좋은 예: 이름을 붙여주자
user?.let { currentUser ->
currentUser.orders?.let { orderList ->
orderList.first()
}
}
과용하지 말 것
// 굳이 스코프 함수를 쓸 필요 없는 경우
name?.let { println(it) }
// 그냥 이게 더 낫다
if (name != null) println(name)
코드가 더 읽기 쉬워질 때만 사용하자.
선택 기준 요약
객체를 설정하고 그 객체가 필요? → apply
객체를 설정하고 다른 결과가 필요? → run
null 체크 후 변환? → let
로깅/디버깅 등 부수 효과? → also
이미 있는 객체에 여러 멤버 접근? → with
댓글