Effective Kotlin #2 가독성

yeji·2022년 11월 11일
0

Effective Kotlin

목록 보기
2/7

01부 2장. 가독성

코틀린은 간결성을 목표로 설계된 프로그래밍 언어가 아니라, 가독성(readability)을 좋게 하는 데 목표를 둔 프로그래밍 언어다.

item 11. 가독성을 목표로 설계하라

개발자가 코드를 작성하는 데는 1분이 걸리지만, 이를 읽는 데는 10분이 걸린다.

  • 프로그래밍은 쓰기보다 읽기가 중요하다.
    • 코드를 작성하다가 오류가 생기면 작성하는 시간보다 더 오랜 시간을 보아야 한다.

인식 부하 감소

  • 가독성이란 코드를 읽고 얼마나 빠르게 이해할 수 있는지를 의미
  • 이해하기 쉬운지는 읽는 사람이 얼마나 많은 관용구(구조, 함수, 패턴)에 익숙한지에 따라 달라진다.
    • 사용빈도가 적은 관용구는 코드를 복잡하게 만든다. 그리고 그런 관용구들을 한 문장 내부에 조합해서 사용하면 복잡성은 훨씬 더 빠르게 증가한다.

(참고)
let

  • 수신 객체를 이용해 작업한 후 마지막 줄을 return
  • null check 후 코드를 실행할 때 사용
  • nullable한 수신객체를 다른 타입의 변수로 변환해야 하는 경우 사용
public inline fun <T, R> T.let(block: (T) -> R): R
  • let 사용한 경우 return 되는 것이 null이면 run밑으로 빠진다
    run
  • 수신 객체를 return하지 않고, run 블록의 마지막 라인을 return하는 범위 지정 함수
  • 수신 객체에 대해 특정한 동작을 수행한 후 결과 값을 return받아야 하는 경우 사용
public inline fun <T, R> T.run(block: T.() -> R): R

정리

  • 인지부하를 줄이는 방향으로 작성해라
  • 짧은 코드보다 익숙한 코드를 더 빠르게 읽을 수 있다.

극단적이 되지 않기

정리

  • 관용구를 이해하는데 비용이 발생하더라도, 그만한 가치가 있다면 사용해도 좋다.
  • 문제가 되는 경우는 그만한 가치가 없을 때(=이유없이 복잡성을 추가하는 경우)이다.

let 으로 인해 예상치 못한 결과가 나올 수 있다고 했다.
그렇다고 let 을 쓰지말고, 무조건 if-else를 쓰는게 좋다고 이해하면 안된다.
-> 극단적이 되지 말자. let은 좋은 코드를 만들기 위해 널리/다양하게 활용되는 관용구이다.

  • nullable 안전 호출
// 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) // print 를 뒤로 이동시킨 경우
  • 데코레이터를 사용해서 객체를 wrap할 때
var obj = FileInputStream("/file.gz")
	.let(::BufferedInputStream)
	.let(::ZipInputStream)
	.let(::ObjectInputStream)
	.readObject() as SomeObject

컨벤션

사람에 따라 가독성에 대한 관점이 다르다.

  • 함수 이름을 어떻게 지어야 하는지
  • 어떤 것이 명시적이어야하는지, 어떤 것이 암묵적이어야 하는지
  • 어떤 관용구를 사용해야 하는지
operator fun String.invoke(f: ()->String): String = this + f()

infix fun String.and(s: String) = this + s

위의 코드는 아래와 같은 컨벤션 규칙들을 위반한다.

  • 연산자는 의미에 맞게 사용해야한다.
  • 람다를 마지막 인자로 사용한다 라는 컨벤션을 적용하면 코드가 복잡해진다.
  • 함수 이름을 보고 동작을 유추할 수 있어야한다. and를 다른 의미로 사용하고있다.
  • 이미 있는 것을 다시 만들 필요는 없다 문자열 결합은 이미 코틀린에 내장된 함수가 있다.

item 12. 연산자 오버로드를 할 때에는 의미에 맞게 사용하라

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

fun Iterable<Int>.product(): Int = 
		fold(1) { acc, i -> acc * i }
        
---
print(10 * 6.facorial()) // 10 * (6!) = 7200

// 연산자 오버로딩
opertaor fun Int.not() = factorial()
print(10 * !6) // 7200

print(10 * 6.not()) // 7200

연산자 오버로딩을 이용하면 실제 팩토리얼과 비슷하게 ! 기호를 이용해서 표현이 가능하다.
하지만 함수의 이름이 not 이므로 논리 연산에 맞게 사용해야지, 팩토리얼 연산에 사용하면 안된다.

연산자 오버로딩이라는 자유는 많은 개발자가 해당 기능을 오용하게 만든다.

코틀린의 표준 함수인데도 잘못 이해해서 사용하기 쉽다. 개발자가 연산자 오버로딩을 사용할 때에는 항상 신중해야한다.

일반적인 의미로 사용되고 있지 않다면, 연산자를 볼 때마다 연산자를 개별적으로 이해해야 하기 때문에 코드를 이해하기 어렵다.

  • 혼란스럽고 오해의 소지가 있다.
  • 관례에 어긋난다.

분명하지 않은 경우

관례를 충족하는지 어긋나는지 확실하지 않은 경우가 문제이다.

의미가 명확하지 않다면, infix 를 활용한 확장 함수를 사용하는 것이 좋다.

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

val tripledHello = 3 timesRepeated { print("Hello") } // 2항 연산자 처럼 사용
tripleHello() // 출력 : HelloHelloHello

또는 톱레벨 함수를 사용한다.

repeat(3) { print("Hello") } // 출력: HelloHelloHello

규칙을 무시해도 되는 경우

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

  • 해당 도메인을 위해 설계된 DSL 이기 때문이다.
  • DSL 코드의 경우에는 연산자를 잘못 이해할 일이 없으므로, 연산자 오버로딩을 사용해도 큰 상관이 없다.

item 13. Unit? 을 리턴하지 말라

코틀린에서는 void 대신, Unit 과 Nothing 이라는 타입을 제공해준다.

  • Unit 은 함수가 끝났으나, 의미있는 반환값이 없는 경우 사용한다.
    • fun report() { // 아무것도 반환하지 않으면 return Unit 이 반환된다. }
  • Nothing 은 함수가 끝이 나지 않는 경우 사용한다
    - (ex throw Exception, while (true) { yield ... })
    Unit?은 Unit 또는 null이라는 값을 가질 수 있다.

Unit?도 사용하면 괜찮지 않을까? 라는 생각으로 간혹 Unit을 Boolean처럼 사용하는 경우가 있다.

  • Boolean 이 true or false 를 리턴하듯 Unit? 은 Unit or null 을 리턴하기 때문이다.
// Boolean 을 이용하는 경우
fun isCorrectKey(key: String): Boolean = ...

if (isCorrectKey(key)) return
// Unit? 을 이용하는 경우
fun verifyKey(key: String): Unit? = ...

verifyKey(key) ?: return

Unit?을 사용하면 읽는 입장에서 verifyKey(Key)가 무엇을 반환하는지 예측하기 어렵고, 오류가 생겼을 때 이유를 찾기 힘들게 만든다. Unit?을 사용하지 말자.

따라서 Boolean 을 사용하는 형태로 변경하는 것이 좋다.

기본적으로 Unit? 을 리턴하거나, 이를 기반으로 연산하는 것은 좋지 않습니다.

item 14. 변수 타입이 명확하지 않은 경우 확실하게 지정하라

코틀린은 개발자가 타입을 지정하지 않아도 타입을 지정해서 넣어주는 굉장히 수준 높은 타입 추론 시스템을 갖추고 있다.

  • 코틀린은 Python, JavaScript 처럼 런타임에 동적으로 타입 바운딩되는 스크립트 언어가 아니다. 컴파일 시점에 모든 타입은 결정된다.
  • 하지만 코틀린은 수준 높은 타입 추론 시스템을 갖추고 있어서, 타입을 지정하지 않아도 사용 가능하다.

유형이 명확할 때 코드가 짧아지므로 코드의 가독성이 크게 향상한다. 하지만 유형이 명확하지 않을 때는 남용하면 좋지 않다.

  • 코드를 읽으면서 함수 정의를 보면 확인해야 한다는 것은 가독성이 떨어진다는 뜻. 가독성 향상을 위해서 타입 지정하면 좋다.
  • 깃허브 같은 환경에서는 코드 정의로 쉽게 이동하기 어렵다.
  • 안전을 위해서도 타입을 지정하는 것이 좋다.

item 15. 리시버를 명시적으로 참조하라

item 16. 프로퍼티는 동작이 아니라 상태를 나타내야 한다

kotlin의 property

  • 사용자 정의 setter, getter를 가진다.
    • 코틀린에서는 기본적으로 필드는 캡슐화되어있기에 직접 접근할 수 있는 방법은 없다.
    • getter와 setter를 가진 프로퍼티를 통해 접근해야 한다.
    • var 키워드를 통해 생성 가능하다. (디폴트로 getter, setter 만들어짐)
    • val 키워드를 사용해서 읽기 전용 프로퍼티를 만들 경우 field가 만들어지지 않는다.
  • backing field / backing property
    : backing field의 scheme에 맞지 않는 작업을 한다면 property가 됨
  • 만약 getter, setter를 변경하고 싶다면 프로퍼티의 데이터를 저장해두는 backing field에 대한 레퍼런스인 field를 사용하면 된다.
class Person(name: String) {
    var name = name
    get() = field // field 사용
    set(value) {
        field = value
    }
}


---
var date: Date
	get() = Date(millis) // 매번 Date객체 생성해서 날짜를 저장하고
    set(value) {
    	millis = value.time // 객체는 millis 필드만 가진다.
        // 별도의 property로 옮기고 wrap/unwrap하도록 코드를 변경만 하면 다른 곳에서 이 프로퍼티를 많이 참조하고 있더라도 괜찮다.
    }
  • var를 사용해서 만든 읽고 쓸 수 있는 프로퍼티는 getter와 setter를 정의할 수 있다.
    • 이러한 프로퍼티를 파생 프로퍼티(derived property)라고 부른다.

코틀린의 프로퍼티는 필드가 아니라 접근자를 나타낸다.

  • 프로퍼티는 필드가 필요 없다. 오히려 프로퍼티는 개념적으로 접근자를 나타낸다.
    따라서 코틀린은 인터페이스에도 프로퍼티를 정의할 수 있다.
interface Person {
	val name: String // 프로퍼티 (필드 X)
}
open class Supercomputer {
	open val theAnswer: Long = 42 // getter 생성됨
}

class AppleComputer : Supercomputer() {
	override val theAnswer: Long = 1_800_275_2273 // getter를 override
}

이와 같은 이유로 프로퍼티를 위임할 수 있다. (property delegation)

// 코틀린에서 제공하는 표준 대리자 lazy() 함수에 프로퍼티를 위임합니다.
val db: Database by lazy { connectToDb() }

프로퍼티는 본질적으로 함수이므로 확장 프로퍼티를 만들 수도 있다.

프로퍼티를 함수 대신 사용할 수도 있지만, 그렇다고 완전히 대체해서 사용하는 것은 좋지 않다.
원칙적으로 프로퍼티는 상태를 나타내거나 설정하기 위한 목적으로만 사용하는 것이 좋고, 다른 로직들을 포함하지 않아야 한다.
property의 접근자 메서드에 너무 많은 책임을 할당하면 가독성이 낮아지고, 불필요한 종속성이 생기게 된다.

  • 프로퍼티에 비즈니스 로직이 포함되어있으면, 객체가 어떻게 동작할지 예측하기 어렵다.
  • get의 멱등성이 깨질 수 있습니다. 함수를 여러번 호출하게되면, 반환값이 달라질 수 있다.
  • 타입 변환의 경우에도, Int.toDouble() 같은 함수를 사용하지, 프로퍼티에서 변환을 처리할거라 예상하기 어렵다.
  • 보통 프로퍼티getter, setter가 연산비용이 높다고 생각하지 않습니다. 이는 개발자의 최적화 대상에서 빠지게 된다.
  • 관습적으로 getter, setter가 객체의 상태를 변화시킬거라고 생각하지 않는다.

item 17. 이름있는 아규먼트를 사용하라

코드에서 인자(Argument)의 의미가 명확하지 않은 경우가 많습니다.
아래와 같은 경우, 개발자는 이게 구분자separator인지, 접두사prefix인지 지레 짐작하게 된다.

item 18. 코딩 컨벤션을 지켜라

profile
🐥

0개의 댓글