시배's Android

Effective Kotlin | 3장. 재사용성 본문

Book/Effective Kotlin

Effective Kotlin | 3장. 재사용성

si8ae 2024. 2. 21. 23:15

아이템19. knowledge를 반복하여 사용하지 말라

  • knowledge는 넓은 의미로 의도적인 정보를 뜻합니다.
  • 프로젝트를 진행할 때 정의한 모든 것이 knowledge입니다.
  • 알고리즘의 작동 방식, UI의 형태, 우리가 원하는 결과 등이 모두 의도적인 정보이며, knowledge입니다.
  • 로직 : 프로그램이 어떠한 식으로 동작하는지와 프로그램이 어떻게 보이는지
  • 공통 알고리즘 : 원하는 동작을 하기 위한 알고리즘
  • 둘의 가장 큰 차이점은 시간에 따른 변화입니다.
  • 비즈니스 로직은 시간이 지나면서 계속해서 변하지만 콩통 알고리즘은 한 번 정의된 이후에는 크게 변하지 않습니다.
  • knowledge도 계속해서 변한다.
  • 회사가 사용자의 요구 또는 습관을 더 많이 알게 되었다.
  • 디자인 표준이 변화했다.
  • 플랫폼, 라이브러리, 도구 등이 변화해서 이에 대응해야 한다.
  • knowledge 반복은 프로젝트의 확장성을 막고, 쉽게 깨지게 만듭니다.

언제 코드를 반복해도 될까?

독립적인 2개의 안드로이드 애플리케이션을 만들고 있다고 하자. 빌드 도구 설정이 비슷할 것이므로, 이를 추출해서 knowledge 반복을 줄일 수 있다고 생각할 수 있다. 하지만 두 애플리케이션은 독립적이므로 구성 변경이 일부 필요할 수도 있습니다. 한 애플리케이션 쪽의 구성만 변경해야 한다면 문제가 됩니다. 이처럼 신중하지 못한 추출은 변경을 더 어렵게 만들어 버린다.

  • 다른 knowledge를 나타내는지 함께 변경될 가능성이 높은가? 따로 변경될 가능성이 높은가? 질문으로 결정하자.

단일 책임 원칙

  • 클래스를 변경하는 이유는 단 한 가지여야 한다.
  • 서로 다른 곳에서 사용하는 knowledge는 독립적으로 변경할 가능성이 많습니다. 따라서 비슷한 처리를 하더라도, 완전히 다른 knowledge로 취급하는 것이 좋습니다.
  • 다른 knowledge는 분리해 두는 것이 좋습니다.

아이템20. 일반적인 알고리즘을 반복해서 구현하지 말라

이미 있는 것을 활용하자.

  • 코드 작성 속도가 빨라진다.
  • 구현을 따로 읽지 않아도, 함수의 이름 등만 보고도 무엇을 하는지 확실하게 알 수 있습니다.
  • 직접 구현할 때 발생할 수 있는 실수를 줄일 수 있습니다.
  • 제작자들이 한 번만 최적화하면, 이러한 함수를 활용하는 모든 곳이 최적화의 혜택을 받을 수 있습니다.

많이 사용되는 알고리즘을 추출하는 방법으로는 톱레벨 함수, 프로퍼티 위임, 클래스 등이 있습니다.
확장 함수는 이러한 방법들과 비교해서, 다음과 같은 장점을 갖고 있습니다.

  • 함수는 상태를 유지하지 않으므로, 행위를 나타내기 좋습니다.
  • 톱 레벨 함수와 비교해서, 확장 함수는 구체적인 타입이 있는 객체에만 사용을 제한할 수 있으므로 좋습니다.
  • 수정할 객체를 아규먼트로 전달받아 사용하는 것보다는 확장 리시버로 사용하는 것이 가독성 측면에서 좋습니다.
  • 확장 함수는 객체에 정의한 함수보다 객체를 사용할 때, 자동 완성 기능 등으로 제안이 이루어지므로 쉽게 찾을 수 있습니다.

아이템21. 일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들어라

  • 프로퍼티 위임을 사용하면 일반적인 프로퍼티의 행위를 추출해서 재사용할 수 있습니다.
  • lzy 프로퍼티는 이후에 처음 사용하는 요청이 들어올 때 초기화 되는 프로퍼티를 의미합니다.
  • 프로퍼티 위임을 사용하면, 이외에도 변화가 있을 때 이를 감지하는 observable 패턴을 쉽게 만들 수 있습니다.
  • 코틀린은 프로퍼티 위임을 사용해서 간단하고 type-safe하게 구현할 수 있습니다.
  • 프로퍼티 위임은 다른 객체의 메서드를 활용해서 프로퍼티의 접근자를 만드는 방식입니다.
  • 게터는 getValue, 세터는 setValue 함수를 사용해서 만들어야 합니다.
var token : String? by LoggingProperty(null)
var attemts : Int by LoggingProperty(0)

private class LoggingProperty<T> (var value : T) {
    operator fun getValue(
        thisRef : Any?,
        prop : KProperty<*>
    ) : T {
        print("${prop.name} returned value $value")
        return value
    }

    operator fun setValue(
        thisRef : Any?,
        prop : KProperty<*>
        newValue : T
    ) {
        val name = prop.name
        print("$name changed from $value to $newValue")
        value = newValue
    }
}
  • 컨텍스트와 프로퍼티 레퍼런스의 경계도 함께 사용하는 영태로 바뀝니다.
  • 프로퍼티에 대한 레퍼런스는 이름 어노테이션과 관련된 정보 등을 얻을 때 사용됩니다.
  • 컨텍스트는 함수가 어떤 위치에서 사용되는지와 관련된 정보를 제공해 줍니다.

코틀린에서 자주 사용되는 프로퍼티 델리게이터

  • lazy
  • Delegates.observable
  • Delegates.vetoable
  • Delegates.notNull

아이템22. 일반적인 알고리즘을 구현할 때 제네릭을 사용하라

  • 타입 아규먼트를 사용하는 함수를 제네릭 함수라고 부릅니다.
  • 타입 파라미터는 컴파일러에 타입과 관련된 정보를 제공하여 컴파일러가 타입을 조금이라도 더 정확하게 추측할 수 있게 해 줍니다.
  • 컴파일 과정에서 최종적으로 이러한 타입 정보는 사라지지만, 개발 중에는 특정 타입을 사용하게 강제할 수 있습니다.
  • 타입 파라미터의 중요한 기능 중 하나는 구체적인 타입의 서브타입만 사용할 수 있게 타입을 제한하는 것입니다.
fun < T : Comparable<T>> Iterable<T>.sorted() : List<T> {

}

fun <T, C : MutableCollection<in T>> Iterable<T>.toCollection(destination : C) : C {

}

타입에 제한이 걸리므로 내부에서 해당 타입이 제공하는 메서드를 사용할 수 있습니다.

아이템23 타입 파라미터의 섀도잉을 피하라

  • 프로퍼티와 파라미터가 같은 이름을 가질 수 있습니다. 이렇게 되면 지역 파라미터가 외부 스코프에 있는 프로퍼티를 가립니다. 이를 섀도잉이라고 부릅니다.
  • 섀도잉 현상은 클래스 타입 파라미터와 함수 타입 파라미터 사이에서도 발생합니다.
interface Tree
class Birch : Tree
class Spruce : Tree

class Forest {  
    fun addTree(tree : T) {

    }
}

이렇게 코드를 작성하면, Forest와 addTree의 타입 파라미터가 독립적으로 동작합니다.

아이템24. 제네릭 타입과 variance 한정자를 활용하라

  • invariant라는 것은 제네릭 타입으로 만들어지는 타입들이 서로 관련성이 없다는 의미입니다.
  • out은 타입 파라미터를 covariant(공변성)로 만듭니다. 이는 A가 B의 서브타입일 때, Cup 가 Cup의 서브타입이라는 의미 입니다.
  • in 한정자는 반대 의미입니다. contravariant(반병성)으로 만듭니다. 이는 A가 B의 서브타입일 때, Cup가 Cup의 슈퍼타입이라는 의미 입니다.
  • 코틀린 함수 타입의 모든 파라미터 타입은 contravariant입니다.
  • 또한 모든 리턴 타입은 covariant입니다.
  • 함수 타입을 사용할 때는 이처럼 자동으로 variance 한정자가 사용됩니다.

variance 한정자의 안정성

  • 자바의 배열은 covariant입니다.
  • 이는 배열을 기반으로 제네릭 연산자는 정렬 함수 등을 만들기 위해서라고 이야기합니다.
Integer[] numbers = {1,4,1,2};
Object[] objects = numbers;
objects[2] = "B";
  • numbers를 Object[]로 캐스팅해도 구조 내부에서 사용되고 있는 실질적인 타입이 바뀌는 것은 아닙니다. 따라서 이러한 배열에 String 타입의 값을 할당하면, 오류가 발생합니다.
  • 코틀린은 이러한 결함을 해결하기 위해서 Array(IntArray, CharArray) invariant로 만들었습니다.
class Box<out T> {
    var value : T? = null //오류

    fun set(value : T) { //오류
        this.value = value
    }

    fun get() : T = value ?: error("")
}
  • 코틀린은 public in 한정자 위치에 covariant 타입 파라미터가 오는 것을 금지하여 이러한 상황을 막습니다.
  • 가시성을 private로 제한하면 오류가 발생하지 않습니다. 객체 내부에서는 업캐스트 객체에 covariant를 사용할 수 없기 때문입니다.
  • covariant는 public out 한정자 위치에서도 안전하므로 따로 제한되지 않습니다.
class Box<in T>(
    val value : T
)

val garage : Box<Car> = Box(Car())
val amphibiousSpot : Bos<Amphibious> = garage
val boat : Boat = garage.value // Car를 위한 공간입니다.

val noSpot : Box<Nothing> = Box<Car>(Car())
val boat : Nothing = noSpot.value
  • kotlin은 contravariant 타입 파라미터를 public out 한정자 위치에 사용 하는 것을 금지하고 있습니다.
  • private일 때는 아무 문제가 없습니다.

아이템25. 공통 모듈을 추출해서 여러 플랫폼에서 재사용하라

  • 다른 플랫폼에 동일한 제품을 구현한다면, 재사용할 수 있는 부분이 더 많을 것입니다.
  • 특히 비즈니스 로직 부분들은 거의 동일합니다.
  • 소스코드를 공유할 수 있다면, 큰 이득이 발생할 것입니다.
  • 코틀린 멀티 플랫폼 기능을 활용하면, 로직을 한 번만 구현하고, 두 플랫폼에서 이를 재사용할 수 있습니다.
  • 공통 모듈을 만들고, 여기에 다양한 비즈니스 로직을 구현하면 됩니다.