시배's Android

Effective Kotlin | 2장. 가독성 본문

Book/Effective Kotlin

Effective Kotlin | 2장. 가독성

si8ae 2024. 2. 13. 22:10

아이템11. 가독성을 목표로 설계하라

// 구현 A
if (person != null && person.isAdult) {
    view.showPerson(person)
} else {
    view.showError()
}

// 구현 B
person?.takeIf { it.isAdult }
    ?.let(view::showPerson)
    ?. view.showError()
  • 가독성이란 코드를 읽고 얼마나 빠르게 이해할 수 있는지를 의미합니다.
  • 구현 A는 수정하기 쉽습니다.
  • 구현 A는 디버깅도 더 간단합니다.
  • 기본적으로 인지부하를 줄이는 방향으로 코드를 작성해야한다.

극단적이지 않기

  • let으로 인해서 예상하지 못한 결과가 나올 수 있다. (view.showPerson 에서 nullable을 리턴하는 경우)
  • let은 절대로 쓰면 안 된다를 의미하는 바는 아니다.
class Person(val name : String)
var person : Person? = null

fun printName() {  
    person?.let {  
        println(it.name)  
    }  
}
  • 연산을 아규먼트 처리 후로 이동시킬 때
  • 데코레이터를 사용해서 객체를 랩할 때
  • 아이템12. 연산자 오버로드를 할 때는 의미에 맞게 사용하라
  • 플러스 연산자가 일반적인 의미로 사용되지 않고 있다면, 연산자를 볼 때마다 연산자를 개별적으로 이해해야 하기 때문에 코드를 이해하기 어려울 것입니다.
  • 모든 연산자가 이러한 이름이 나타내는 역할을 할 거라고 기대합니다.
  • 이처럼 이름만으로 연산자의 사용이 크게 제한됩니다.
  • 의미가 명확하지 않다면, infix를 활용한 확장 함수를 사용하는 것이 좋습니다.
  • 톱레벨 함수를 사용하는 것도 좋습니다.
  • 도메인 특화 언어를 설계할 때는 오버로딩 규칙을 무시해도 되는 중요한 경우가 있습니다.
infix fun Int.timesRepeated(operation : () -> Unit) = {
    repeat(this) { operation() }
}

val tripledHello = 3 timesRepeated { print("Hello") }
tripledHello()

infix

코틀린에서 infix는 특별한 키워드로, 이를 사용하여 함수를 중위 표기법(infix notation)으로 호출할 수 있게 됩니다. 중위 표기법은 일반적으로 함수 이름이 연산자처럼 사용되는 경우에 적합합니다.

예를 들어, plus 함수는 두 개의 숫자를 더하는 함수입니다. 기본적으로 이 함수를 호출할 때는 일반적인 방식인 plus(3, 5)와 같이 호출합니다. 그러나 infix 키워드를 사용하여 중위 표기법으로 호출할 수 있습니다.

infix fun Int.add(other: Int): Int {
    return this + other
}

위의 코드에서 add 함수는 infix 키워드로 선언되어 있습니다. 따라서 다음과 같이 중위 표기법으로 호출할 수 있습니다.

val result = 3 add 5 // 이 호출은 3.add(5)와 동일합니다.

중위 표기법을 사용하면 코드가 더 읽기 쉬워질 수 있고, 특히 수학적인 연산을 다룰 때 가독성을 높일 수 있습니다. 하지만 중위 표기법은 해당 함수가 매개변수를 하나만 가지고 있을 때에만 사용할 수 있습니다.

아이템13. Unit?을 리턴하지 말라

  • 마치 Boolean이 true 또는 false를 갖는 것처럼, Unit?은 Unit 또는 null이라는 값을 갖리 수 있습니다.
  • Unit?으로 Boolean을 표현하는 것은 오해의 소지가 있으며, 예측하기 어려운 오류를 만들 수 있습니다.
getData()?.let { view.showData(it) } ?: view.showError()
  • 이 코드는 showData가 null을 리턴하고, getData가 null이 아닌 값을 리턴할 때, showData와 showError가 모두 호출됩니다.
  • 이런 코드 보다는 if-else 조건문을 사용하는 것이 훨씬 이해하기 쉽고 깔끔합니다.아이템14. 변수 타입이 명확하지 않은 경우 확실하게 지정하라
  • 가독성을 위해 코드를 설계할 때 읽는 사람에게 중요한 정보를 숨겨서는 안 됩니다.
  • 코드를 읽으면서 함수 정의를 보며 타입을 확인하면 되지 않나? 라고 생각할 수도 있지만, 이는 곧 가독성이 떨어진다는 의미입니다.
  • 코드 정의로 쉽게 이동할 수 없는 깃허브 등의 환경에서 코드를 읽을 수도 있습니다.
  • 사람도 작업할 때 활용할 수 있는 메모리는 한정적이므로, 이런 것에 쓸데없이 메모리를 낭비하는 것은 좋지 않습니다.
val data : UserData = getSomeData()

아이템14. 변수 타입이 명확하지 않은 경우 확실하게 지정하라

  • 가독성을 위해 코드를 설계할 때 읽는 사람에게 중요한 정보를 숨겨서는 안 됩니다.
  • 코드를 읽으면서 함수 정의를 보며 타입을 확인하면 되지 않나? 라고 생각할 수도 있지만, 이는 곧 가독성이 떨어진다는 의미입니다.
  • 코드 정의로 쉽게 이동할 수 없는 깃허브 등의 환경에서 코드를 읽을 수도 있습니다.
  • 사람도 작업할 때 활용할 수 있는 메모리는 한정적이므로, 이런 것에 쓸데없이 메모리를 낭비하는 것은 좋지 않습니다.
val data : UserData = getSomeData()

아이템15. 리시버를 명시적으로 참조하라

  • 스코프 내부에 둘 이상의 리시버가 있는 경우, 리시버를 명시적으로 나타내면 좋습니다.
  • 리시버가 명확하지 않다면, 명시적으로 적어서 이를 명확하게 해 주세요.
  • 레이블 없이 리시버를 사용하면, 가장 가까운 리시버를 의미합니다.
  • 외부에 있는 리시버를 사용하려면, 레이블을 사용해야 합니다.
class Node(val name : String) {

        fun makeChild(childName : String) = 
            create("$name.$childName").apply {
                print("Created ${this?.name} in "+
                    " ${this@Node.name}")
           }

        fun create(name : String) : Node? = Node(name)
 }

DSL 마커

  • 코틀린 DSL을 사용할 떄는 여러 리시버를 가진 요소들이 중첩되더라도, 리시버를 명시적으로 붙이지 않습니다.
  • DSL은 원래 그렇게 사용하도록 설계되었기 때문입니다.
  • 암묵적으로 외부 리시버를 사용하는 것을 막는 DslMarker라는 메타 어노테이션을 사용합니다.
  • DSL 마커는 가장 가까운 리시버만을 사용하게 하거나, 명시적으로 외부 리시버를 사용하지 못하게 할 때 활용할 수 있는 굉장히 중요한 메커니즘 입니다.아이템16. 프로퍼티는 동작이 아니라 상태를 나타내야 한다
  • 프로퍼티는 사용자 정의 세터와 게터를 가질 수 있습니다.
  • backing field는 프로퍼티의 데이터를 저장해 둡니다.
  • 프로퍼티는 필드가 필요 없습니다.
  • 프로퍼티는 개념적으로 접근자를 나타냅니다.
  • 따라서 코틀린은 인터페이스에도 프로퍼티를 정의할 수 있는 것입니다.
  • 프로퍼티는 본질적으로 함수이므로, 확장 프로퍼티를 만들 수도 있습니다.
  • 큰 컬렉션의 경우 답을 찾을 때 많은 계산량이 필요합니다.
  • 관습적으로 이런 게터에 그런 계산량이 필요하다고 예상하지는 않습니다. 따라서 프로퍼티가 아니라 함수로 구현해야 합니다.

아이템16. 프로퍼티는 동작이 아니라 상태를 나타내야 한다

  • 프로퍼티는 사용자 정의 세터와 게터를 가질 수 있습니다.
  • backing field는 프로퍼티의 데이터를 저장해 둡니다.
  • 프로퍼티는 필드가 필요 없습니다.
  • 프로퍼티는 개념적으로 접근자를 나타냅니다.
  • 따라서 코틀린은 인터페이스에도 프로퍼티를 정의할 수 있는 것입니다.
  • 프로퍼티는 본질적으로 함수이므로, 확장 프로퍼티를 만들 수도 있습니다.
  • 큰 컬렉션의 경우 답을 찾을 때 많은 계산량이 필요합니다.
  • 관습적으로 이런 게터에 그런 계산량이 필요하다고 예상하지는 않습니다. 따라서 프로퍼티가 아니라 함수로 구현해야 합니다.

프로퍼티 대신 함수를 사용하는 것이 좋은 경우

  • 연산 비용이 높거나, 복잡도가 O(1) 보다 큰 경우
  • 비즈니스 로직을 포함하는 경우
  • 결정적이지 않은 경우
  • 변환의 경우
  • 게터에서 프로퍼티의 상태 변경이 일어나야 하는 경우

아이템17. 이름 있는 아규먼트를 사용하라

  • 파라미터가 명확하지 않은 경우에는 이를 직접 지정해서 명확하게 만들어 줄 수 있습니다.
  • 파라미터를 사용하면 더 신뢰할 수 있습니다.
  • 변수를 사용하는 방법도 개발자의 의도를 쉽게 알 수 있지만, 실제로 코드에서 제대로 사용되고 있는지는 알 수 없습니다.
  • 변수를 잘못 만들 수도 있고, 함수 호출 때 잘못된 위치에 배치할 수도 있습니다.

이름 있는 아규먼트는 언제 사용해야 할까?

  • 이름을 기반으로 ㄱ밧이 무엇을 나타내는지 알 수 있습니다.
  • 파라미터 입력 순서와 상관 없으므로 안전합니다.
  • 디폴트 아규먼트으 ㅣ경우
  • 같은 타입의 파라미터가 많은 경우
  • 함수 타입의 파라미터가 있는 경우

아이템18. 코딩 컨벤션을 지켜라

  • 어떤 프로젝트를 접해도 쉽게 이해할 수 있습니다.
  • 다른 외부 개발자도 프로젝트의 코드를 쉽게 이해할 수 있습니다.
  • 다른 개발자도 코드의 작동 방식을 쉽게 추측할 수 있습니다.
  • 코드를 병합하고, 한 프로젝트의 코드 일부를 다른 코드로 이동하는 것이 쉽습니다.