코틀린은 간결성, 짧은 코드를 목표로 설계된 프로그래밍 언어가 아니라 가독성(read-ability)를 목표로 설계된 프로그래밍 언어이다.
어떻게 하면 코틀린에서 제공하는 기능을 사용하여 깨끗하고 의미 있는 코드를 설계할 수 있을까. 그 방법에 대해 알아보자.
널리 알려진 이야기로는 개발자가 코드를 작성하는 데는 1분 걸리지만, 이를 읽는 데는 10분이 걸린다.
라는 이야기가 있다.
프로그래밍에 있어 코드를 "쓰는 것"보다 "읽는 것"이 중요하고, 이에 따라 개발자는 항상 가독성을 생각하면서 코드를 작성해야 한다.
가독성은 사람에 따라 다르게 느껴질 수 있다.
하지만 일반적으로 경험
과 인식
에 의해 만들어진 어느정도의 규칙이 있다.
다음 예를 보자.
// 구현 A
if (person != nuill && person.isAdult) {
view.showPerson(person)
} else {
view.showError()
}
// 구현 B
person?.takeIf { it.isAdult }
?.let{ view::showPerson }
?: view.showError()
r
하지만 숙련된 개발자만을 위한 코드는 좋은 코드가 아니다. A와 B를 비교한다면 A가 훨씬 가독성이 좋은 코드이다.
if 블록에 작업을 추가해야 한다고 한다면, A는 매우 수정하기 쉽다. 하지만 B는 함수 참조를 더이상 사용할 수 없으므로, 코드를 수정해야 한다.
이처럼 일반적이지 못하고, 창의적인 구조는 유연하지 않고, 지원도 제대로 받지 못한다.
기본적으로
인지 부하
를 줄이는 방향으로 코드를 작성하자. 우리의 뇌는 패턴을 인식하고, 패턴을 기반으로 프로그램의 작동 방식을 이해한다.
가독성은 뇌가 프로그램의 작동 방식
을 이해하는 과정을 더 빠르게 만든다.
코드를 짧게 만드는 것 보다, 자주 사용되는 패턴을 사용하여 익숙한 코드로 만든다면 더 빠르게 읽을 수 있다.
관용구(takeIf, 안전호출(?), let, Elvis 연산자, 제한된 함수 레퍼런스)등을 아예 쓰지 말라는 이야기가 아니다.
예를 들어 nullable 가변 프로퍼티가 있고, null이 아닐때만 어떤 작업을 수행해야 하는 경우가 있다고하자.
가변 프로퍼티는 쓰레드와 관련된 문제를 발생시킬 수 있기에 스마트 캐스팅이 불가능하다.
일반적으로는 다음과 같이 안전호출과 let을 사용한다.
var pserson: Pserson? = null
person?.let{
print(it.name)
}
이러한 관용구는 널리 사용되며, 많은 사람이 쉽게 인식한다.
또다른 예를 보자.
students
.filter { it.result >= 50 }
.joinToString(separator = "\n") {
"${it.name} ${it.surname}, ${it.result}
}
.let(::print)
var obj = FileInputStream("/file.gz")
.let(::BufferedInputStream)
.let(::ZipInputStream)
.let(::ObjectInputStream)
.readObject() as SomeObject
이러한 코드들은 디버그하기 어렵고, 경험이 적은 코틀린 개발자는 이해하기 어렵다. 따라서 비용이 발생한다. 하지만 이러한 비용이 지불할 만한 가치가 있으므로 사용해도 괜찮다.
문제가 되는 경우는 비용을 지불할 만한 가치가 없는 코드에 비용을 지불하는 경우(정당한 이유 없이복잡성을 추가할 때)이다.
어떤 것이 비용을 지불할 만한 코드인지, 아닌지 균형을 맞추어서 사용하자. 또한 두 구조를 조합해서 사용하면 단순하게 개별적인 복잡성의 합보다 훨씬 커진다는 것을 기억하자.
사람에 따라서 가독성에 대한 관점이 다르다.
하지만 회사, 팀에서 개발을하며 각 그룹의 컨벤션에 맞추어서 코딩을 진행한다. 어떤 것이 명시적이어야 하는지, 어떤 것이 암묵적이어야 하는지, 어떤 관용구를 사용해야 하는지 등을 각자 정하고 이에 따라 개발한다.
하지만 기억해야하는 몇 가지 규칙이 있다.
val abc = "A" {"B"} and "C"
print(abc) // ABC
이 코드가 가능하게 하려면 다음과 같은 코드가 필요하다.
operator fun String.invoke(f: ()->String): String
= this + f()
infix fun String.add(s: String) this + s
이 코드는 수많은 규칙을 위배한다.
람다를 마지막 아규먼트로 사용한다
라는 컨벤션을 적용하면 코드가 복잡해진다. invoke연산자와 함께 이러한 컨벤션을 적용하는 것은 신중해야 한다.