kotlin을 처음 사용했을 때, 코드리뷰로 많이 조언받았던 함수들이다.
책이나, 인터넷을 봐도 이해가 잘 안됐지만, 몇번 사용해보고 나서 얻은 지식으로 (나름) 쉽게 풀어쓰고자 한다.
let
: A를 받아서 쓰고, B를 반환하겠다.
run
: A를 받아서 쓰고, B를 반환하겠다.
also
: A를 받아서 쓰고, A를 반환하겠다.
apply
: A를 받아서 쓰고, A를 반환하겠다.
with
: A를 받아서 쓰고, B를 반환하겠다.
위에 정의부분을 보면 똑같은 기능을 여러 메소드가 하고있다.
여러 메소드로 나눈 데에는 이유가 있을 터, 언제 사용하는지 알아보자.
* 물론 내부적으로 컨벤션이 다를 수 있다. 우리는 이렇게 사용한다~ 정도로만 알아두자.
let
: A를 통해 어떤 작업을 하고, 그 결과를 받아오는 경우
run
: A 내부의 메소드를 호출하고, 그 결과를 받아오는 경우 / 혹은 반환값이 없더라도 추가적인 작업 수행
also
: A를 이용해 로그를 찍는다던지, validation을 체크한다던지 반환값이 필요없는 추가작업 수행
apply
: A의 필드값을 변경하는 경우 / A 의 멤버메소드를 호출하는 경우
with
: A의 변수명이 길어서 반복적으로 쓰면 가독성이 떨어질 때, 동일한 변수명 재사용
사용 빈도는 let > also >> apply > with > run 정도 된다.
(run은 많이 사용하진 않는다.)
람다 수신 객체란, 람다 함수 내에서 it이나 this가 가리키는 객체를 의미한다.
이 객체에 접근할 때
let, also는 it
을
run, apply, with는 this
를 사용하게 된다.
왜 이런 차이가 생기게 되는걸까? 프로토타입을 확인해보자.
public inline fun <T, R> T.let(block: (T) -> R): R
public inline fun <T, R> T.run(block: T.() -> R): R
public inline fun <T> T.also(block: (T) -> Unit): T
public inline fun <T> T.apply(block: T.() -> Unit): T
public inline fun <T, R> with(receiver: T, block: T.() -> R): R
let, also 의 block은 (T) -> R 또는 (T) -> Unit 이고
run, apply, with의 block은 T.
() -> R 또는 T.
() -> Unit 이다.
즉, run, apply, with의 block은 확장함수 개념으로 들어가 this를 사용하게 된다.
this를 사용하게 됨으로써 주의해야할 점이 생기는데, 바로 쉐도잉(Shadowing)
문제이다.
data class Meeting(
val professorName: String,
val studentNo: Int,
val classRoomNo: Int,
)
data class Student(
val no: Int,
val name: String,
)
data class Professor(
val name: String,
val dept: String,
) {
fun makeMeeting(student: Student, classRoomNo: Int): Meeting {
return with(student) {
Meeting(
professorName = this.name,
studentNo = no,
classRoomNo = classRoomNo,
)
}
}
}
학생, 교수 각각의 클래스와
면담 데이터를 담은 Meeting 클래스가 있다고 치자.
교수가 면담을 잡기 위해서는 Professor 객체의 makeMeeting 메소드를 호출하면 된다.
fun main(){
val student = Student(no = 1, name="김학생")
val professor = Professor(name="김교수", dept="CS")
val meeting = professor.makeMeeting(student = student, classRoomNo = 13)
println(meeting)
}
김교수가 김학생과의 면담을 잡으면 결과가 어떻게 될까?
Meeting(professorName=김학생, studentNo=1, classRoomNo=13)
정답은, 김학생이 일기토를 이기고 교수자리에 앉게된다.
왜 이런 문제가 생겼을까?
this.name에서 this
는 Professor 가 아닌 with에 있는 student를 보기 때문이다.
이를 바로잡기 위해서는 아래와 같이 수정하면 된다.
fun makeMeeting(student: Student, classRoomNo: Int): Meeting {
return with(student) {
Meeting(
professorName = this@Professor.name,
studentNo = no,
classRoomNo = classRoomNo,
)
}
}
val memberBenefit = orderRepository.findByIdOrNull(orderNo)?.let {
benefitService.calculateByMemberNo(it.memberNo).benefit
}
val (productInfo, memberInfo) = Tuples.of(
async { productClient.getProduct(order.productNo) },
async { memberClient.getMember(order.memberNo) }
).run { Tuples.of(t1.await(), t2.await()!!) }
val request = request.awaitBodyOrNull<OrderChangeRequest>()?.also {
it.validate()
} ?: OrderChangeRequest.EMPTY
val order = Order(
no = orderNo,
ordererName = ordererName,
).apply {
updatePrice(3000) // price 필드는 생성자로 관리하지 않는다.
}
data class OrderRequestModel(
val no: String,
val ordererName: String,
...
) {
companion object {
fun createBy(orderRequest: OrderRequest): OrderRequestModel {
return with(orderRequest) {
OrderRequestModel(
no = no,
ordererName = ordererName,
...
)
}
}
}
}
뭐가 됐든 공식문서로 보는게 최고다.
https://kotlinlang.org/docs/scope-functions.html
Scope Functions에 대한 각각의 특징과 주의사항이 잘 정리되었군요 ㅎㅎㅎ!
정말 무의식적으로 쓰지만? 항상 쓰면서도 이게 맞나 헷갈리는 것 같아요 ㅎㅎ
저는 요즘 여러 고차함수를 체이닝해서 사용하는걸 지향하는데요, with 보다는 run을 더 많이 사용하게 되더라구요~(사실 with을 안씀...)
nullable 타입 다룰때도 간결하구여ㅎㅎ
말씀해주신대로, 회사 내부의 컨벤션에 따라 맞춰서 가독성을 확보하는게 우선일 것 같아여
좋은 글 감사합니당