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 객체는 이러한 것으로 사용할 수 없습니다.
이는 세트와 맵이 내부적으로 해시 테이블을 사용하고, 해시 테이블은 처음 요소를 넣을때 요소의 값을 기반으로 버킷을 결정하기 때문입니다.
따라서 요소에 수정이 일어나면 해시 테이블 내부에서 요소를 찾을 수 없게 되어 버립니다.
- 참고로 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 을 사용하면 리스트에 변경이 있을 때 로그를 출력할 수 있습니다.
- 변경할 수 있는 리스트를 만들어야 한다면 2가지 방법이 있다.
@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 컬렉션 사용을 고려하라
728x90
'공부 > 이펙티브코틀린' 카테고리의 다른 글
아이템 5 - 예외를 활용해 코드에 제한을 걸어라 (0) | 2024.03.31 |
---|---|
아이템 4 - inferred 타입으로 리턴하지 말라 (0) | 2024.03.31 |
아이템 3 - 최대한 플래폼 타입을 사용하지 말라 (0) | 2024.03.31 |
아이템 2 - 변수의 스코프를 최소화하라 (0) | 2024.03.25 |
아이템 52 - mutable 컬렉션 사용을 고려하라 (0) | 2024.03.25 |
댓글