본문 바로가기

카테고리 없음

가독성을 목표로 설계하라

로버트 마틴의 클린코드라는 책을 통해 널리 알려진 이야기로 개발자가 코드를 작성하는데 1분 이를 읽는데 10분 이라는 말이 있습니다. 즞 어던 개발자도 읽는데 시간이 많이 걸린다는 소리 입니다. 프로그래밍은 쓰기 보다 읽기가 중요합니다. 따라서 항상 가독성을 생각하면서 코드를 작성해야 합니다.

 

인식 부하 감사

사실 가독성은 사람에 따라 다르게 느낄 수 있습니다. 하지만 일반적으로 많은 가람의 경험과 인식에 대한 과학으로 만들어진 어느정도의 규칙이 있습니다.

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

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

1번 과 2번중 2번은 코드가 더 짧지만 읽고 이해하기 어렵습니다.

가독성이란 코드를 읽고 얼마나 빠르게 이해할수 있는지를 의미 합니다. 이는 우리의 뇌가 얼마나 많은 관용구에 익숙해져 있는지에 따라서 다릅니다. 초보자에게는 1번이 더 이해하기 쉽습니다. 기본적인 관용구를 사용하기 때문입니다. 구현 2번 코드는 일반적으로 사용되는 관용구(안전 호출 ?, tekeIf,let,Elvis 연산자,제한된 함수 레퍼런스 view::showPerson)를 사용하고 있습니다. 숙련된 개발자라면 읽을 수 있지만 숙련된 개발자만을 위한 코드는 가독성이 좋은 코드가 아닙니다. 1번이 비교 할수 없을 정도로 가독성이 좋습니다. 신입 개발자에게 elvis 연산자를 이와 같이 쓰는것을 처음 볼수 잇고 제한된 함수 레퍼런스가 무엇인지 모를수도 있으므로 그리고 숙련된 개발자여도 내내 코틀린만 붙잡고 있는게 아니기에 이해하기 힘들수도 있습니다.

사용빈도가 낮은 관용구는 코드를 복잡하게 만듭니다. 그런 관용구를 내부에 사용한다면 코드는 복잡해 집니다. 

1번은 수정하기도 쉽습니다. if 블록에 작업을 추가한다고 했을때  1번은 추가하기 쉽지만 2번은 더 이상 함수를 참조할 수도 없으므로 코드를 수정해야 합니다. 그리고 else 블록 쪽을 수정하는 일은 어렵습니다. Elvis 연산자의 오른쪽 부분이 하나 이상의 표현식을 갖게 하려면 함수를 추가해야 합니다.

if (person != null && person.isAdult){
  view.showPerson(person)
  view.hideProgressWithSuccess()
} else {
  view.showError()
  view.hideProgress()
}

person?.takeIf {it.isAdult}
  ?.let {
    view::showPerson(it)
    view.hideProgressWithSuccess()
} ?:run {
  view.showError()
  view.hideProgress()
}

 

구현 1번은 디버깅도 더 간단합니다. 일반적으로 디버깅 도구조차 이러한 기본 구조를 더 잘 분석해 주기 때문입니다.

이처럼 일반적이지 않고 굉장히 창의적인 구조는 유연하지 않고, 지원도 제대로 받지 못합니다. 

예를 들어 여기에 person이 null인지 확인한는 코드를 넣고 null이 아닐경우에는 성인인지 아동인지에 따라서 다른 처리를 하게 하는 조건문을 추가한다고 해 봅시다.  if/else와 관련된 부분을 수정할 수 있습니다. 반면에 구현 2번을 수정하려면, 굉장한 고통이 따를 것입니다. 

 

참고로 구현 1번과 2번은 실행 결과가 다릅니다.

 

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)
  }
}

이런 광용구는 널리 사용되며, 많은 사람들이 쉽게 인식합니다. 이외에도 다음과 같은 경우 사용됩니다

  • 연산을 아규먼트 처리 후로 이동시킬 때
  • 데코레이터를 사용해서 객체를 랩할 때

두가지 예로 살펴 봅시다.

 

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)

 

이 코드가 기능하게 하려면 

operator fun String.invoke(f: ()-> String):String =
  this + f()
 
  infix fun String.and(s: String) = this + s

 

이 코드는 수많은 규칙을 위반 합니다.

  • 연산자는 의미에 맞게 사용해야 합니다. invoke를 이러한 사용하면 안됩니다.
  • 람다를 마지막 아규먼트로 사용한다 하는 컨벤션을 여기에 적용하면 코드가 복잡해 집니다. invoke 연산자와 함게 이러한 컨벤션을 적용하는 것은 신중해야 합니다.
  • 현재 코드에서 and라는 함수 이름이 실제 함수 내부에서 이루어지는 처리와 맞지 않습니다.
  • 문자열을 결합하는 기능은 이미 언어에 내장되어 있습니다. 이미 있는 것을 다시 만들 필요는 없습니다.
728x90