이펙티브 코틀린 Item 11: 가독성을 목표로 설계하라

woga·2023년 4월 9일
0

코틀린 공부

목록 보기
14/54
post-thumbnail

개발자들은 코드 작성 1분 읽기에 10분이 걸린다는 말이 있다. 그만큼 읽기가 중요하다.
물론 가독성은 사람마다 다르게 느껴질텐데, 일반적으로 사람의 '경험'과 '인식에 대한 과학'으로 만들어진 규칙이 있다.

인식 부하 감소

  • 구현 A
if (person != null && person.isAdult) {
	view.showPerson(person)
} else {
	view.showError()
}
  • 구현 B
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) 편리한 코드를 쓰자 -> 그럼 코틀린이 제공하는 편리한 함수는 언제 쓰는가? 때에 따라 쓸수 있어야하는데 익숙하지 않다고 계속 쓰지 않는게 맞는것인가?

이런 두 가지를 고민을 번갈아하면서 코드의 복잡성과 미칠 영향까지 고려하여 짜게 된다.
우리의 아키텍처와 서비스의 피쳐에서 어떨지도 고민하며 개발을 하는 것.. 그게 바로 소프웨어 엔지니어의 덕목 중 하나가 아닐까

profile
와니와니와니와니 당근당근

0개의 댓글