반응형
Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

안드로이드 개발자 노트

[이펙티브 코틀린] Item1. 가변성을 제한하라 본문

Kotlin/이펙티브 코틀린

[이펙티브 코틀린] Item1. 가변성을 제한하라

어리둥절범고래 2023. 5. 23. 20:36
반응형

읽기, 쓰기가 가능한 프로퍼티나 mutable 객체는 상태를 가집니다.

상태를 갖게 한다는 것은 변한다는 것이고, 변하는 요소를 관리하는 것은 어렵습니다.

 

 

  • 프로그램을 이해하고 디버그하기 힘들어진다.
  • 코드의 실행을 추론하기 어려워진다.
  • 멀티스레드 프로그램일 경우 충돌이 발생할 수 있다.
  • 테스트하기 어렵다.
  • 정렬되어 있는 리스트의 경우 가변 요소를 추가한다면, 요소의 변경이 일어날 때마다 리스트 전체를 다시 정렬해야한다.

 

가변성을 제한할 수 있는 방법은 immutable 객체를 만들거나, 프로퍼티를 변경할 수 없게(read only) 막는 것이 가장 쉽습니다.

이 중에서 많이 사용되고 중요한 것들을 정리해 보면 다음과 같습니다.

 

 

  1. 읽기 전용 프로퍼티(val)
  2. 가변 컬렉션과 읽기 전용 컬렉션 구분하기(immutable)
  3. 데이터 클래스의 copy

 


읽기 전용 프로퍼티(val)

 

val은 마치 값(value)처럼 동작하며, 일반적인 방법으로는 값이 변하지 않습니다.

val을 사용한 프로퍼티는 읽기 전용이지만 아래 두 가지 경우에는 내부적으로 변할 수 있습니다.

 

mutable 객체를 담고 있는 경우

val list = mutableListOf(1,2,3)
list.add(4)

print(list) // [1, 2, 3, 4]

 

사용자 정의 접근자로 var 프로퍼티를 사용하는 val 프로퍼티의 경우

var name: String = "James"
var surname: String = "Lee"
val fullName
    get() = "$name $surname"
    
fun main() {
    println(fullName) // James Lee
    name = "Kim"
    println(fullName) // James Kim
}

이 경우 스마트 캐스트가 불가능합니다.

값을 사용하는 시점의 name에 따라서 다른 결과가 나올 수 있기 때문입니다.

var name: String? = "Marton"
var surname: String = "Braun"
val fullName: String?
    get() = name?.let {"$it $surname"}

val fullName2: String? = name?.let {"$it $surname"}

fun main() {
    if (fullName != null) {
    	println(fullName.length) // 오류
        //println(fullName?.length) // get할 때 마다 null 체크를 해야한다.
    }

    if (fullName2 != null) {
    	println(fullName2.length) // 12 (Marton Braun)
    }
}

 

이러한 경우 때문에, val은 불변(immutable)을 의마하는 것은 아니라는 것을 유의해야 합니다.

 

 


가변 컬렉션과 읽기 전용 컬렉션 구분하기

 

출처:   kotlinlang.com

 

  • 읽기 전용 컬렉션: Iterable, Collection, Set, List
  • 가변 컬렉션: MutableIterable, MutableCollection, MutableSet, MutableList

 

읽기 전용 컬렉션이 내부의 값을 변경할 수 없다는 의미는 아니며,

대부분의 경우 변경할 수 있지만 읽기 전용 인터페이스가 이를 지원하지 않으므로 변경할 수 없습니다.

 

이렇듯 코틀린은 내부적으로 immutable하지 않은 컬렉션을 외부적으로 immutable하게 보이게 만들어서 안정성을 얻습니다.

이러한 안정성은 개발자가 다운캐스팅을 할 경우 문제가 됩니다.

 

컬렉션 다운캐스팅은 immutable 컬렉션을 mutable 컬렉션으로 캐스팅하는 것을 말하며(코틀린은 애초에 이를 지원하지 않는다) 이런 코드는 안전하지 않고, 예측하지 못한 결과를 초래합니다.

 

읽기 전용에서 mutable로 변경해야 한다면, 복제(copy)를 통해서 새로운 mutable 컬렉션을 만들어야 합니다.

복제를 활용해 코드를 작성하면 어떠한 규약도 어기지 않을 수 있으며, 기존의 객체는 여전히 immutable이라 안전하다고 할 수 있습니다.

 


데이터 클래스의 copy

 

immutable 객체를 사용하면 다음과 같은 장점이 있습니다.

 

  • 한 번 정의된 상태가 유지되므로, 코드를 이해하기 쉽다.
  • 공유했을 때도 충돌이 없어 병렬처리에 안전하다.
  • immutable 객체에 대한 참조는 변경되지 않으므로, 쉽게 캐시할 수 있다.
  • 방어적 복사본을 만들 필요가 없고 객체를 복사할 때 깊은 복사를 따로 하지 않아도 된다.
  • 다른 객체를 만들 때 활용하기 좋고 더 쉽게 예측이 가능하다.
  • set 또는 map의 키로 사용할 수 있다. 참고로 mutable 객체는 이러한 것으로 사용할 수 없다. 해시 테이블은 처음 요소를 넣을 때 요소의 값을 기반으로 버킷을 결정하기 때문에, 변경이 일어나면 내부에서 요소를 찾을 수 없게 되기 때문이다.
val names: SortedSet<FullName> = TreeSet()
val person = FullName("AAA", "AAA")
names.add(person)
names.add(FullName("BBB", "BBB")
names.add(FullName("CCC", "CCC")

print(s) // [AAA AAA, BBB BBB, CCC CCC]
print(person in names) // true

person.name = "ZZZ"
print(names) // [ZZZ AAA, BBB BBB, CCC CCC]
print(person in names) // false

 

마지막 출력을 보면, Set 내부에 해당 person 객체가 있음에도 false를 리턴합니다.

객체를 변경했기 때문입니다.

 

mutable 객체는 예측하기 어렵고 위험하다는 단점이 있고 immutable 객체는 변경할 수 없다는 단점이 있습니다.

따라서 immutable 객체는 자신의 일부를 수정한 새로운 객체를 만들어 내는 메서드를 가져야 합니다.

 

예를 들어 User라는 immutable 객체가 있고, 성(surname)을 변경해야 한다면, 아래와 같은 메서드를 제공해서, 자신을 수정한 새로운 객체를 만들어 낼 수 있게 해야 합니다.

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

var user = User("James", "Lee")
user = user.withSurname(surname = "Kim")
print(user) // User(name="James", surname="Kim")

 

이런 함수를 프로퍼티 하나하나 만드는 것은 귀찮습니다.

data 한정자를 사용하면 copy라는 이름의 메서드를 만들어 줍니다.

copy 메서드는 모든 기본 생성자 프로퍼티가 같은 새로운 객체를 만들어줍니다.

data class User (val name:String, val surname: String)

var user = User("James", "Lee")
user = user.copy(surname = "Kim")
print(user) // User(name="James", surname="Kim")

 


다른 종류의 변경 가능 지점

 

변경할 수 있는 리스트를 만든다고 할 때, 두 가지 선택지가 있습니다.

 

  1. mutable 컬렉션으로 만들기
  2. var로 읽고 쓸 수 있는 프로퍼티로 만들기

두 가지 모두 변경할 수 있습니다만 방법이 다릅니다.

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

list1.add(1)
list2 = list2 + 1

list1 += 1 // list1.plustAssign(1)
list2 += 1 // list2 = list.plus(1)

 

두 가지 모두 변경 가능 지점이 다릅니다.

 

첫 번째 코드는 구체적인 리스트 구현 내부에 변경 가능 지점이 있어 멀티스레드 처리 시 내부적으로 적절한 동기화가 되어 있는지 확실하게 알 수 없으므로 위험합니다.

 

두 번째 코드는 프로퍼티 자체가 변경 가능 지점이며 멀티스레드 처리의 안정성이 더 좋다고 할 수 있습니다.

 

또한, mutable 리스트 대신 mutable프로퍼티를 사용하는 형태는 Delegates.observable과 같은 사용자 정의 세터를 활용해서 변경을 추적할 수도 있습니다.

var names by Delegates.observable(listOf<String>()) { _, old, new -> 
	println("Names changed from $old to $new")
}

names += "Fabio" // Names changed from [] to [Fabio]
names += "Bill" // Names changed from [Fabio] to [Fabio, Bill]

 

프로퍼티와 컬렉션을 모두 변경 가능한 지점으로 만드는 것은 최악의 방식입니다.

var list3 = mutableListOf<Int>() // 최악의 방식

 


다른 종류의 변경 가능 지점

 

상태를 나타내는 mutable 객체를 외부에 노출하는 것은 굉장히 위험합니다.

data class User(val name: String) 

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

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

    //...
}

fun main() {
    val userRepository = UserRepository()

    // loadAll을 사용해서 private 상태인 UserRepository를 수정할 수 있다.
    val storedUsers = userRepository.loadAll()
    storedUsers[4] = "Kirill"
    //...

    print(userRepository.loadAll()) // {4=Kirill}
}

 

이러한 문제를 해결하는 방법은 두 가지입니다.

 

첫 번째는 리턴되는 mutable 객체를 복제하는 것으로, 이를 방어적 복제(defensive copying)라고 부릅니다.

이때 data class의 copy메서드를 활용하면 좋습니다.

class UserHolder {
    private val user: MutableUser()

    fun get(): MutableUser {
    	return user.copy()

    //...
}

 

두 번째로, 지금까지 언급했던 것처럼 가능하다면 무조건 가변성을 제한하는 것이 좋습니다.

읽기 전용 슈퍼타입으로 업캐스트하여 가변성을 제한할 수도 있습니다.

class UserRepository {
    private val storedUsers: MuatableMap<Int, String> = mutableMapOf()

    fun loadAll(): Map<Int,String> {
    	return storedUsers
    }

    //...
}

 


정리

  • var보다는 val을 사용하는 것이 좋다.
  • mutable 프로퍼티보다는 immutable 프로퍼티를 사용하는 것이 좋다.
  • mutable 객체와 클래스보다는 immutable 객체와 클래스를 사용하는 것이 좋다.
  • 변경이 필요한 대상을 만든다면, immutable 데이터 클래스로 만들고 copy메서드를 활용하는 것이 좋다.
  • 변이 지점을 적절하게 설계하고, 불필요한 변이 지점은 만들지 않는 것이 좋다.
  • mutable 객체를 외부에 노출하지 않는 것이 좋다.

 

 

예외사항

 

성능적인 측면에서 효율성을 따져야 한다면 immutable 객체보다 mutable 객체를 사용하는 것이 좋을 수도 있습니다.

immutable 객체를 사용할 때는 언제나 멀티스레드 처리에 많은 주의가 필요합니다.

 

 

 

반응형