개발자들은 코드 작성 1분 읽기에 10분이 걸린다는 말이 있다. 그만큼 읽기가 중요하다.
물론 가독성은 사람마다 다르게 느껴질텐데, 일반적으로 사람의 '경험'과 '인식에 대한 과학'으로 만들어진 규칙이 있다.
if (person != null && person.isAdult) {
view.showPerson(person)
} else {
view.showError()
}
person?.takeIf{ it.isAdult }
?.let(view::showPerson)
?: view.showError()
딱 봤을때 읽기 편한건 무엇이라고 보는가? A다.
가독성이란 코드를 읽고 얼마나 빠르게 이해할 수 있는지를 의미한다. 뇌가 얼마나 많은 함수, 구조, 패턴에 익숙해져 있는지에 따라 다르다.
코틀린 기본 개발자나, 숙련된 개발자 모두 A가 읽기 편하기 때문에 A의 압승이다.
B는 숙련된 개발자들만 읽을 수 있는데, 이를 위한 코드는 좋은 코드가 아니가.
물론 배우면 된다지만 이런 코드가 무엇을 하는지 이해하는 시간이 또 필요하다. 게다가 숙력된 코틀린 개발자도 이런 코드는 익숙하지 않아서 이해하는데 시간이 걸릴 것이다.
또한, A는 수정하기가 쉽다. if 블록에 작업을 수정해야 한다면 그저 함수를 수정하면 된다. 그러나 B는 구조가 유연하지 않아서 저 코드 자체를 수정해야 할수도 있다.
게다가 A는 디버깅도 간단하다. B를 만약 수정해야한다면 이 창의적인 구조를 다시 뜯어보고 사이드이펙이 없는지 신경써야한다는 점에 있어서 리소스가 벌써 많이 들거같지 않은가?
참고로 A,B는 실행 결과가 다르다.
let은 람다식의 결과를 리턴한다.
즉, showPerson()
이 null을 리턴하면 그 다음 엘비스인 showError()
를 호출한다. 익숙하지 않은 구조를 사용한다면 이렇게 잘못된 동작의 코드가 나오기도 한다.
(실제 이 이슈는 나도 입사 초반에 겪었던 이슈다. 쓸 때 편하지만 결국 잘 모르고 쓰면 버그로 돌아온다..)
기본적으로 인지 부하를 줄이는 방향으로 작성하는 것이 좋다.
이는 뇌가 프로그램의 작동 방식을 이해하는 과정을 더 짧게 만든다. 즉, 가독성이 좋다. 기본적으로 짧은 코드는 빠르게 읽는다쳐도 이해하기 쉬운건 익숙한 코드가 더 빠르다.
방금 let으로 예상치 못한 결과가 나온댔는데 그렇다고 절대로 쓰면 안된다는 것은 아니다.
일반적으로 let은 null이 아닐 때 어떤 작업을 수행할 때 잘 쓰인다. (안전 작업)
class Person(val name: String)
var person: Person? = null
fun printName() {
person?.let {
print(it.name)
}
}
이런 관용구는 잘 쓰이고 있어서 이해하기 쉽다 이 외에도 let는 아래의 케이스에서 쓰인다
print(students.filter{}.joinToString{}
students.filter{}.joinToString{}.let(::print)
var obj = FileInputStream("/file.gz")
.let(::BufferedInputStream)
.let(::ZipInputStream)
.readObject() as SomeObject
이 코드들은 디버기하기 어려워서 리소스 비용이 필요하다.
하지만 이 비용은 지불할 만한 가치가 있다
문제가 되는 경우는 비용을 지불할 만한 가치가 없는 코드에 비용을 지불하는 경우(정당한 이유 없이 복잡성을 추가할 때)이다.
물론 이것도 사람마다 생각이 달라 논란이 잇을 수 있다. 결국은 균형이 중요하다. 이를 씀으로서 어떤 복잡성을 줄이는지 파악해보자.
또한 두 구조를 조합해서 사용하면 단순하게 개별적인 복잡성의 합보다 훨씩 커진다는 것을 잊지 말자
사람에 따라서 가독성에 대한 관점이 다르다는 것을 알아봤다. 이를 이해하고 기억해야하는 몇 가지 규칙이 있다.
val abe = "A" { "B" } and "C"
print(abc) // ABC
operator fun String.invoke(f: ()->String): String = this + f()
infix fun String.and(s: String) = this + s
정말.. 왜 굳이 이렇게 쓰는건지 제일 먼저 의문이 들지 않는가?
연산자는 의미에 맞게 사용해야 한다. invoke를 이러한 형태로 사용하면 안된다.
'람다를 마지막 아규먼트로 사용한다'라는 컨벤션을 여기에 적용하면, 코드가 복잡하다. invoke 연산자와 함께 이러한 컨벤션을 적용하는 것은 신중해야 한다.
현재 코드에서 and
라는 함수 이름이 실제 함수 내부에서 이루어지는 처리와 맞지 않다
문자열을 결합하는 기능은 이미 언어에 내장되어 있다. 이미 있는 것을 다시 만들 필요없다.
물론 우리가 쓰이는 코드는 몇 주만 지나도 레거시가 되는 코드들이다. 그렇게 되면 유지보수가 중요하고 개발자의 가독성을 높이는게 가장 큰 관건이라고 생각한다.
이 가독성에 대해서는 사실 할 말이 많다. 또한 나도 가독성이 코드를 작성하는데 있어 1순위인 사람으로 항상 좋은 코드를 작성하기 위해 노력중인데 언제나 고민이 있다.
1) 코틀린이 제공하는 편리한 함수를 사용하자 -> 숙련된 코틀린 개발자의 함수, 이해하는데 시간이 걸리고 한 번에 이해가기 쉽지 않다
2) 편리한 코드를 쓰자 -> 그럼 코틀린이 제공하는 편리한 함수는 언제 쓰는가? 때에 따라 쓸수 있어야하는데 익숙하지 않다고 계속 쓰지 않는게 맞는것인가?
이런 두 가지를 고민을 번갈아하면서 코드의 복잡성과 미칠 영향까지 고려하여 짜게 된다.
우리의 아키텍처와 서비스의 피쳐에서 어떨지도 고민하며 개발을 하는 것.. 그게 바로 소프웨어 엔지니어의 덕목 중 하나가 아닐까