들어가기 앞서

  • 코틀린의 핵심 철학은 '실용주의(pragmatism)'

  • 실용주의는 다음과 같은 비지니스 요구 사항을 충족한다는 것을 의미

    • 생선성(productivity)

    • 확장성(scalability)

    • 유지보수성(maintainability)

    • 신뢰성(reliability)

    • 효율성(efficiency)

  • 이 책의 목표

    코틀린을 활용해서 더 안전하고, 더 가독성이 좋고, 더 확장성이 있으며, 더 효율적인 코드를 작성하는 방법 설명

좋은 코드

안정성

1. 가변성을 제한하라

  • 코틀린의 프로퍼티는 상태(state)를 가질 수 있음.

    read-write property(var) 혹은 mutable object

  • 상태를 가지는 것

    • 프로그램 이해하기 어렵고 디버그 어려워짐

    • 코드 실행을 추론하기 어려워짐

    • 멀티스레드일 때 적절한 동기화 필요

    • 테스트 어려워짐

    • 변경을 알려야함

      (정렬된 리스트일 경우 엘리먼트 변경시 매번 재정렬 필요)

  • 가변성은 생각보다 단점이 많아 이를 제한하는 언어도 있음 → 순수 함수형 언어(Haskell, …​)

  • 일반적으로 var 보다 val 을 많이 사용함

  • 코틀린은 가변성을 제한할 수 있게 설계되어 있음

읽기 전용 프로퍼티(val)
  • val 로 선언된 프로퍼티는 마치 값(value)처럼 동작

  • 읽기 전용 프로퍼티가 완전히 변경 불가능한 것은 아니라는 것을 주의

    val 은 읽기 전용 프로퍼티지만, 변경할 수 없음(불변, immutable)을 의미하는 것이 아니라는 것을 기억해야 함

  • 읽기 전용 프로퍼티가 mutable 객체를 담고 있다면, 내부적으로 변경 가능

    '프로퍼티를 읽을 수만 있다는 것(읽기 전용) vs. 값이 변할 수 없는 것(가변성)' 구분할 줄 알아야 함

  • 읽기 전용 프로퍼티는 다른 프로퍼티를 활용하는 사용자 정의 게터로도 정의 가능

    var id = "1"
    var name = "jun"
    val key
        get() = "$id:$name" (1)
    
    fun main() {
        println(key) // 1:jun
        id = 2
        println(key) // 2:jun (2)
    }
    1. 사용자 정의 게터

    2. val 임에도 불구하고 값이 변경됨

  • 코틀린의 프로퍼티는 기본적으로 캡슐화 되어 있고, 사용자 정의 접근자(getter, setter) 가질 수 있음 = 유연함 제공

    (아이템 16 참고)

  • valvar 로 오버라이드 가능

    interface Element {
        val active: Boolean
    }
    
    class ActualElement: Element {
        override var active: Boolean = false
    }
    
    fun main() {
        with(ActualElement()) {
            println(active) // false
            active = true
            println(active) // true
        }
    }
가변 컬렉션과 읽기 전용 컬렉션 구분
  • 읽기 전용 인터페이스: Iterable, Collection, Set, List

  • 읽고 쓸 수 있는 인터페이스: MutableIterable, MutableCollection, MutableSet, MutableList

  • 읽기 전용 컬렉션을 mutable 컬렉션으로 다운캐스팅하면 안됨

    (해당 인터페이스의 계약을 위반하고, 추상화를 무시하는 행위. 안전하지 못하고, 예측하지 못한 결과를 초래함)

    val list = listOf(1, 2, 3)
    
    // 하지 말아야 할 행위. 다운캐스팅
    if (list is MutableList) {
        list.add(4)
    }
  • 복제(copy)를 통해 새로운 mutable 컬렉션을 만드는 list.toMutableList 를 활용해야 함

데이터 클래스의 copy
  • data 한정자는 copy 라는 이름의 메서드를 만들어 줌

  • String 이나 Int 처음 내부적인 상태를 변경하지 않는 immutable 객체를 많이 사용하는 데에는 이유가 있음

    • 변경되지 않으므로(상태가 유지되므로) 코드 이해하기 쉬움

    • 공유시 충돌 X, 안전하게 병렬 처리

    • 쉽게 캐싱할 수 있음

    • 방어적 복사본(defensive copy)을 만들 필요가 없음. 또한 깊은 복사하지 않아도 됨

    • 실행을 쉽게 예측할 수 있음

    • 맵, 셋의 키로도 사용 가능

2. 변수의 스코프를 최소화하라

  • 상태를 정의할 떄 변수나 프로퍼티의 스코프를 최소화하라

  • 프로그램을 추적하고 관리하기 쉬워짐

  • 다른 개발자에 의해 잘못 사용되는 것을 막음

  • 코틀린의 if, when, try-catch, Elvis 표현식 활용 가능

  • 구조분해 선언(destructuring declaration) 활용 가능

  • 람다에서 변수를 캡쳐하는 것 주의

3. 최대한 플랫폼 타입을 사용하지 말라

  • 플랫폼 타입(platform type): 다른 프로그래밍 언어에서 전달되어서 nullable인지 아닌지 알 수 없는 타입

    • String! 과 같이 타입 뒤에 ! 기호를 붙혀서 표기

  • 자바와 코틀린을 함께 사용할 때, 자바 코드를 직접 조작할 수 있다면 가능한 @Nullable, @NotNull 어노테이션을 붙여서 사용

    • JetBrains, JSR-305, ReactiveX, Lombok 등에 관련 어노테이션이 존재함

  • 플랫폼 타입이 전파되는 일은 위험

    • 항상 위험을 내포, 안전한 코드를 원한다면 이런 코드를 제거

public class JavaClass {
    public String getValue() { return null; }
}

fun statedType() {
    val value: String = JavaClass().value // NPE
    println(value.length)
}

fun platformType() {
    val value = JavaClass().value
    println(value.length) // NPE
}

The Pragmatic Programmer [pp] should be required reading for all developers. To learn all about design patterns, refer to the book by the “Gang of Four” [gang].

4. inferred 타입으로 리턴하지 말라

  • 코틀린의 타입 추론(type inference)은 JVM 세계제어 가장 널리 알려진 코틀린의 특징

  • 할당 때 inferred 타입은 정확하게 오른쪽에 있는 피 연산자에 맞게 설정됨. 절대 슈퍼클래스 or 인터페이스로 설정되지 않음

    open class Animal
    class Zebra: Animal()
    
    fun main() {
        var animal = Zebra() // (1)
        animal = Animal() // 오류: Type mismatch
    }
    1. 타입을 선언하지 않고 추론을 통해 타입 지정

  • 타입을 확실하게 지정해야 하는 경우 명시적으로 타입을 지정.

  • 타입은 굉장히 중요한 정보이므로, 숨기지 않는 것이 좋음

5. 예외를 활용해 코드에 제한을 걸어라

  • 다양한 방법으로 코드에 제한을 걸 수 있음

    • require 블록: 인자를 제한할 수 있음

      • require 함수는 조건을 만족하지 못할 때 무조건 IllegalArgumentException 을 throw 함

    • check 블록: 상태와 관련된 동작을 제한할 수 있음

      • 이런 경우에 check 함수를 하용함

        • 어떤 객체가 미리 초기화되어 있어야만 처리를 하게 하고 싶은 함수

        • 사용자가 로그인 했을 때만 처리하게 하고 싶은 함수

        • 객체를 사용할 수 있는 시점에 사용하고 싶은 함수

      • IllegalStateException 을 throw함

      • 일반적으로 require 블록 뒤에 배치

    • assert 블록: 어떤 것이 true인지 확인할 수 있음. assert 블록은 테스트 모드에서만 작동.

    • return or throw 와 함께 활용하는 Elvis 연산자

  • 제한을 걸면 문서를 읽지 않은 개발자도 문제를 확인할 수 있음

  • 코드가 어느 정도 자체적으로 검사됨

  • 스마트 캐스트 기능 활용

6. 사용자 정의 오류보다는 표준 오류를 사용하라

  • require, check, assert 함수를 사용하면 대부분의 코틀린 오류를 처리할 수 있음

  • 최대한 표준 라이브러리의 오류를 사용하는 것이 좋음

    • 많은 개발자가 알고 있으므로 이를 재사용

  • 나타내기 위한 적절한 오류가 없을 경우 사용자 정의 오류 사용

    • but, 설정보다는 관례

  • "규칙 60. 표준 예외를 사용하라" - 이펙티브 자바 2판

7. 결과 부족이 발생할 경우 null과 Failer를 사용하라

코드 설계

재사용성

8. knowledge를 반복하여 사용하지 말라

Appendix A: test

Glossary

mud

wet, cold dirt

rain

water falling from the sky

References

  • [pp] Andy Hunt & Dave Thomas. The Pragmatic Programmer: From Journeyman to Master. Addison-Wesley. 1999.

  • [gang] Erich Gamma, Richard Helm, Ralph Johnson & John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. 1994.