Effective Kotlin [2장: 가독성]

동현·2023년 7월 13일
0

Effective Kotlin

목록 보기
2/2
post-thumbnail

Item 11: 가독성을 위해 설계하라

프로그래밍의 대부분의 작업은 쓰는 것이 아니라 읽는 것이다. 그만큼 가독성을 좋게 설계하는 것은 중요하다.

인지 부하를 줄이는 방향으로 코드를 작성하라

다음 두 코드를 살펴보자.

// 코드 A
if (person != null && person.isAdult) {
	view.showPerson(person)
} else {
	view.showError()
}
// 코드 B
person?.takeIf { it.isAdult }
	?.let(view::showPerson)
    ?: view.shoeError()

얼마나 빨리 이해할 수 있는지에 따라 두 코드의 가독성은 달라진다. 숙련된 Kotlin 개발자에게는 takeIf, let, Elvis 연산자 등 kotlin 만의 관용구를 쓰는 코드 B가 더 이해하기 쉬울 수 있다. 하지만 반대로 아직 숙련되지 않은 개발자는 코드 A가 더 이해하기 쉬울 것이다. 따라서 코드 B와 같이 숙련된 개발자만을 위한 코드는 좋은 코드는 아니다.

// 코드 A
if (person != null && person.isAdult) {
	view.showPerson(person)
    view.hideProgressWithSuccees()
} else {
	view.showError()
    view.hideProgress()
}
// 코드 B
person?.takeIf { it.isAdult }
	?.let {
    	view.showPerson(it)
        view.hideProgressWithSuccess()
    } ?: run {
    	view.showError()
        view.hideProgress()
    }

추가 작업을 실행하도록 할 때 코드 A가 더 수정하기 쉽다. 여기서, (person == null && !person.isAdult) 일 때 다른 오류를 보여주도록 조건문 분기를 추가한다고 가정하면, A가 훨씬 쉬울 것이다.
뿐만 아니라, 코드 A와 코드 B의 실행 결과는 다를 수 있다. let은 람다식의 결과를 반환하기 때문에 코드 B의 view.showPerson(it)null을 반환하면 run 블록이 호출되어 다른 결과가 발생할 수 있다.
따라서, 기본적으로 인지 부하를 줄이는 방향으로 코드를 작성하는 것이 중요하다.

극단적인 방향으로 코드를 작성하지 말라

이전 예제에서 let의 오용 사례를 보여줬다고 해서 항상 사용하지 말아야 되는 것은 아니다.
Nullable한 가변 프로퍼티가 있고 그것이 null이 아닐 때만 작업을 수행하는 경우, let을 사용할 수 있다.

class Person(val name: String)
var person: Person? = null

fun printName() {
	preson?.let {
    	print(it.name)
    }
}

뿐만 아니라

students
	.filter { it.result >= 50 }
    .joinToString(separtor = "\n") {
    	"${it.name} ${it.surname}. ${it.result}"
    }
    .let(::print)
  1. 인수 계산 후에 작업을 이동시키는 경우
var obj = FileInputStream("/file.gz")
	.let(::BufferInputStream)
    .let(::ZipInputStream)
    .let(::ObjectInputStream)
    .readObject() as SomeObject
  1. 객체를 데코레이터로 래핑하기 위해 사용하는 경우

에도 let을 효율적으로 사용할 수 있다.

관습을 따르라

사람마다 가독성에 대한 관점이 다르다. 하지만, 이해하고 따라야 할 일반적인 관습은 존재한다.

관습을 무시한 최악의 코드의 예시를 살펴보자.

val abc = "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 보다 appendplus가 메소드 이름으로 더 적당하다.
  • 이미 문자열을 합치는 기능을 가지고 있기 때문에, 메소드를 재정의할 필요가 없다.

따라서, 지켜야할 일반적인 관습은 따르며 코드를 작성해야 한다.

Item 12: 연산자를 오버로딩할 때 의미에 맞게 사용하라

fun Int.factorial(): Int = (1..this).product()

fun Iterable<Int>.product(): Int = fold(1) { acc, i -> acc * i }

operator fun Int.not() = factorial()

! 연산자로 팩토리얼을 계산하기 위해 not 연산자를 오버로딩한 코드이다.

print(10 * !6)	// 7200

비록 기능은 제대로 동작할지라도, 이러한 연산자 오버로딩은 사용자로 하여금 혼란을 준다. !은 본래 팩토리얼 계산이 아닌 논리적 연산의 기능을 수행해야 한다.
사용자의 오용 가능성을 없애기 위해 연산자는 본래의 의미가 가진 기능대로 사용되어야 한다.

불분명한 케이스

operator fun Int.times(operation: () -> Unit): () -> Unit = {
	repeat(this) { operation() }
}

val tripledHello = 3 * { print("Hello") }

tipledHello()	// HelloHelloHello

다음과 같이 사용법이 모호한 경우도 있다. times 연사자를 오버로드한 경우 사용자에 따라 함수를 세 번 반복하여 호출한다고 생각할 수도 있고 아닐 수도 있다. 이처럼 의미가 명확하지 않을 때는 infix를 활용한 확장 함수를 사용하는 것이 좋다.

infix fun Int.timesRepeated(operation: () -> Unit): () -> Unit = {
	repeat(this) { operation() }
}

val tripledhello = 3 timesRepeated { print("Hello") }

tripledHello()

infix를 통해 이항 연산자처럼 사용함으로써 사용법을 좀 더 명확하게 할 수 있다.

repeat(3) { print("Hello") }

하지만, 가장 좋은 방법은 이미 정의된 top-level 함수인 repeat를 사용하는 것이다.

언제 규칙을 깨도 괜찮은가?

도메인 특화 언어(Domain Specific Language, DSL)을 설계할 때는 연산자 오버로딩 규칙을 무시해도 괜찮다.

body {
	div {
		+"Some text"
	}
}

다음은 HTML에서 String.unaryPlus의 오버로딩 규칙을 무시한 예시이다.

정리

  • 연산자 오버로딩을 사용할 때는 그 의미에 맞게 사용하자
  • 연산자의 의미가 명확하지 않다면, 연산자 오버로딩을 사용하지 않는 것이 좋다
  • 그럼에도 불구하고, 사용하고 싶다면 infix 확장 함수 또는 Top-level 함수를 사용하자

Item 13: Unit?을 반환하는 것을 피하라

Unit?은 Unit 또는 null 두 가지 값이 가능하다.

fun verifyKey(key: String): Unit? = //...

verifyKey(key) ?: return

다음 코드처럼 elvis 연산자를 이용한 활용도 가능하다.

getData()?.let{ view.showData(it) } ?: view.showError()

하지만, 이런 방식은 예상치 못한 오류를 발생시키기도 한다. 해당 코드에서 showData가 null을 반환하고 getData가 null이 아닌 값을 반환할 시, showDatashowError가 모두 호출된다.

따라서, Unit?은 가독성이 떨어지고 예상치 못한 오류를 발생시킬 수도 있기 때문에 사용하는 것을 피해야 한다.

Item 14: 타입이 명확하지 않은 경우 명시해라

Kotlin은 타입을 생략할 수 있는 훌륭한 타입 추론 시스템을 가지고 있다. 그러나, 해당 기능은 타입이 명확하지 않은 경우 사용해서는 안 된다.

val data = getSomeData()

해당 코드를 보면 getSomeData가 어떤 타입의 데이터를 반환해주는지 알 수 있다. 물론 IDE에서는 직접 함수로 이동해 확인할 수 있지만, Github와 같은 환경에서는 확인도 어려울 뿐더러 일일이 확인하는 일에는 시간이 낭비된다.

따라서, 타입을 명시하는 것은 중요하다.

profile
https://github.com/DongChyeon

0개의 댓글