본문 바로가기
공부/이펙티브코틀린

아이템 1 - 가변성을 제한하라

by 띵커베르 2024. 3. 23.
728x90
  • 아이템 1: 가변성을 제한하라.
    • 가변 프로퍼티 var -> 프로그램 실행중에 변경될 수 있음을 의미
    • mutable 객체 -> 내부 상태를 변경할 수 있는 객체
    • 변할 수 있는 지점은 줄일수록 좋다

  • 코틀린에서 가변성 제한하기
읽기 전용 프로퍼티 - 읽기 전용 프로퍼티는 val 키워드 사용하여 정의 -> 한번 초기화되면 변경할 수 없다.

- 프로퍼티가 가르키는 객체의 상태가 불변임을 가르키는 것은 아니다.
val list = mutableListOf(1, 2, 3)
list = mutableListOf(4, 5, 6) //에러

- list 는 mutableListOf 를 통해 생성된 가변 리스트를 가리키는 읽기 전용 프로퍼티

- list 가 val 로 선언되었기 때문에 list = mutableListOf(4, 5, 6) <- 코드처럼 재할당 할 수 없다
가변성 - 객체의 상태가 변경될 수 있는것을 의미
- 가변 컬렉션과, 불변 컬렉션 존재 함
val immutableList = listOf(1, 2, 3) // 불변 리스트

val mutableList = mutableListOf(1, 2, 3) // 가변 리스트
 

 

  • var 는 getter, setter 를 모두 제공하지만, val 은 변경이 불가능하므로 getter 만 제공한다.
  • 그래서 val 을 var 로 오버라이드 할 수 있다.
// 함수 본문에서 override
class ActualElement : Element {
    override var active: Boolean = false
}

// 생성자에서 override
class ActualElement2(override val active: Boolean) : Element

@Test
fun p9_1() {
    val element = ActualElement()
    element.active = true
    println(element.active)

    val element2 = ActualElement2(true)
    println(element2.active)
}

 

  • 스마트 캐스트
    • 컴파일러가 코드를 분석하여 타입이 확실한 경우 자동으로 타입을 캐스팅 해준다.
    • is -> 타입을 확인할때 사용
@Test
fun p10_1() {
    fun printLength(obj: Any) {
        if (obj is String) {
            // obj는 자동으로 String 타입으로 캐스트됩니다.
            println(obj.length) // 스마트 캐스트
        }
    }
}

 

  • 12페이지 코드 - 읽기전용 컬렉션
    @Test
    fun p12_1() {
        val list = listOf(1, 2, 3)
        if (list is MutableList) {
            assertThrows(UnsupportedOperationException::class.java) { list.add(4) }
//            list.add(4)
        }

        //읽기전용에서 mutable 로 변경해야 한다면 복제를 통해서 새로운 mutable 컬렉션을 만들어야 한다.
        val mutableList = list.toMutableList()
        mutableList.add(4)
        assertThat(mutableList).hasSize(4)
    }
  • 다운캐스팅 하지마세요
  • 리스트를 읽기 전용으로 리턴하면 이를 읽기 전용으로만 사용해야 합니다.
// 이렇게 하지마세요
@Test
fun p12_2() {
    val list = listOf(1, 2, 3)
    // list.add(4) - 에러
    if (list is MutableList) {
        list.add(4) // 이렇게 하지마시오      
    }
}

// 이렇게 하세요
// 읽기전용에서 mutable 로 변경해야 한다면 복제를 통해서 새로운 mutable 컬렉션을 만들어야 한다.
@Test
fun p13_1() {
    val list = listOf(1, 2, 3)
    val mutableList = list.toMutableList()
    mutableList.add(4)
    assertThat(mutableList).hasSize(4)
    assertThat(list).hasSize(3)
}
  • immutable 객체를 많이 사용하는 이유
    • 한 번 정의된 상태가 유지되고, 코드를 이해하기 쉽다
    • 공유했을 때도 충돌이 따로 이루어지지 않으므로, 병렬 처리를 안전하게 할 수 있다.
    • 참조는 변경되지 않으므로, 쉽게 캐시할 수 있다
    • 방어적 복사본을 만들 필요가 없다. 또한 객체를 복사할 때 깊은 복사를 따로 하지 않아도 됩니다.
    • immutable 객체는 세트(set) 또는 맵(map) 의 키로 사용할 수 있습니다.
      • 참고로 mutable 객체는 이러한 것으로 사용할 수 없습니다.
        이는 세트와 맵이 내부적으로 해시 테이블을 사용하고, 해시 테이블은 처음 요소를 넣을때 요소의 값을 기반으로 버킷을 결정하기 때문입니다.
        따라서 요소에 수정이 일어나면 해시 테이블 내부에서 요소를 찾을 수 없게 되어 버립니다.
    • immutable 객체를 Int 와 같이 내부적으로 plus, minus 메서드로 자신을 수정한 새로운 Int 를 리턴할 수 있듯이 내부 메서드를 만들어 주면 좋다
      하지만 이러한 경우 모든 프로퍼티를 대상으로 함수를 하나하나 만드는 것은 굉장이 귀찮은 일이다.
      이럴때는 data 한정자를 사용해서 copy 메서드를 활용하자
13페이지 4번 관련.. 
<방어적 복사본을 만들 필요가 없다. 또한 객체를 복사할 때 깊은 복사를 따로 하지 않아도 됩니다.>

data class Person_p13(val name: String, val surname: String)
@Test
fun `p13 4번 - 방어적복사 + 깊은복사 필요가없다`() {
    val originPerson = Person_p13("Injin", "Jeong")
    val copyPerson = originPerson
//        originPerson.name = "minsu" // 에러
//        originPerson.surname = "Kim" // 에러
    assertThat(originPerson).isEqualTo(copyPerson)
}


// name과 surname은 불변이지만, friends 리스트는 가변 상태
data class PersonWithFriends(
    val name: String,
    val surname: String,
    val friends: MutableList<String> = mutableListOf()
) 
@Test
fun `p13 4번 - 방어적복사 + 깊은복사 예외케이스`() {
    val originPerson = PersonWithFriends("Injin", "Jeong")
    originPerson.friends.add("minsu")

    val copyPerson = originPerson.copy(friends = originPerson.friends.toMutableList())
    copyPerson.friends.add("suchan")

    assertThat(originPerson.friends).hasSize(1)
    assertThat(copyPerson.friends).hasSize(2)
}

@Test
fun `p14 6번 예시`() {
    val person = MutablePerson("Injin Jeong")
    val map = mutableMapOf(person to "Developer")

    println(map[person]) // "Developer" 출력
    assertThat(map[person]).isEqualTo("Developer")

    // 객체 상태 변경
    person.name = "Jane"

    // 상태 변경 후 해시 코드가 변경됨
    println(map[person]) // null 출력
    assertThat(map[person]).isNull()
}

 

 


  • 다른 종류의 변경 가능 지점
    • 변경할 수 있는 리스트를 만들어야 한다면 2가지 방법이 있다.
      • mutable 컬렉션을 만드는 것
      • val list1: MutableList<Int> = mutableListOf() // mutable list
        • 구체적인 리스트 구현 내부에 변경 가능 지점이 있다.
          • mutableList 가변컬렉션이기 때문에 컬렉션 내부의 요소를 변경할 수 있다.(add, removeAt ---)
      • var 로 프로퍼티를 만드는 것
        • var list2: List<Int> = listOf() // var 프로퍼티 선언 
        • 프러퍼티 자체가 변경 가능 지점이다.
        • listof 는 불변 리스트를 참조하지면 var 로 선언되어있기에 + 같은연산으로 새로운 list 를 반환하면 참조를 변경한다
    • 개인적인 생각이지만 애플리케이션 요구사항에 따라 적절히 잘 사용하면 될거 같다 대신 var list5 = mutableListOf<Int>() 같이 안하면 될듯.
    • mutable 리스트 대신 mutable 프로퍼티를 사용하는 형태는 사용자 정의 세터(또는 이를 사용하는 델리게이트) 를 활용해서 변경을 추적할 수 있습니다.
      예를 들어 Delegates.observable 을 사용하면 리스트에 변경이 있을 때 로그를 출력할 수 있습니다.
@Test
fun p17_1() {
    var names by Delegates.observable(listOf<String>()) { _, old, new ->
        println("====================================")
        println("Names changed from $old to $new")
    }

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

    /**
     * Delegates.observable은 프로퍼티의 참조나 값 자체의 변경을 감지하는 용도로 유용하지만,
     * 리스트와 같은 컬렉션의 내용 변경을 직접 감지하지는 않습니다.
     * 컬렉션의 내용 변경을 감지하려면 컬렉션을 래핑하거나 컬렉션의 변경 가능한 메서드를
     * 오버라이딩하는 등의 추가적인 구현이 필요합니다.
     */
    var myList: MutableList<Int> by Delegates.observable(mutableListOf()) { prop, old, new ->
        println("List has changed from $old to $new")
    }

    myList.add(1) // 리스트 변경
    myList = mutableListOf(1, 2, 3) // 리스트 재할당
}

====================================
Names changed from [] to [Fabio]
====================================
Names changed from [Fabio] to [Fabio, Bill]
====================================
List has changed from [1] to [1, 2, 3]

 

 


 

  • 변경 가능 지점 노출하지 말기
    • 상태를 나타내는 mutable 객체를 외부에 노출하는 것은 굉장히 위험하다
class UserRepository {
    private val storedUsers: MutableMap<Int, String> = mutableMapOf()

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

    fun loadAll2(): Map<Int, String> {
        return storedUsers
//        return storedUsers.toMap()
    }
}

@Test
fun p18_1() {
    val userRepository = UserRepository()
    val storedUsers = userRepository.loadAll()
    storedUsers[4] = "sososososo"

    println(storedUsers)

    println("===")

    val storedUsers2 = userRepository.loadAll2()
//        storedUsers2[4] = "sososososo" //에러

}

{4=sososososo}
===
  • 위 코드처럼 loadAll 후 수정이 일어나면 위험할 수 있다. 이를 처리하는 방법은 두 가지 이다
  • 첫 번째는 return 되는 mutable 객체를 복제하는 것입니다.(copy, toMap - - -)
  • 가능하다면 가변성을 제한하는 것이 좋다 -> 컬렉션은 객체를 읽기 전용 슈퍼타입으로 업캐스트 하여 가변성을 제한할 수 있다(Map)

 


  • var 보다는 val 을 사용하자
  • mutable 프로퍼티보다는 immutable 프로퍼티를 사용하는 것이 좋습니다.
  • mutable 객체와 클래스보다는 immutable 객체와 클래스를 사용하는 것이 좋습니다.
  • 변경이 필요한 대상을 만들어야 한다면 immutable 데이터 클래스로 만들고 copy 를 활용하는 것이 좋습니다.
  • 컬렉션에 상태를 저장해야 한다면, mutable 컬렉션보다는 읽기 전용 컬렉션을 사용하는 것이 좋습니다.
    • 컬렉션을 다룰 때 외부에서는 컬렉션 내용을 변경할 수 없도록 하면서, 내부에서는 필요에 따라 변경을 관리하게 하라 같은 맥락
    • Backing Property 패턴 을 쓰라는게 아닐까..?
  • 변이 지점을 적절하게 설계하고, 불필요한 변이 지점은 만들지 않는 것이 좋습니다.
  • mutable 객체를 외부에 노출하지 않는 것이 좋습니다.

 

---

Backing Property 패턴

class Item(val name: String)

class Inventory {
    // 클래스 내부에서 사용될 가변 리스트
    private val _items = mutableListOf<Item>()

    // 외부에 노출될 읽기 전용 리스트
    val items: List<Item> get() = _items

    // Item을 추가하는 메서드
    fun addItem(item: Item) {
        _items.add(item)
    }

    // Item을 제거하는 메서드
    fun removeItem(item: Item) {
        _items.remove(item)
    }
}

    @Test
    fun `Banking Property 패턴`() {
        val inventory = Inventory()
        val items = inventory.items
//        items.add(Item("Sword")) // 에러
        inventory.addItem(Item("Sword"))
        inventory.addItem(Item("Shield"))
        inventory.addItem(Item("Potion"))

        println(inventory.items)
        inventory.removeItem(inventory.items[1])
        println(inventory.items)
    }

 


- 몇 가지 예외가 있다. 효율성 때문에 immutable 객체보다 mutable 객체를 사용하는 것이 좋을 때가 있다

- 이러한 최적화는 코드에서 성능이 중요한 부분에서만 사용하는 것이 좋습니다.

- 참고: 아이템52: mutable 컬렉션 사용을 고려하라

2024.03.25 - [공부/이펙티브코틀린] - 아이템 52 - mutable 컬렉션 사용을 고려하라

728x90

댓글