[Effective Kotlin] 가변성을 제한하라.
가변성을 제한하라.
읽고 쓸수 있는 프로퍼티 var을 사용하거나 mutable 객체를 사용하면 상태를 가질 수 있습니다. 상태를 가지면 해당 요스의 동작은 사용 방법뿐만 아니라 그 이력에도 의존하게 됩니다.
가변성이 있을 시 문제점
1. 프로그램을 이해하고 디버그하기 힘들어집니다. 이러한 상태를 갖는 부분들의 관계를 이해해야 하며, 상태 변경이 많아지면 이를 추적하는것이 힘들어집니다. 이러한 클래스는 이해하기도 어렵고, 이후에 코드를 수정하기도 힘들어집니다. 클래스가 예상하지 못한 상황 또는 오류를 발생시키는 경우에도 큰 문제가 됩니다.
2. 가변성이 있으면, 코드의 실행을 추론하기 어려워집니다. 시점에 따라서 값이 달라질 수 있으므로, 현재 어떤 값을 갖고 있는지 알아야 코드의 실행을 예측할 수 있습니다. 또한 한 시점에 확인한 값이 계속 동일하게 유지된다고 확신할 수 없습니다.
3. 멀티스레드 프로그램일 때는 적절한 동기화가 필요합니다. 변경이 일어나는 모든 부분에서 충돌이 발생할 수 있습니다.
4. 테스트하기 어렵습니다. 모든 상태를 테스트해여 하므로, 변경이 많을수록 더 많은 조합을 테스트해야 합니다.
5. 상태 변경이 일어날 때, 이러한 변경을 다른 부분에 알려야하는 경우가 있습니다. 예를 들어 리스트에 가변 요소를 추가한다면, 요소에 변경이 일어날 때마다 리스트 전체를 다시 정렬해야 합니다.
Solution
1. var보다는 val을 사용하는 것이 좋습니다.
- var은 set, get 모두 허용하고, val은 get만 허용하기 떄문에
2. mutable 프로퍼티보다는 immutable 프로퍼티를 사용하는 것이 좋습니다.
3. mutable 객체와 클래스보다는 immutable 객체와 클래스를 사용하는 것이 좋습니다.
4. 변경이 필요한 대상을 만들어야 한다면, immutable 데이터 클래스로 만들고 copy를 활용하는 것이 좋습니다.
class User(val name: String, val surname: String) {
fun withSurname(surname: String) = User(name, surname)
}
var user = User("Maja", "Markiewicz")
user = user.withSurname("moskala")
print(user) // User(name=Maja, surname=Moskala)
위처럼 모든 프로퍼티를 대상으로 이런 함수를 하나하나 만드는 것은 굉장히 귀찮은 일이기 때문에 아래처럼 data 한정자의 copy를 사용합니다.
data class User(val name: String, val surname: String)
var user = User("Maja", "Markiewicz")
user = user.copy(surname = "Moskala")
print(user) // User(name=Maja, surname=Moskala)
5. 컬렉션에 상태를 저장해야 한다면, mutable 컬렉션보다는 읽기 전용 컬렉션을 사용하는 것이 좋습니다.
var announcements = listOf<Announcement>()
private set
이렇게 하면 여러 객체를 변경하는 여러 메서드 대신 세터를 사용하면 되고, 이를 private로 만들 수 있기 때문입니다.
6. 변이 지점을 적절하게 설계하고, 불필요한 변이 지점은 만들지 않는 것이 좋습니다.
1. 리턴되는 mutable 객체를 복제하는 것(방어적 복제)
class UserHolder {
private val user: MutableUser()
fun get(): MutableUser {
return user.copy()
}
}
2. 컬렉션은 객체를 읽기 전용 슈퍼타입으로 업캐스트하여 가변성을 제한
data class User(val name: String)
class UserRepository {
private val storedUsers: MutableMap<Int, String> =
mutableMapOf()
fun loadAll(): Map<Int, String> {
return storedUsers
}
/*
나쁜 코드 :
fun loadAll(): MutableMap<Int, String> {
return storedUsers
}
val userRepository = UserRepository()
val storedUsers = userRepository.loadAll()
storedUsers[4] = "Kirill"
print(userRepository.loadAll()) // {4=kirill}
아래와 같은 코드는 돌발적인 수정이 일어날 때 위험할 수 있습니다.
*/
7. mutable 객체를 외부에 노출하지 않는 것이 좋습니다.