Kotlin의 스코프 함수(scope function)란:
특정 객체 컨텍스트 내에서 코드 블록을 실행합니다.
코드의 명확성, 가독성 개선에 도움을 줍니다.
스코프 함수는 코드 가독성과, 라인 수 단축에 큰 도움을 줍니다.
코드 블록 범위를 지정해줘서, 메소드 분리 없이도 변수의 종료시점을 정할 수 있다는게 큰 장점입니다.
그러나 이렇게 좋은만큼, 어뷰징하기도 쉬운 기능입니다.
아래는 정확히 같은 기능을 수행하는 코드입니다.
어떤 코드가 더 읽기 쉬운가요?
대부분은 왼쪽 형태가 더 익숙하실거라 생각합니다.
차이를 더 잘 보여드리기 위해 제가 과장해서 코드를 꼬아두긴 했지만, 이렇게 안 쓰느니만 못한 상황이 있습니다.
즉 적재적소에 써야하지만, 문제는 어떤 함수를 써야하는지 헷갈리는 경우가 많습니다.
이 함수 써도 그만, 저 함수 써도 그만인 상황이 많아서인데요,
Kotlin에서도 공식 가이드라인을 주지는 않고, 기능적 차이에 대해서만 안내하고 있습니다.
팀 컨벤션이 있다면 그에 따르면 되지만, 없는 경우 어떻게 정해야 하는지 저 또한 곤란했습니다.
그래서 저는 나름의 기준을 세워서 개인 프로젝트에 활용해보고 있습니다.
스코프 함수에 대해 간단히 정리하고, 제가 어떤 컨벤션으로 사용하고 있는지 적어보려 합니다.
기본적인 스코프 함수 종류입니다.
함수 | 참조 | 반환값 | 확장함수 여부 |
---|---|---|---|
let | it | Lambda result | O |
run | this | Lambda result | O |
run | - | Lambda result | X (별도 객체 컨텍스트 없음) |
with | this | Lambda result | X (객체 컨텍스트를 매개변수로 받음) |
apply | this | Context object | O |
also | it | Context object | O |
this
와it
의 차이점?
this
- 람다 함수의 Receiver 객체를 참조 (
T.() -> R
)- 프로퍼티 접근 시
this
키워드 생략 가능- Receiver 객체 내부에서 코드를 실행하는 것처럼 작성 가능
- 코드 블록 내에서 외부 변수 혼용 시, 키워드 생략으로 인해 무엇이 멤버고 외부 변수인지 혼란스러울 수 있음
it
- 람다 함수의 Argument로 들어온 객체를 참조 (
(T) -> R
)this
와 달리, 외부 변수와 명확하게 구분됨
💖 표식을 붙여둔 문항을 가장 중요하게 생각합니다.
1. let
사실 상 let
의 주 용도라고 볼 수 있는 케이스입니다.
if문 없이 간결하게 null 체크가 가능합니다.
before
if (nums == null) {
return null
} else {
return nums.length == 0
}
after
nums?.let {
nums.length == 0
}
접근해야 하는 프로퍼티의 depth가 너무 깊은 경우, 별도 변수 선언 없이 let
으로 단축할 수 있습니다.
before
val students = school.classroom.students
students.get(0).friend.age += 1
students.length == 1
students.get(0).age += 1
after
school.classroom.students.let {
it.get(0).friend.age += 1
it.length == 1
it.get(0).age += 1
}
메소드 분리 없이도, 임시적으로 필요한 변수의 생명주기를 정할 수 있습니다.
before
val tmp = something()
...
// 같은 범위 안에 있는 변수에서 계속 tmp에 접근 가능
after
something().let { tmp ->
...
}
// tmp 접근 불가능
2. run
로직을 그룹핑할 때 일반적으로 메소드를 분리하는데요,
run
을 아래처럼 활용하면 별도로 메소드를 분리하지 않아도 됩니다.
(재활용 할 가능성이 없는 로직인 경우에만 권장합니다.)
before
fun test() {
testPositiveCase()
testNegativeCase()
}
private fun testPosiiveCase() {
...
}
private fun testNegativeCase() {
...
}
after
fun test() {
// test positive case
run {
}
// test negative case
run {
}
}
3. with
this
로 참조하기에, 멤버 접근 시 키워드를 생략할 수 있어 코드가 간결해보입니다.
오로지 멤버 프로퍼티에만 접근할 경우에 추천합니다.
before
val dto = Dto(member1 = foo, member2 = bar)
assertEqual(dto.member1, true)
assertEqual(dto.member2, true)
after
val dto = Dto(member1 = foo, member2 = bar)
with (dto) {
assertEqual(member1, true)
assertEqual(member2, true)
...
}
4. apply
따로 생성자를 만들 필요 없이 객체를 생성할 수 있다는게 큰 장점입니다.
before
class Instance(
val x: Int,
val y: Int,
val w: Int,
val h: Int
)
val instance1 = Instance(
x = 10
y = 20
w = 5
h = 15
)
after
class Instance()
val instance1 = Instance().apply {
x = 10
y = 20
w = 5
h = 15
}
전 기본적으로는 setter를 이용하지 않고,
객체 내부 메소드를 선언해서 값을 수정하도록 작성하고 있습니다.
before
class Student(
var age: Int
)
...
val student = Student(age = 1)
student.age += 1
after
class Student(
private var age: Int
) {
fun increaseAge() = apply {
age += 1
}
}
5. also
let
과 비슷하게 후작업에 사용되지만 반환형이 필요없을 때 사용합니다.
before
fun createEntity() {
val entity = Entity(...)
logger.debug('$entity created')
}
after
fun createEntity() = Entity(...)
.also { logger.debug('$it created') }