Kotlin Scope 함수에 대해 알아보자

Ryan·2025년 11월 14일
0

Kotlin

목록 보기
5/5
post-thumbnail

코틀린의 Scope 함수를 정리하고 학습한 뒤 각각에 어떤 상황에서 scope 함수를 쓰는것이 좋을지 알아보겠습니다.

Scope 함수?

Scope 함수란?

Scope 함수(Scope Function) 는 객체를 더 간결하고 읽기 쉽게 다루기 위해,
객체의 컨텍스트(Context) 안에서 람다 블록을 실행하도록 도와주는 Kotlin 표준 라이브러리의 함수입니다.

스코프 함수를 사용한 예를 들면서 한번 볼게요.

val person = Person()
person.name = "Tom"
person.age = 30
person.introduce()

// Scope 함수 적용
val person = Person().apply {
    name = "Tom"
    age = 30
    introduce()
}

매번 person.을 써야 해서 코드가 길고 보기 불편하죠.
이걸 한 번에 묶어서 표현할 수 있는 게 바로 Scope 함수예요.

💡 추가로 알아보면 좋을 것

  • 고차 함수(Higher-Order Functions)란?
    함수를 인자로 받거나, 함수를 반환하는 함수
    즉, 함수를 데이터처럼 다루는 개념
      fun operate(x: Int, y: Int, operation: (Int, Int) -> Int): Int { // 함수 전달
          return operation(x, y)
      }
  • 람다 표현식(lambda expression )이란?
    이름이 없는 함수(익명 함수, anonymous function)
    간단한 함수를 한 줄로 표현하는 문법
     fun main() {
         val result = operate(3, 5) { a, b -> a + b } // { paramter -> body } 형식
         println(result) // 8
     }
  • 수신 객체란?
    수신 객체 타입: 확장함수를 가지게 되는 클래스의 이름
    수신 객체: 확장 함수를 호출하는 클래스의 객체

Scope 함수 종류

스코프 함수에는 let, run, apply, also, with 가 있어요.
각각은 유사한 기능을 수행하지만 함수 정의와 구현에 각각이 차이가 있어 다르게 사용됩니다.

함수(Function)객체 참조(Object Reference)결과 값(Return Value)사용 사례(Common Use Case)
letit람다 마지막행(Lambda result)null 처리 또는 값 변환
runthis람다 마지막행(Lambda result)계산 또는 초기화
applythis수신 객체(Object itself, 객체 자체)객체 구성
alsoit수신 객체(Object itself, 객체 자체)side effect 추가
withthis람다 마지막행(Lambda result)그룹화 작업
  • 각각의 스코프 함수 내부 정의
    inline fun <T, R> with(receiver: T, block: T.() -> R): R { // 수신객체 지정 람다
        return receiver.block()
    }
    
    inline fun <T> T.also(block: (T) -> Unit): T { // 람다 파라미터
        block(this)
        return this
    }
    
    inline fun <T> T.apply(block: T.() -> Unit): T { // 수신객체 지정 람다
        block()
        return this
    }
    
    inline fun <T, R> T.let(block: (T) -> R): R {  // 람다 파라미터
        return block(this)
    }
    
    inline fun <T, R> T.run(block: T.() -> R): R { // 수신객체 지정 람다
        return block()
    }

this 와 it

Kotlin의 Scope 함수(let, apply, run, also, with)는 객체를 블록 안에서 사용할 수 있는 공통점이 있습니다.
그런데 블록 안에서 그 객체를 부르는 방식(객체 참조)이 두 가지로 나뉘어요.

1️⃣ this — 수신 객체 지정 람다 (Receiver Lambda)

  • 객체의 내부 컨텍스트에서 코드를 실행
  • 객체 내부에 프로퍼티를 바로 사용
  • this는 생략 가능

위와 같은 특징들로 this 는 주로 함수를 호출하거나 내부에서 프로퍼티를 접근하는 경우 유용합니다.

val person = Person().apply {
    name = "안드콩"     // this.name
    age = 25          // this.age
    introduce()       // this.introduce()
}

여기서 thisPerson 가리키게 됩니다. apply 블로 안에서 프로퍼티를 수정하거나 메소드를 바로 호출이 가능하게합니다.

2️⃣ it — 람다 인자 (Lambda Argument)

  • 객체를 인자로 넘겨받는 형태
  • 외부에서 객체를 전달받아 다루는 방식
  • it은 생략 불가능
val person = Person("안드콩", 25)
person.also { it -> 
    println("이름: ${it.name}, 나이: ${it.age}")
}

val person = Person("안드콩", 25)
person.also { value ->
    println("이름: ${value.name}, 나이: ${value.age}")
}

여기서 itperson 을 가리키게 됩니다.


이제 !! 각각의 함수에 대한 종류와 목적에 대해 알아보겠습니다.

그전에 하나 정리하고 가자면 ~

  • apply also 는 (수신)객체 자체를 반환합니다.
  • let run with 는 람다 결과(람다의 마지막 행) 을 반환합니다.

let(Null saftey 및 값 변환)

let은 nullable 객체를 처리하거나 값을 변환하는 데 주로 사용합니다.
객체를 it 로 참조하고 람다의 결과를 반환합니다.

3가지 정도의 경우에 사용합니다.

  • 지정된 값이 null 이 아닌 경우에 코드를 실행해야 하는 경우.
  • Nullable 객체를 다른 Nullable 객체로 변환하는 경우.
  • 단일 지역 변수의 범위를 제한 하는 경우.
@Composable
fun ProfileScreen(user: User?) {    // nullable 처리
    user?.let {
        Text("이름: ${it.name}")
    } ?: Text("사용자 정보가 없습니다.")
}
    
val nicknameLength = user?.let { it.nickname.length } ?: 0 // 값 변환

getPersonDao().let { dao ->    // 지역 스코프 제한
    dao.insert(Person("안드콩", 25))
}

run(계산 및 초기화)

runthis를 사용하여 객체를 참조하고 람다의 결과를 반환합니다.
객체를 초기화하거나 값을 계산하는 데 적합합니다.

val service = MultiportService("https://example.kotlinlang.org", 80)

// 값 초기화 run
val result = service.run {
    port = 8080
    query(prepareRequest() + " to port $port")
}

// 값 초기화 let
val letResult = service.let {
    it.port = 8080
    it.query(it.prepareRequest() + " to port ${it.port}")
}

이 예제는 값을 초기화 하는 코드입니다. let 으로도 충분히 표현 가능하지만, run 을 사용한 부분이 훨씬 깔끔해 보입니다.

참고로 run 은 확장 함수가 아닌 run 단독으로 쓰일경우 함수로도 사용가능합니다. 이경우 여전히 마지막 줄의 값을 반환합니다. 주로, 계산을 특정 범위 내에서 사용하고 싶을때 사용하면 유용합니다.

val hexNumberRegex = run {
    val digits = "0-9"
    val hexDigits = "A-Fa-f"
    val sign = "+-"

    Regex("[$sign]?[$digits$hexDigits]+")
}

apply(객체 구성)

apply는 객체를 this로 참조하고 객체 자체를 반환합니다.
어떤 객체의 인스턴스를 생성과 동시에, 변수에 담기 전 초기화를 할 때 주로 사용합니다.

val user = User().apply {
    name = "Alice"
    age = 25
}

also(Side Effect 처리)

also는 객체를 it로 참조하고 객체 자체를 반환합니다. 로깅 또는 유효성 검사와 같은 Side Effect 처리을 위해 설계되었습니다.

class Book(author: Person) {
    val author = author.also {
      requireNotNull(it.age)
      print(it.name)
    }
}

with(그룹화 작업)

with는 객체를 가져와 this로 참조하고 람다의 결과를 반환합니다. 그룹화 작업에 적합합니다.

Non-nullable (Null 이 될수 없는) 수신 객체 이고 결과가 필요하지 않은 경우에만 with 를 사용합니다.

val person: Person = getPerson()
with(person) {
    print(name)
    print(age)
}

사실 이외에도 takeIf, takeUnless 등등.. 몇개가 더 있는데요. 이부분은 공식문서를 참고하시면 좋을거 같습니다 !!

⚠️ Scope 함수 사용 시 주의점

Kotlin 공식 문서에서는 이렇게 말합니다

범위 함수는 코드를 간결하게 만들지만,
가독성을 떨어뜨리거나 혼동을 줄 수 있으므로 과도한 사용은 피해야 한다.

특히 여러 Scope 함수를 중첩하거나 연쇄 호출(chain) 하는 경우
컨텍스트 객체를 헷갈리기 쉬우니 주의하라.

기본적으로 중첩해서 사용하는 경우는 가독성을 해치기에 지양하라고 이야기 하고 있습니다.

특히 this 의 경우, (수신)객체가 암시적으로 전달되기에, 이를 사용하는 apply, run, with 는 중첩해서는 안됩니다.

alsolet 을 중첩할 경우에는 it 를 사용해서는 안됩니다. 대신 명식적인 이름을 제공해 코드상 이름이 혼동되지 않게 합니다.

바른 예)

val greeting = userRepository.getUser()
    ?.also { println("데이터 로드 성공: ${it.name}") }
    ?.let { user -> "안녕하세요, ${user.name}님!" }
    ?: "사용자 정보를 불러올 수 없습니다."

다음과 같이 각각의 스코프 함수의 특성에 맞게 적절히 체이닝 하는 것은 좋아보입니다.

결론

스코프 함수를 공부하다 보면 “굳이 이렇게까지 사용할 필요가 있을까?”라는 의문이 들 수 있습니다.

하지만 다양한 스코프 함수의 의도를 이해해두면 다른 사람이 작성한 코드를 읽는 데 큰 도움이 되고 상황에 맞게 적절히 사용했을 때 훨씬 간결하고 코틀린스러운 코드를 작성할 수 있습니다.

공식 문서에서도 다음과 같이 말합니다.

서로 다른 스코프 함수는 사용 사례가 겹칠 수 있으며, 팀이나 프로젝트에서 정한 규칙에 따라 적절한 함수를 선택해 사용할 수 있습니다.

즉, 스코프 함수는 “무조건 이렇게 써야 한다”는 정답이 있는 것이 아니라,
어떤 목적을 가지고 이 스코프 함수를 선택했는지 명확하다면 팀 내 컨벤션에 따라 통일감 있게 사용하는 것이 가장 중요합니다.

올바른 사용 목적을 기반으로 일관성 있게 적용한다면 스코프 함수는 분명 코드 품질을 높이는 데 기여할 수 있습니다.

📕 참고 자료

다음의 링크를 참고했습니다.

Scope functions 코틀린 공식문서
Extensions 코틀린 공식문서
고차함수와 람다 코틀린 공식문서

profile
Seungjun Gong

0개의 댓글