본문 바로가기

안드로이드/Kotlin

4. Effective Kotlin - 가독성

가독성을 목표로 설계하라

인지 부하 감소

가독성은 사람에 따라 다르게 느낄 수 있다.

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


// Type:B
person?.takeIf { it.isAdult }
      ?.let(view::showPerson)
      ?: view.showError()

가독성은 뇌가 얼마나 많은 관용구에 익숙해졌냐에 따라 다르다. Type:A의 경우 kotlin을 사용해보지 않은 개발자라도 빠르게 이해할 수 있다. 하지만, Type:B의 경우 kotlin에서 일반적으로 사용되는 패턴으로 익숙하지 않아 어려울 수 있다.

그리고, 사실 Kotlin을 오래 하지 않으면 Type:B에 익숙해지기 쉽지 않다.

추가로, Type:A는 수정하기도 쉽다. if 블록에 새로운 작업이 생긴다면 하단에 한 줄 더 추가하면 된다. 하지만, Type:B는 함수에서 앨비스 연산자 끝에 추가 작업이 필요하다면 run등의 함수를 사용해아 한다. 그리고, 디버깅 시스템도 전자를 더 잘 인식한다. Type:B가 항상 나쁜 것은 아니지만 가독성을 높이기 위해선 자주 사용하는 관용구를 써야 된다.

Unit?을 리턴하지 말 것

Unit?을 리턴하게 된다면, 어떤 경우일까? 다음을 보자.

fun keyIsCorrect(key: String): Boolean = //...

if (!keyIsCorrect(key)) return

다음과 같이 사용할 수도 있다.

fun verifyKey(key: String): Unit? = //...

verifyKey(key) ?: return

이는 사용할 수도 있는 것이지만, 가독성 측면에서 최악이다.

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

타입 추론을 활용하면 개발 시간 및 효율성을 향상 시킬 수 있다. 하지만, 이전 포스팅에서 소개했듯 안정성이 떨어진다. 더불어 가독성 측면에서도 좋지 않다. 해당 변수가 어떤 타입인지 모르는 것은 개발자가 타입을 추론해야되고, 이는 코드 해석에 더 많은 시간을 쓴다는 것을 의미한다.

리시버를 명시적으로 참조하라

무언가를 더 자세하게 설명하기 위해, 명시적으로 긴 코드를 작성할 때가 있다. 대표적인 예시가 this이다. 이를 사용하면, 출처가 어느 곳인지를 명확하게 밝힐 수 있다.

class User: Person() {
  private var beersDrunk: Int = 0
  fun drinkBeers(num: Int) {
    this.beersDrunk += num
  }
}

조금 더 복잡한 예시를 보자

class Node(val name: String) {
  fun create(name: String): Node? = Node(name)
  fun makeChild(childName: String) = create("$name.$childName")
                                              .apply { print("Created ${name}") }  
}

fun main() {
  val node = Node("parent")
  node.makeChild("child")
}

위는 "Create parent.child"가 출력될 것 같이 보이지만, "Create parent'가 출력된다.

다음과 같이 개선할 수 있다.

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

fun main() {
  val node = Node("parent")
  node.makeChild("child")
}

위에서 apply 내부에서 this?를 사용한 이유는 create의 결과가 Node?이기 때문이다. 그리고, this@Node로 부모를 명시적으로 처리하고 있다. 이렇게 리시버를 명확히하면 안정성도 향상되고 가독성도 뛰어나다.

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

코틀린의 프로퍼티는 자바의 필드와 비슷해보이지만, 전혀 다른 개념이다. 같은 점은 둘 다 데이터를 저장한다는 점이다. 하지만 프로퍼티에는 더 많은 기능이 있다. 프로퍼티는 사용자 정의 getter/setter를 가질 수 있다.

var name: String? = null
    get() = field?.toUpperCase()
    set(value) {
      if (!value.isNullOrBlank()) {
        field = value;
      }
    }

프로퍼티는 위와 같이 사용할 수도 있지만, 프로퍼티 위임을 통해 확장 프로퍼티를 만들 수도 있다. 프로퍼티의 본질은 결국 함수이기 때문이다.

하지만, 원칙적으로 프로퍼티는 상태로만 활용해야 한다. 프로퍼티를 사용하는 것 대신 명시적으로 함수를 사용해야될 경우는 다음과 같다.

  • 복잡도가 O(1) 보다 크거나 연산 비용이 크다.
  • 비즈니스 로직
  • 결과값이 고정되지 않은 경우
  • 변환을 수행하는 경우
  • 상태 변화가 발생하는 경우

반대로 상태를 추출/설정할 때는 프로퍼티를 적극 활용하라. 다음과 같은 코드를 절대 작성하지 마라.

var name: String = ""

fun getName() = name;
fun setName(value: String) {
  this.name = value;
}

결국 프로퍼티는 상태를 나타내고, 함수는 행동을 나타내야 한다.

Named Argument를 사용하라.

코드에서 인자의 의미가 명확하지 않은 경우가 있다.

val text = (1..10).joinToString("|")

위에서 "|"는 무엇을 의미할까? 다음과 같이 작성해보자.

val text = (1..10).joinToString(seperator="|")

혹은 변수를 사용해도 괜찮다.

val seperator = "|"
val text = (1..10).joinToString(seperator)