개발자는 어떤 코드를 작성하는 것보다 읽는 데 많은 시간을 소모합니다. 따라서 항상 가독성을 생각하면서 코드를 작성해야 합니다.
인식 부하 감소
가독성은 사람에 따라 다르게 느낄 수 있습니다. 하지만 일반적으로 많은 사람의 '경험'과 '인식'에 대한 과학으로 만들어진 어느 정도의 규칙이 있습니다.
// 구현 A
if (person != null && person.isAdult) {
view.showPerson(person)
} else {
view.showError()
}
// 구현 B
person?.takeIf { it.isAdult }
?.let { view::showPerson }
?: view.showError()
가독성이란 코드를 일고 얼마나 빠르게 이해할 수 있는지를 의미합니다. 이는 우리의 뇌가 얼마나 많은 관용구(구조, 함수, 패턴)에 익숙해져 있는지에 따라서 다릅니다. Kotlin 초보자에게 구현 A가 더 읽고 이해하기 쉽습니다. 일반적인 관용구(is/else, &&, 메서드 호출)를 사용했기 때문입니다.
구현 B는 Kotlin에서는 꽤 일반적인 관용구이므로, 경험이 많은 Kotlin 개발자라면 일반적으로 사용되는 관용구이므로, 쉽게 읽을 수 있을 것입니다. 하지만 숙련된 개발자만을 위한 코드는 좋은 코드가 아닙니다.
따라서 구현A가 훨씬 가독성이 좋은 코드입니다.
또한 구현 A는 수정하기 쉽습니다. if블록에 작업을 추가해야 한다고 생각하면, 쉽게 추가할 수 있을 것입니다.
하지만 구현 B는 더 이상 함수 참조를 사용할 수 없으므로, 코드를 수정해야 합니다. 그리고 else 블록 쪽을 수정하려면 함수를 추가로 사용해야 합니다.
// 구현 A
if (person != null && person.isAdult) {
view.showPerson(person)
view.hideProgressWithSuccess()
} else {
view.showError()
view.hideProgress()
}
// 구현 B
person?.takeIf { it.isAdult }
?.let {
view::showPerson(it)
view.hideProgressWithSuccess()
} ?: run {
view.showError()
view.hideProgress()
}
구현 A는 디버깅도 더 간단합니다. 왜냐하면 일반적으로 디버깅 도구조차 이러한 기본 구조를 더 잘 분석해 주기 때문입니다.
이처럼 일반적이지 않고 '굉장히 창의적인' 구조는 유연하지 않고, 지원도 제대로 받지 못합니다.
참고로 위의 구현은 실행결과가 다릅니다. 왜냐하면 let은 람다식의 결과를 리턴합니다. 즉 showPerson이 null을 리턴하면, 두 번째 구현 때는 showError도 호출하게 됩니다.
정리하면 기본적으로 '인지 부하'를 줄이는 방향으로 코드를 작성하세요. 우리의 뇌는 패턴을 인식하고, 패턴을 기반으로 프로그램의 작동 방식을 이해합니다.
극단적이 되지 않기
위에서 let으로 인해서 예상힞 못한 결과가 나올 수 있다고 했습니다. 이 이야기를 "let은 절대로 쓰면 안된다"로 이해하는 사람들이 꽤 많습니다. 하지만 let은 좋은 코드를 만들기 위해서 다양하게 활용되는 관용구입니다.
예를 들어 nullable 가변 프로퍼티가 있고, null이 아닐 때만 어떤 작업을 수행해야 하는 경우가 있다고 합시다. 가변 프로퍼티는 스레드와 관련된 문제를 발생시킬 수 있으므로, 스마트 캐스팅이 불가능합니다. 여러 방법이 있지만 일반적으로 아래와 같이 안전 호출 let을 사용합니다.
class Person(val name: String)
var person: Person? = null
fun printName() {
person?.let {
print(it.name)
}
}
이외에도 다음과 같은 경우에 let을 많이 사용합니다.
- 연산을 Argument처리 후로 이동 시킬 때
- 데코렝터를 사용해서 객체를 Wrap할 때
students
.firter { 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
이 코드들은 디버그하기 어렵고, 경험이 적은 Kotlin 개발자는 이해하기 어렵습니다. 따라서 비용이 발생합니다. 하지만 이 비용은 지불할 만한 가치가 있으므로 괜찮습니다.
컨벤션
많은 개발에서 함수 이름을 어떻게 지어야 하는지, 어떤 것이 명시적이어야 하는지, 어떤 것이 암묵적이어야 하는지, 어떤 관용구를 사용해야 하는지 등으로 토론합니다.
operator fun String.invoke(f: ()->String): String = this + f()
infix fun String.and(s: String) = this + s
// 필자가 생각하는 최악의 코드
val abc = "A" { "B" } and "C"
print(abc) // ABC
위의 코드는 아래의 수많은 규칙들을 위반합니다.
- 연산자는 의미에 맞게 사용해야 합니다. invoke를 이러한 형태로 사용하면 안 됩니다.
- "람다를 마지막 아규먼트르로 사용한다"라는 커벤션을 여기에 적용하면, 코드가 복잡해집니다.
invoke 연산자와 함께 이러한 컨벤션을 적용하는 것은 신중해야 합니다. - 현재 코드에서 and라는 함수 이름이 실제 함수 내부에서 이루어지는 처리와 맞지 않습니다.
- 문자열을 결함하는 기능은 이미 언어에 내장되어 있습니다.
이미 있는 것을 다시 만들 필요는 없습니다.
'Kotlin' 카테고리의 다른 글
[Effective Kotlin] 13 - Unit?을 리턴하지 말라 (0) | 2023.04.22 |
---|---|
[Effective Kotlin] 12 - 연산자 오버로드를 할 때는 의미에 맞게 사용하라 (0) | 2023.04.15 |
[Effective Kotlin] 10 - 단위 테스트를 만들어라 (0) | 2023.04.02 |
[Effective Kotlin] 9 - use를 사용하여 리소스를 닫아라 (0) | 2023.03.26 |
[Effective Kotlin] 8 - 적절하게 null을 처리하라 (1) | 2023.03.19 |