고딩왕 코범석

특성 본문

Language & Framework/Kotlin

특성

고딩왕 코범석 2022. 8. 5. 00:35
반응형

Index

  1. 코틀린의 이모저모
    1. Type Alias와 as import
    2. 구조분해와 componentN 함수
    3. Jump와 Label
    4. TakeIf와 TakeUnless
  2. scope function
    1. scope function이란?
    2. scope function의 분류
    3. scope function의 적절한 용례
    4. scope function과 가독성

코틀린의 이모저모

Type Alias와 as import

typealias는 긴 이름의 클래스 혹은 함수 타입이 있을 때 축약하거나 더 좋은 이름을 쓰고싶을 경우 사용한다.

typealias FruitFilter = (Fruit) -> Boolean

fun filterFruits(fruits: List<Fruit>, filter: FruitFilter) {...}

data class UltraSuperGuardianTribe(
    val name: String
)

typealias USGTMap = Map<String, UltraSuperGuardianTribe>

다른 패키지의 같은 이름 함수를 동시에 가져오고 싶다면? as import를 사용한다.

import inflearnjavatokotlin.section5.packageA.helloPrint as helloPrintA
import inflearnjavatokotlin.section5.packageB.helloPrint as helloPrintB

fun main() {
    helloPrintA()
    helloPrintB()
}

구조분해와 componentN 함수

구조분해는 복합적인 값을 분해하여 여러 변수를 한번에 초기화하는 것을 의미한다.

fun main() {
    val person = Person("고범석", 28) // Person은 data class
    val (name, age) = person
    println("$name, $age")
}

PersonData class이다. Data classcomponentN이란 함수도 자동으로 만들어주기 때문에 가능하다.

componentN이란 Data class의 필드가 N개 있을 경우 N번째에 대한 프로퍼티를 가져오도록 하는 함수이다. 즉, 프로퍼티의 순서에 의존하게 되며 val (name, age) = person을 풀면 다음과 같이 된다.

// val (name, age) = person
val name = person.component1()
val age = person.component2()

componentN 함수는 연산자 속성을 갖고 있어 일반 클래스에서 componentN 함수를 작성한다면 operator 키워드를 붙여 작성해야 한다.

class Person(
    val name: String,
    val age: Int,
) {
    operator fun component1() = this.name

    operator fun component2() = this.age
}

Jump와 Label

return, break, continue 는 다음과 같이 정의하고 있다.

  • return : 기본적으로 가장 가까운 enclosing function 또는 익명함수로 값이 반환
  • break : 가장 가까운 루프 제거
  • continue : 가장 가까운 루프를 다음 step으로 보냄

for, while문에서 break, continue의 기능은 자바와 동일하다.

단, forEach에서 break, continue를 사용할 수 없다. break, continue를 써야한다면 for문을 사용하자.

TakeIf와 TakeUnless

코틀린에서는 method chaining을 위한 함수를 제공한다.

fun getNumberIfEvenOrNullTakeIf(number: Int) =
    number.takeIf { it % 2 == 0 }

fun getNumberIfEvenOrNullTakeUnless(number: Int) =
    number.takeUnless { it % 2 != 0 }

takeIf의 코드를 살펴보자.

public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
    contract {
        callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
    }
        // 여기
    return if (predicate(this)) this else null
}

조건을 만족하면 자신을 반환하고 아니라면 null을 반환하는 것을 알 수 있다.

위로

scope function

scope function이란?

scope는 영역, function은 함수를 의미한다. scope function은 일시적인 영역을 형성하는 함수를 의미한다. scope function을 이용해 printPerson()을 리팩토링해보자.

 fun printPerson(person: Person?) {
    if (person != null) {
        println(person.name)
        println(person.age)
    }
}

// let으로 리팩토링
fun printPersonLet(person: Person?) {
    person?.let {
        println(it.name)
        println(it.age)
    }
}

정리해보면 람다를 사용해 일시적인 영역을 만들고 코드를 더 간결하게 만들거나, method chaining에 활용하는 함수를 scope function이라 한다.

scope function의 분류

코틀린의 scope function에는 다섯 가지의 종류가 있다.

  • let
  • run
  • also
  • apply
  • with (확장함수 아님)
it 사용 this 사용
람다의 결과 반환 let run
객체 그 자체를 반환 also apply
  • this : 생략이 가능한 대신, 다른 이름을 붙일 수 없다.
  • it : 생략이 불가능한 대신, 다른 이름을 붙일 수 있다.

각 scope function 별로 this, it을 사용하는데 왜 이런 차이가 발생할까?

public inline fun <T, R> T.let(block: (T) -> R): R {
    return block(this)
}

public inline fun <T, R> T.run(block: T.() -> R): R {
    return block()
}

let은 일반 함수를 받지만 run은 확장함수를 받는다. 확장함수는 본인 자신을 this를 호출하고 생략할 수 있기 때문에 이러한 차이가 발생한다.

with는 파라미터와 람다를 받는다. 또한 람다에서는 확장함수를 받기 때문에 this로 접근하고 생략 가능하다.

public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    return receiver.block()
}

scope function의 적절한 용례

let

하나 이상의 함수를 call chain 결과로 호출할 때 사용한다.

val strings = listOf("APPLE", "CAR")
strings.map{ it.length }
    .filter { it > 3 }  // 리스트로 반환
    .let(::println)  // .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()
println(modifiedFirstItem)

run

객체 초기화와 반환 값의 계산을 동시에 해야할 때 사용한다. 객체를 만들어 DB에 바로 저장하고 그 인스턴스를 활용할 때가 좋은 예다.

val person = Person("고범석", 28)
    .run(personRepository::save)

val person = Person("고범석", 28).run {
    hobby = "독서"
    personRepository.save(this)
}

자바를 사용했다 보니 repository에서 반환된 값을 사용하는 것이 익숙하다. 반복되는 생성 후처리는 생성자, 프로퍼티, init block으로 넣는 것이 유지보수에 유리하다. 단, 생성자가 너무 길어지는 경우 run을 사용하면 코드가 깔끔해진다.

apply, also

객체 설정 시 객체를 수정하는 로직이 call chain 중간에 필요할 때 사용한다.

fun createPerson(
    name: String,
    age: Int,
    hobby: String,
): Person {
    return Person(
        name = name,
        age = age,
    ).apply {
        this.hobby = hobby
    }
}
mutableListOf("one", "two")
    .also { println("추가 전: $it") }
    .add("four")

with

특정 객체를 다른 객체로 변환해야 하는데, 모듈 간의 의존성에 의해 정적 팩토리 혹은 toClass 함수를 만들기 어려울 때 사용한다.

객체를 변환하는데 한 쪽에 로직을 넣기 어려울 때 사용한다.

return with(person) {
    PersonDto(
        name = name,
        age = age,
    )
}

scope function과 가독성

두 가지의 코드를 보고 가독성을 비교해보자.

// 1번 로직
if (person != null && person.isAdult) {
    view.showPerson(person)
} else {
    view.showError()
}

// 2번 로직
person?.takeIf{ it.isAdult }
    ?.let(view::showPerson)
    ?: view.showError()

2번 로직은 코틀린 개발자만 더 알아보기 쉬울 수 있다. 1번의 경우는 특히 디버깅과 수정이 쉽다.

정리하자면 사용 빈도가 적은 관용구는 코드를 더 복잡하게 만들고 한 문장 내에서 조합해서 사용하면 복잡성이 훨씬 증가한다.

하지만 적절한 컨벤션을 적용하면 유용하게 활용할 수 있다. 즉, 팀에 대한 코틀린 숙련도, 취향 차이에 따라 달라질 수 있다.

위로

반응형

'Language & Framework > Kotlin' 카테고리의 다른 글

FP  (0) 2022.08.03
OOP  (0) 2022.08.02
코드 제어  (0) 2022.07.30
변수, 타입, 연산자  (0) 2022.07.30