1. scope function이란 무엇인가
scope : 영역
function : 함수
scope function : 일시적인 영역을 형성하는 함수
Kotlin 표준 라이브러리에는 객체 내부에서 코드를 실행할 수 있는 여러 함수가 제공되는데 그게 scope function이다.
람다를 사용해 일시적인 영역을 만들고 코드를 더 간결하게 만들거나,
method chaning에 활용하는 함수를 scope function이라고 합니다
자바 코드 예시 1)
val hyeon9mak = Person("현구막", 28, "집")
println(hyeon9mak)
hyeon9mak.moveTo("사무실")
hyeon9mak.incrementAge()
println(hyeon9mak)
코틀린으로 리팩토링한 코드 1)
Person("현구막", 28, "집").let {
println(it)
it.moveTo("사무실")
it.incrementAge()
println(it)
}
자바 코드 예시 2)
fun printPerson(person: Person?) {
if (person != null) {
println(person.name)
println(person.age)
}
}
코틀린으로 리팩토링한 코드 2)
fun printPerson(person: Person?) {
person?.let{
println(it.name)
println(it.age)
}
}
let은 스코프 함수(scope function) 중 하나로, 객체를 람다식 내부에서 처리할 때 유용하게 사용됩니다.
주로 아래와 같은 상황에서 사용됩니다
- Nullable 객체를 안전하게 처리할 때 (?.let).
- 객체를 잠깐 다른 이름으로 바꿔 작업할 때.
- 메서드 체이닝을 할 때.
let은 inline 함수이다.
let은 확장함수이다. let을 사용하는 객체의 확장함수가 된다. [ null이 아니라는 가정하에 ]
파라미터는 익명함수이다. [ 함수타입이 적혀있음 ]
익명함수는 let을 사용하는 객체를 파라미터로 받는다.
let 함수의 람다식은 호출한 객체를 매개변수로 전달받습니다.
[ 코틀린에서 it은 호출한 객체를 의미한다. 즉 따로 설정해주지 않으면 기본이름이 it이다. ]
이 매개변수의 기본 이름이 it입니다.
it은 람다식 내부에서 let을 호출한 객체를 참조한다.
it은 기본 이름이므로, 원하는 경우 다른 이름을 지정할 수 있습니다
fun printPerson(person: Person?) {
person?.let{ employee ->
println(employee.name)
println(employee.age)
}
}
스코프 함수가 필요한 이유
- 코드 가독성 향상 스코프 함수는 객체를 람다식으로 처리할 수 있게 해주므로, 코드가 간결하고 읽기 쉬워집니다. 객체를 여러 번 참조할 필요 없이, 람다 내에서 객체를 직접 다룰 수 있기 때문에 코드가 더 직관적으로 변합니다.
- 예를 들어, 다음과 같은 코드가 있을 때:
val person = Person("Alice", 30)
person.name = "Bob"
person.age = 31
println(person.name)
println(person.age)
스코프 함수를 사용하면:
val person = Person("Alice", 30)
person.apply {
name = "Bob"
age = 31
}.also {
println(it.name)
println(it.age)
}
- 이렇게 변경할 수 있습니다. **apply**와 also 같은 스코프 함수를 사용하면 객체를 직접 수정하면서도 코드가 훨씬 더 직관적이고 간결해집니다.
- 코드 중복 감소 객체를 여러 번 참조하지 않고, 한 번만 참조하여 여러 작업을 수행할 수 있기 때문에 중복을 줄일 수 있습니다. 특히, 객체의 여러 속성을 수정하거나, 여러 메서드를 호출할 때 유용합니다.
- 체이닝(Chaining) 사용 가능 여러 스코프 함수를 체이닝하여 사용하면, 연속적으로 여러 작업을 처리할 수 있습니다. 예를 들어, apply, also, run, with 등의 스코프 함수들을 조합하여 한 번의 표현식으로 여러 작업을 처리할 수 있습니다.
val person = Person("Alice", 30)
person.apply {
name = "Bob"
}.also {
println(it.name)
}.run {
println("Person's age: $age")
}
2. scope function의 분류
let, run -> 익명함수의 결과를 반환한다.
also, apply -> 객체 그 자체를 반환한다.
with -> 확장함수가 아니다.
람다식에서는 가장 마지막 expression이 return값이 된다.
scope function 인 let과 run은 익명함수의 결과를 반환하므로 value1, value2는 age가 저장된다.
also , apply는 객체 자체를 반환하므로 value3, value4에는 person객체가 저장된다.
let , also는 scope function을 호출한 객체를 it으로 참조하고
run, apply는 scope function을 호출한 객체를 this로 참조한다.
this : 생략이 가능한 대신, 다른 이름을 붙일 수 없다.
it : 생략이 불가능한 대신, 다른 이름을 붙일 수 있다.
let의 경우 it 대신 p처럼 원하는 이름을 사용할 수 있다.
run의 경우 원하는이름을 붙여서 사용할 수 없지만 this를 생략할 수 있다.
let 경우 파라미터로 일반함수를 받는다 [ 함수 타입을 적어준 모습 ]
그래서 let을 사용할때 주로 익명함수를 전달하고 익명함수는 람다식으로 구현한다.
그리고 람다식에서는 전달된 인자가 하나인 경우 해당 인자를 it으로 참조할 수 있다.
그리고 람다식에서는 전달된 인자에 이름을 지정해서 사용할 수 있다.
run 같은 경우 확장함수를 파라미터로 받는다.
확장함수에서 this는 확장하려는 클래스의 객체 [= 리시버 객체]를 의미한다.
it은 람다식에서만 사용된다.
with
with(파라미터, 람다) : this를 사용해 접근하고, this는 생략 가능하다.
[ with는 확장함수가 아니다. ]
fun main() {
val person = Person("최태현", 100)
with(person){ // with는 파라미터로 리시버[리시버타입 객체]와 확장함수를 받는다, 즉 어떤객체와 그 객체의 확장함수를 받는다.
println(name) // this는 생략가능
println(this.age) // with는 this 사용가능 [ 파라미터가 확장함수이므로 ]
}
}
3. 언제 어떤 scope function을 사용해야 할까
let
let을 사용하는 경우
non-null 값에 대해서만 코드 블록을 실행시킬 때 : 제일 많이 사용되는 경우
val number: Int? = 42
number?.let {
println("The number is $it")
}
일회성으로 제한된 영역에 지역 변수를 만들 때
val numbers = listOf(1, 2, 3, 4, 5)
numbers.map { it * 2 }
.let { doubledNumbers ->
println("Doubled numbers: $doubledNumbers")
}
객체 초기화와 반환 값의 계산을 동시에 해야 할 때
fun main() {
val person = Person("Unknown", 0).let {
it.name = "Bob"
it.age = 30
it // 람다식의 마지막 expression은 해당 람다식의 반환값이 된다.
}
println(person)
}
run
run을 사용하는 경우
apply
apply를 사용하는 경우
apply 특징 : 객체 그 자체가 반환된다.
객체 설정을 할 때에 객체를 수정하는 로직이 call chain 중간에 필요할 때
data class Car(var brand: String, var color: String, var year: Int)
fun main() {
val car = Car("Unknown", "White", 0).apply {
brand = "Toyota"
color = "Red"
year = 2023
}
println(car) // Car(brand=Toyota, color=Red, year=2023)
}
also
also를 사용하는 경우
also 특징 : 객체 그 자체가 반환된다.
객체를 수정하는 로직이 call chain 중간에 필요할 때
with
with를 사용하는 경우
특정 객체를 다른 객체로 변환해야 하는데,
모듈 간의 의존성에 의해 정적 팩토리 혹은 toClass 함수를 만들기 어려울 때
data class UserDTO(val fullName: String, val age: Int)
data class User(val firstName: String, val lastName: String, val age: Int)
fun main() {
val userDTO = UserDTO("Alice Johnson", 25)
val user = with(userDTO) {
val nameParts = fullName.split(" ")
User(
firstName = nameParts[0],
lastName = nameParts[1],
age = age
)
}
println(user) // User(firstName=Alice, lastName=Johnson, age=25)
}
특정 객체를 기반으로 작업 수행하는 경우
data class Report(val title: String, val content: String)
fun main() {
val report = Report("Monthly Report", "This is the content of the report.")
val summary = with(report) {
"Title: $title\nContent Preview: ${content.take(20)}..."
}
println(summary)
// Title: Monthly Report
// Content Preview: This is the content...
}
4. scope function과 가독성
1번 코드 : if-else를 이용한 전통적인 코드
2번 코드 : scope function을 이용한 코틀린스러운 코드
1번코드가 훨씬 코드를 이해하기 쉽다.
1번코드가 더 좋은 코드인 이유
1. 구현 2는 숙련된 코틀린 개발자만 더 알아보기 쉽다. 어쩌면 숙련된 코틀린 개발자도 잘 이해하지 못할 수 있다.
2. 구현 1의 디버깅이 쉽다.
3. 구현이 1이 수정도 더 쉽다.
view.showPerson()의 결과가 null이라면 뒤에 ?: elvis연산자는 null일때 실행되므로
view.showError()를 실행시키게 된다.
즉 view.showPerson(), view.showError()둘다 실행되는 버그가 발생한다.
사용 빈도가 적은 관용구는 코드를 더 복잡하게 만들고 이런 관용구들을 한 문장 내에서 조합해 사용하면 복잡성이 훨씬 증가한다.
하지만 scope function을 사용하면 안되는 것도 아니다 적절한 convention을 적용하면 유용하게 활용할 수 있다
'인프런 > 자바 개발자를 위한 코틀린 입문' 카테고리의 다른 글
12) 코틀린의 이모저모 (0) | 2025.01.11 |
---|---|
11) 코틀린에서 컬렉션을 함수형으로 다루는 방법 (0) | 2025.01.10 |
10) 코틀린에서 람다를 다루는 방법 (0) | 2025.01.10 |
9) 코틀린에서 다양한 함수를 다루는 방법 (0) | 2025.01.10 |
8) 코틀린에서 배열과 컬렉션을 다루는 방법 (0) | 2025.01.09 |
댓글