Kotlin

Effective Kotlin - Item01

dev_roach 2022. 5. 25. 23:16
728x90

가변성을 제한해라

들어가기에 앞서

이건 완전한 번역글이 아닙니다. 원서를 읽고 느낀점을 적은 글 입니다.

본문

일단 첫장 부터 가변성을 제한하라고 적혀있다. 책에서 나온 글귀중 이런 글이 있다.

When an element holds state, the way it behaves depends not only on how you use it, but also on its history

나는 이뜻을 이렇게 해석했는데 element 가 상태를 지니고 있을때 상태를 다루는 방법은 오직 어떻게 사용하는 것에만 의존하는 것이 아니라 그것의 History(객체의 변화 상태를 보여주는 것이라고 나는 해석했다) 에도 관련있다.
그래서 아래 예시가 나오는데 같이 한번보자.

class BankAccount {

    var balance = 0.0
        private set

    fun deposit(depositAmount: Double) {
        balance ++ depositAmount
    }

    @Throws(InsufficientFunds::class)
    fun withdraw(withdrawAmount: Double) {
        if (balance < withdrawAmount) {
            throw InsufficientFunds
        }
    }
    balance -= withdrawAmount
}

val account = BankAccount()

acount.deposit(100.0)
println(account)
account.withdraw(50.0)
println(account)

저자는 상태를 지닌 객체를 다루는 것은 양날의 검이라고 한다. 한쪽면은 객체가 변화하는 과정을 나타내는 것이 가능하게 해주며 이건 매우 유용하다고 한다. 쉽게 예를 들면 우리는 account.deposit(100.0) 을 하고 난뒤에
println(account) 를 해서 아주 쉽게 객체의 값을 파악할 수 있다. 반면에 값을 저장할수 있는 객체를 관리하는 것은 매우어렵다고 한다. 저자는 아래와 같은 이유를 말했다.

  1. 많은 변경지점이 존재파는 프로그램을 디버그하거나 이해하는 것은 어렵다. 쉽게 말하면 어떻게 연관되어 있는지 파악하기도 힘들고, 변경지점이 많다면 어디서 변경하는지 추적하기도 어렵기 때문이다.
  2. 가변성은 코드를 알아보기 어렵게 만든다. -> 왜냐하면 이 값은 언제든 변경될 수 있으며, 우리가 특정시간에 확인을 해도 항상 같은 값은 아닐수도 있기 때문이다. -> 쉽게 생각해보면 멀티스레드에서 싱글톤 가변성 객체를 공유하는 경우
  3. 멀티스레드 프로그래밍 환경에서 동기화를 필요로 한다. -> 위의 멀티스레드 예시.
  4. 가변성 객체는 테스트 하기 어렵다. 우리는 객체가 가질 수 있는 모든 상태에 관해 테스트를 진행해야 하므로 테스트가 많아질 것이다.
  5. 가변성이 있는 상태라면, 우리는 종종 다른 클래스에서 이것을 notify 받아야 할때가 있을 것이다. 쉽게 예를 들면 정렬을 해야하는 list 에 원소가 추가되면 원소는 다시 재정렬을 해야 한다.

멀티 스레드 환경에서 같은 값을 수정했을때 문제가 발생하는 예시를 한번 보자.

var num = 0
for (i in 1..1000) {
    thread {
        Thread.sleep(10)
        num += 1
    }
}
Thread.sleep(5000)
println(num) // 대부분의 상황에서 1000 말고 다른 숫자가 나온다. 993, 997 둥둥

coroutines 를 사용하면 Thread 의 관여가 적으므로 더 적은 충돌이 일어날 것이지만 단지 적을뿐일것이다.

suspend fun main() {

    var num = 0

    coroutineScope {
        for (i in 1..1000) {
            launch {
                delay(10)
                num += 1
            }
        }
    }
    println(num)
}

우리가 일반적인 상황에서 구현한다면 아래와 같이 Synchronization 을 이용하여 구현하게 될것이다.

var lock = Any()
var num = 0
for (i in 1..1000) {
    thread {
        Thread.sleep(10)
        synchronized(lock) {
            num += 1
        }
    }
}
Thread.sleep(5000)
println(num) 

지금은 이렇게 간단하지만 실무에서 synchronized 를 통해 구현한다면 코드의 복잡성을 높일 수 있다.
그러니 이제 Kotlin 에서 가변성을 제한하는 법을 한번 알아보자.

Limiting Mubablity in Kotlin

코틀린은 가변성을 제한할 수 있도록 설계된 언어이다. 코틀린에서는 불변객체를 만드는 것과 상태가 불변성을 가지도록 만드는 것이 쉽다. 그 이유는 아래와 같다.
- val : 오직 읽기만 가능한 Properties
- Mutable and read-only Collection 의 분리 -> mutableListOf(), immutableListOf()
- data classes 의 copy function 지원

Read-Only Properties val

코틀린에서는 val(read-only), var(read-write) 를 선택하여 properties 의 속성 를 지정할 수 있다. read-only 의 단적인 예시를 보자.

val a = 10
a = 20 // ERROR, Only Read-Only property

read-only 속성은 getter 를 커스텀으로 write 할 수 있다. 다만 근데 이건 kotlin 자체의 delegate 설명을 보는게 좋다. 왜 getter / setter 를 backing field 로 둘 수 있는지 알 수 있다.

var fullName 
    get() = "$name $username"

사실 예전부터 왜 Kotlin 에서 자동으로 Custom Accessor 를 만들어? 라고 생각이 들긴했었는데 Effective Kotlin 에서는 이것이 기본적으로 encapsulated 되어 있고 유연함을 준다. 라고 말하는데.. 흐음 난 잘 모르겠다.
data class 를 사용할때는 정말 좋은것 같은데.. OOP 입장에서는 필요하지 않은 인터페이스를 default 로 만드는게 맞나? 라는 생각도 든다. 하지만 그래도 Java 보다는 맘에드는 언어이다. 여하튼 다시 본문으로 돌아가자.
여튼 val 은 immutable 한 속성이 아니라 단지 read-only 일 뿐이다 그렇기에 mutation point 를 제공하지 않는다는 점이다. 사실 kotlin val 이 immutable 하다고 생각이 든다면 immutable 과 read-only 의 차이를 알아야 한다.

아래의 코드를 보자.

fun main() {
    val list = mutableListOf<Int>(1,2,3)
    val copy_list = list // [1,2,3]
    list.add(4)
    println(copy_list) // [1,2,3]
}

이게 어떤 결과가 나올것 같은가? 애초에 val 은 read-only 일뿐 immutable 하지않다. 즉, 저자가 val 을 써서 얻을 수 있다는 이점은 Mutable point 를 줄일 수 있다는 점이다.
사실 이 강점은 예를들면, property 자체는 read-only 값으로 쓰여야 하는데, 누군가 read-write 로 쓸 수도 있다. 즉, 사람이 실수를 했을때 mutable point 가 증가하는 실수를 줄여줄 수 있는 것이다.

Mutable and read-only Collection 의 분리

요것 또한 아주 좋은 기능이라고 생각하는데, Data 를 읽어온뒤 변환과정을 거칠 필요가 없다면 사실 List 로 주는게 좋다.
왜냐하면 read-only Collections 은 상태를 수정하는 method 를 제공하지 않기때문이다.

inline fun <T, R> Iterable<T>.map(
    transformation: (T) -> R
): List<R> {
    val list = ArrayList<R>()
    for (elem in this) {
        list.add(transformation(elem))
    }
    return list
}

List 의 map Function 을 한번 보면 ReturnType 이 List 이다. MutableList 의 map 함수를 봐도 둘다 List 라는 Interface 를 Return 한다.
따라서 이러한 방법도 가능하다.

    var a: List<Int> = listOf<Int>(1, 2, 3)
    val c = a
    val b: MutableList<Int> = mutableListOf(4, 5, 6, 7)
    a = b.map {
        it + 1
    }
    println(a) // [5, 6, 7, 8]
    println(c) // [1, 2, 3]

저자는 아래처럼 다운 캐스팅으로 인한 잘못된 코드 또는 악용이 이뤄질 수도 있다고 한다. 실제로 아래코드는 컴파일 타임을 통과하나 Runtime 에 Unsupported Exception 이 발생한다.

    var test: List<Int> = listOf(1, 2, 3)

    if (test is MutableList<Int>) {
        test.add(3)
    }

    println(test)

만약 mutableList 로 바꾸고 싶다면, toMutableList() 를 이용하면 된다.

Copy in data classes

이건 마틴 파울러 책을 많이 읽어서 그런지 좀 익숙하다. 독자는 변경 가능점이 낮은 이유에 대해 설명을 한번 더 하는데 다 적진 않고 내가 중요하다고 생각한 것만 적겠다.

  1. Immutability 는 변렬 프로그래밍에서 공유되는 Objcet 의 충돌이 없으므로 더 쉽다. -> 이건 멀티스레드 프로그래밍을 하다보면 느끼게 될 것이라고 생각한다. 이 상황에서는 반드시 공유되는 Objcet 를 지양하거나 불변객체를 써야 한다.
  2. 우리는 불변객체를 Defensive Copy(모른다면 Effective Java 를 다시 읽고오는 것이 좋다.) 할 필요가 없다. 또한 deep copy 할 필요도 없다.
  3. immutable objcet 는 map 과 set 의 key 로도 활용이 가능하다.

만약 Immutable 한 객체의 값을 변경하고 싶다면 아래와 같이 새롭게 생성하여 Return 해 주면 된다.

class User (
    val name: String,
    var surname: String
) {
    fun withSurname(surname: String) = User(name, surname)
}

Different Kinds of muation points

val list1: MutableList<Int> = mutableListOf()
val list2: List<Int> = listOf()

list1.add(1) // list1.plusAssign(1)
list2 = list2 + 1 list2 = list2.plus(1)

위와 같은 방식으로 MutableList 와 List 모두 수정이 가능하다. 둘다 단일 변경점이 존재하지만, 첫번째 코드는 List 구현체에 코드가 존재한다. 그래서 우리는 멀티스레딩 케이스에서 안전한지 알수가 없다.
두번째 코드는 우리가 synchronizaiton 을 구현해줘야 하지만 오직 단하나의 변경점만 있기 때문에 좀 더 낫다. 음 근데 사실 이 문장 자체는 이해가 잘 안간다.. 애초에 둘다 안전하지 않아 보이고, Synchronization 을 구현해야 할거 같은데..

Do not leak mutation points

이건 예시가 매우 간단하고 이해도 잘간다.

class UserRepository(
    private val storedUsers: MutableMap<Int, String> = mutableListOf()
) {

    fun loadAl(): MutableMap<Int, String> {
        return storedUsers
    }

}

이렇게 했을때 외부에서 loadAll() 을 호출하게 되면 클래스 private 변수에 대한 값이 변경이 가능하니 copy() 메소드를 통해 넘기라는 내용이다.
뭐 책에 더 좋은 예시로 아예 1급 콜렉션으로 만들어 버리기 까지한다. (근데 다만 나는 그냥 이렇게 적겠다.)

class UserRepository(
    private val storedUsers: MutableMap<Int, String> = mutableListOf()
) {

    fun loadAl(): MutableMap<Int, String> {
        return storedUsers.copy()
    }

}

후기

영어 원서를 읽으면 좀 느린거 같지만, 그래도 읽을 만 하다. 다만 이 책을 읽으면서 느낀건 내가 영어를 잘 못읽는 건지, 아니면 이 책이 Context 를 나에게 제대로 잘 못전하는건지 의문이 들었다.
그리고 번역본도 있어서 같이 읽었는데 번역본은 내 느낌상 번역이 이상한 부분이 조금 많은 것 같다.