인프런/자바 개발자를 위한 코틀린 입문

13) 코틀린의 scope function

backend dev 2025. 1. 12.

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) 중 하나로, 객체를 람다식 내부에서 처리할 때 유용하게 사용됩니다.

주로 아래와 같은 상황에서 사용됩니다

  1. Nullable 객체를 안전하게 처리할 때 (?.let). 
  2. 객체를 잠깐 다른 이름으로 바꿔 작업할 때.
  3. 메서드 체이닝을 할 때.

 

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)
    }
}

 

 

스코프 함수가 필요한 이유

  1. 코드 가독성 향상 스코프 함수는 객체를 람다식으로 처리할 수 있게 해주므로, 코드가 간결하고 읽기 쉬워집니다. 객체를 여러 번 참조할 필요 없이, 람다 내에서 객체를 직접 다룰 수 있기 때문에 코드가 더 직관적으로 변합니다.
  2. 예를 들어, 다음과 같은 코드가 있을 때:
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을 적용하면 유용하게 활용할 수 있다

 

댓글