728x90

1. scope function이란?

scope function은 영역 함수라는 뜻으로 일시적인 영역을 형성하는 함수이다

람다를 사용하여 일시적인 영역 생성하여 코드를 더 간결하게 만들거나, method chaining 활용 등에 쓰인다


2. scope function 종류

  it this
람다 결과 값을 반환 let run
객체를 반환 also apply
확장 함수가 아님   with
val person = Person("홍길동", 20)
    
/* age 반환 */
val value1 = person.let { 
    it.age
}

/* age 반환 */
val value2 = person.run {
    this.age
}

/* person 객체 반환 */
val value3 = person.also {
    it.age
}

/* person 객체 반환 */
val value4 = person.apply {
    this.age
}

/* with는 확장 함수가 아님. wtth(파라미터, 람다)를 통해 호출 */
with(person) {
    println(this.name)
    println(this.age)
}

let과 run은 람다에서 결과 값인 age 값을 반환한다

(return이 명시되어있지 않으면 마지막 값이 반환된다)

반면, also와 apply는 결과 값과 상관없이 무조건 최종 값으로 객체를 반환한다

 

it과 this를 사용하는 차이점도 있다

it: 생략이 불가능한 대신 다른 이름을 붙일 수 있다

this: 생략이 가능한 대신 다른 이름을 붙일 수 없다

/* it: 생략이 불가능한 대신 다른 이름을 붙일 수 있다 */
val value5 = person.let { p ->
    p.age
}


/* this: 생략이 가능한 대신 다른 이름을 붙일 수 없다 */
val value6 = person.run {
    age
}

이런 차이가 발생하는 이유는 코틀린의 문법 때문이다

let과 also는 일반 함수를 파라미터로 받지만,

run과 apply는 확장 함수를 파라미터로 받는다는 차이가 있다

 

with는 위에 함수들과 다르게 확장 함수가 아니다

with(파라미터, 람다) 를 통해 호출이 가능하고 this를 사용한다


3. scope function 사용 예시

- let

  • 하나 이상의 함수를 call chain 결과로 호출 할 때
val strings = listOf("APPLE", "CAR")
    strings.map {it.length }
        .filter { it > 3 }
        .let { lengths -> println(lengths) }

 

  • non-null 값에 대해서만 code block을 실행시킬 때 (자주 사용됨)
val length = str?.let {
    println(it.uppercase())
    it.length
}

 

  • 일회성으로 제한된 영역에 지역 변수를 만들 때
val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first()
    .let { firstItem -> 
        if (firstItem.length >= 5) {
            firstItem
        } else {
            "!${firstItem}!"
        }
    }.uppercase()

 

- run

  • 객체 초기화와 반환 값의 계산을 동시에 해야 할 때
    ex) 객체를 만들어 DB에 바로 저장하고, 해당 인스턴스를 사용. person은 실제 저장된 person 객체를 반환함
           hobby를 변경한 후 저장하는 방식으로 응용도 가능. person은 hobby가 독서로 변경된 값을 반환함
val person = Person("홍길동", 20).run(personRepository::save)

/* 값 변경 후 저장하는 방식으로 응용 가능 */
val person = Person("홍길동", 20).run {
    this.hobby = "독서"
    personRepository.save(this)
}

 

- apply

  • 객체 설정할 때 객체를 수정하는 로직이 call chain 중간에 필요할 때
    (아래 예시 처럼은 잘 사용하지 않음. 이론상 가능)
val person = Person("홍길동", 20)
    person.apply { this.growOld() }
        .let { println(it) }

 

  • Test Fixture를 만들 때 (고정된 환경에서 반복적으로 결과 값을 확인할 수 있도록 생성하는 테스트)
    ex) Person 객체를 최초 생성된 시점에는 name, age 값만 입력 받고, 회원가입 이후에 hobby를 입력하는 로직일 경우
fun createPerson(
    name: String,
    age: Int,
    hobby: String,
) : Person {
  return Person(
      name = name,
      age = age,
  ).apply {  
      this.hobby = hobby
  }
}

 

- also

  • 객체 설정할 때 객체를 수정하는 로직이 call chain 중간에 필요할 때
mutableListOf("one", "two", "three")
        .also { println("four 추가 이전 지금 값: ${it}") }
        .add("four")

 

- with

  • 특정 객체를 다른 객체로 변환해야 하는데, 모듈 간의 의존성에 의해 정적 팩토리 혹은 toClass 함수를 만들기 어려울 때
  • 객체를 Converting 해야 하는데 한쪽에 로직을 넣기 어려울 때 사용하면 좋음
    ex) Person 객체를 PersonDto로 변환. this를 생략할 수 있어 필드가 많아도 코드가 간결해진다
return with(person) {
    PersonDto(
        name = name,
        age = age,
    )
}

4. scope function과 가독성

scope function을 사용한 코드가 그렇지 않은 코드보다 가독성이 좋은 코드일까?

 // 일반적인 코드
if (person != null && person.isAdult) {
    view.showPerson(person)
} else {
    view.showError()
}

// scope function을 활용한 코드
person?.takeIf { it.isAdult }
    ?.let(view::showPerson)
    ?: view.showError()

좋은 코드란 건 개인마다 차이가 있겠지만

scope function을 활용한 코드는 숙련된 코틀린 개발자가 아니라면 이해하기 어려울 수 있다

또한, 이러한 이유에서 일반적인 코드가 디버깅과 수정이 더 쉽다

 

위 코드에서도 let(view::showPerson)에서 null 값을 반환하게 된다면 let은 람다의 결과를 반환하기 때문에 elivs 연산자가 실행되어 view.showError()가 실행되어 showPerson과 showError를 모두 실행시켜버리는 버그가 발생할 수 있다

 

하지만 적절한 컨벤션을 적용하면 팀에서 유용하게 활용할 수 있다


https://github.com/tyakamyz/kotlin-study/tree/master/src/main/kotlin/section05/s20_%EC%BD%94%ED%8B%80%EB%A6%B0%EC%9D%98_scope_function

 

728x90
복사했습니다!