변경 가능한 상태를 적절히 관리하는게 어렵다.
- 프로그램 이해하기 어려움
- 코드의 실행을 추론하기 어렵다. 시점에 따라 값이 다르므로 어떤 값을 가진지 알아야 한다.
- 멀티스레드는 적절한 동기화가 필요하다. 변경이 일어나는 모든 부분에 충돌 가능
- 테스트 어렵다. 변경이 많으면 더 많은 조합을 테스트 해야 한다.
- 상태 변경시 다른 부분에 알려야 할 경우가 있다. → 정렬된 리스트에 요소 추가
코틀린에서의 가변성 제한
코틀린은 Immutable 객체를 만들거나, 프로퍼티를 변경 할 수 없게 막는게 쉽다.
- 읽기 전용 프로퍼티(val)
- 가변 컬렉션과 읽기 전용 컬렉션 구분하기
- 데이터 클래스의 copy
읽기 전용 프로퍼티 val
val로 선언된 프로퍼티는 값(value)처럼 동작하며, 일반적으로 값이 변하지 않는다.
val의 값이 변할 수는 있지만, 레퍼런스 자체를 변경 할 수는 없다.
val은 읽기 전용 프로퍼티지만, 변경할수 없음을 의미하진 않는다.
가변 컬렉션과 읽기 전용 컬렉션 구분
코틀린의 컬렉션은 읽기전용, 읽고쓰기용 으로 구분된다.
실제 불변 컬렉션을 사용하는게 아닌, 인터페이스를 이용해서 읽기 전용으로 설계했다.
내부적으로 인터페이스를 사용하고 있으므로, 실제 컬렉션 리턴이 가능. → 플랫폼 고유의 컬렉션 사용 가능.
컬렉션 다운캐스팅은 하면 안된다. → toMutableList() 등 메소드를 통해 복제해서 사용해야 한다.
데이터 클래스의 copy
immutable 객체를 사용하는 이유.
- 한번 정의된 상태가 유지되므로, 코드 이해가 쉽다.
- 안전한 병렬 처리 가능
- 참조는 변경되지 않으므로, 쉽게 캐시 가능
- 방어적 복사본을 만들 필요가 없다. 깊은 복사를 따로 하지 않아도 된다.
- 다른객체를 만들때 활용하기 좋다. 더 쉽게 예측 가능하다.
- set과 map의 키로 사용 가능하다.
immutable 객체를 변경하기 위해서는 새로운 객체를 리턴해서 사용하도록 한다.
data 클래스의 copy 메소드를 사용하면 모든 프로퍼티가 같은 새 객체를 만들 수 있다.
변경 가능만 보면 mutable 객체가 좋아 보이지만, 데이터 모델 클래스로 immutable 객체로 만드는게 더 많은 장점을 가진다.
다른 종류의 변경 가능 지점
변경 가능한 두 리스트
val listl: MutableList<Int> = mutableListOf()
var list2: List<Int> = listOf()
list1.add(1)
list2 = list2 + 1
두가지 모두 정상적 동작. 변경 가능 지점의 위치가 다르다.
첫번째는 리스트 구현 내부에 변경 가능 지점이 위치 → 멀티스레드 처리가 이루어 질 경우 위험
두번째는 프로퍼티 자체가 변경 가능지점 → 멀티스레드 처리의 안정성은 더 좋다(설계 오류시 일부 요소 손실 가능)
mutable 컬렉션 대신 mutable 프로퍼티를 사용하는 형태는 커스텀 세터를 사용해서 변경 추적 가능.
var names by Delegates.observable(listOf<String>()) { _, old, new ->
printIn("Names changed from $old to $new")
}
names += "Wangmin"
// names가 []에서 [Wangmin]로 변합니다.
names += "Minjae"
// names가 [Wangmin]에서 [Wangmin, Minjae]로 변합니다.
mutable 컬렉션도 옵져빙 하려면 추가적인 구현이 필요하다. 따라서 mutable 프로퍼티에 읽기전용 컬렉션을 넣어서 사용하는게 쉽다.
mutable 컬렉션을 사용하는게 더 간단하게 느껴지겠지만, mutable 프로퍼티를 사용하면 객체 변경을 제어하기 더 쉽다.
최악은 프로퍼티, 컬렉션 모두 변경 가능하게 하는것
변경 가능 지점 노출하지 말기
상태를 나타내는 mutable 객체를 외부에 노출하는건 위험하다.
class UserRepository {
private val users: MutableMap<Int, String> = mutableMapOf()
fun loadAll(): MutableMap<Int, String> {
return users
}
}
loadAll을 사용해서 userRepository를 수정 가능하다.
- 방어적 복제
copy를 활용해서 복제된 객체를 리턴
fun loadAll(): MutableMap<Int, String> {
return users.copy()
}
- 가변성 제한
- 읽기전용 슈퍼타입으로 업캐스트해서 가변성 제한
fun loadAll(): Map<Int, String> {
return users
}
정리
- var 보단 val
- mutable 프로퍼티 보다는 immutable 프로퍼티
- mutable 객체, 클래스 보단 immutable 객체, 클래스
- 변경 필요한 대상을 만들땐 data 클래스의 copy 메소드 활용
- 컬렉션에 상태를 저장해야 하면, mutable 컬렉션 보단 읽기 전용 컬렉션 사용
- 변이지점은 적절히 설계, 불필요한 변이지점은 생성하지 않음
- mutable 객체는 외부 노출하지 않음
참조
'Books > Effective Kotlin' 카테고리의 다른 글
아이템 2: 변수의 스코프를 최소화하라 (0) | 2022.02.12 |
---|---|
이펙티브 코틀린 1장 : 안정성 (0) | 2022.02.12 |