[Effective Kotlin] 아이템1 - 가변성을 제한하라

우발자·2025년 11월 16일
1

Effective Kotlin을 읽고 정리하는 글이다.

주제 : 😇 코틀린에서 가변성을 제한하는 방법이 있다.

오늘의 목차

1. 읽기 전용 프로퍼티 (val)

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

3. 데이터 클래스의 copy


㆒ 읽기 전용 프로퍼티

코틀린에서는 val를 사용하면 읽기 전용 프로퍼티를 만들수 있다.

val a = 10
a = 20 // 오류!

하지만 읽기 전용 프로퍼티가 완전히 변경 불가능한 것은 아니라는 것을 주의해야된다.
읽기 전용 프로퍼티가 mutable 객체를 담고 있다면, 내부적으로 변할 수 있다.

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

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

또 다른 경우가 있는데 게터를 정의할 때 var 프로퍼티를 사용하면 var 프로퍼티가 변경될 때 변할 수 있다.

var name : String = "Woo"
var SurName : String = "Kim"

val fullName 
	get() = "$SurName $name"

println(fullName) // "Kim Woo"
name = "Boo"
println(fullName) // "Kim Boo"

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

위에 설명과 마찬가지로 코틀린에선 읽고 쓸 수 있는 컬렉션과 읽기 전용 컬렉션이 있다.

코틀린을 공부하다가 이런 이미지는 많이 봐왔었다.

mutable이 붙은 인터페이스는 대응되는 읽기 전용 인터페이스를 상속 받아서, 변경을 위한 메서드를 추가한 것이다.

inline fun <T,R> Iterable<T>.map(
	transformation: (T) -> R
) : List<R> {
	val list = ArrayList<R>()
    for (elem in this) {
    	list.add(transformaiton(elem))
    }
    return list
}

Iterable.map() 코드를 보면 진짜 불변하게 만들지 않고 읽기 전용으로 설계하였다.
이로 인해서 더 많은 자유를 얻을 수 있다.
가변으로 동작하여 메모리나 접근 자체에 대한 효율을 높이고
사용시에는 읽기 전용으로 안정성을 높일 수 있기 때문이다.

하지만 다운캐스팅을 위반해서 사용하는 일이 있을 수 있다.

val list = listOf(1,2,3)

// 이렇게 사용 금지!!
if(list is MutableList) {
	list.add(4)
}

읽기전용으로 리턴하면 그 객체는 읽기 전용으로만 사용해야 된다!


㆔ 데이터 클래스의 copy

String이나 Int처럼 내부적인 상태를 변경하지 않는 immutable 객체를 많이 사용하면 여러 장점이 있다.

1️⃣ 한 번 정의된 상태가 유지되므로, 코드를 이해하기 쉽다.

2️⃣ immutable 객체는 공유했을 때도 충돌이 따로 이루어지지 않으므로, 병렬 처리를 안전하게 할 수 있다.

3️⃣ immutable 객체에 대한 참조는 변경되지 않으므로, 쉽게 캐시할 수 있다.

4️⃣ immutable 객체는 방어적 복사본을 만들 필요가 없다.

5️⃣ immutable 객체는 다른 객체를 만들 때 활용하기 좋다.

6️⃣ immutable 객체는 set or map의 키로 사용할 수 있다.

  • mutable객체는 사용 불가능하다. set 과 map이 내부적으로 해시 테이블을 사용하고, 해시 테이블은 처음 요소를 넣을 때 요소의 값을 기반으로 버킷을 결정하기 때문에 수정이 발생하면 해시 테이블 내부에서 요소를 찾지 못한다.
data class User(
	val name : String,
    val surName : String
)

var user = User("Woo", "Kim")
user = user.copy(surName = "Park")
print(user) // User(name=woo, surName=Park)

이렇게 data 한정자는 copy라는 메서드를 만들어 주기 때문에 데이터 모델 클래스를 만들어 immutable 객체로 만드는 것이 많은 장점을 가지므로 기본적으로 이렇게 사용하는 것이 좋다.


+추가) 변경 가능 지점을 줄여라

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

list1.add(1)
list2 = list2 + 1

list1과 list2의 동작은 모두 정상적이지만 장단점이 있다.
두가지 모두 변경 가능 지점이 있지만 그 위치가 다르다.

list1은 리스트 구현 내부에 변경 가능 지점이 있고 멀티스레드 처리가 이루어질 경우, 내부적으로 적절한 동기화가 되어 있는지 확실하게 알 수 없다.

list2는 프로퍼티 자체가 변경 가능 지점이다. 따라서 멀티스레드 처리에 안정성이 더 좋다고 할 수 있다.

mutable 컬렉션을 사용하는 것이 더 간단하지만, mutable 프러퍼티를 사용하면 객체 변경을 제어하기 더 쉽다.

❌ 그래서 최악에 방식은 프로퍼티와 컬렉션 모두 변경 가능한 지점을 만드는 것이다.

// 최악 !!!
var list3 = mutableListOf<Int>()

+추가) 변경 가능 지점을 노출하지 말기

data class User(val name : String)

class UserRepository {
	private val storedUser : MutableMap<Int, String> = 
    	mutableMapOf()
        
	fun loadAll() : MutableMap<Int, String> {
    	return storedUsers
    }
}

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

val userRepository = UserRepository()

val storedUsers = userRepository.loadAll()
storedUsers[4] = "Woo"

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

이렇게 loadAll()을 사용해서 private 상태인 UserRepository를 외부에서 변경할 수 있게된다.

이를 방지하는 방법

방어적 복제

class UserHolder {
	private val user : MutableUser()
    
    fun get() : MutableUser {
    	return user.copy()
    }
}

업캐스트

data class User(val name : String)

class UserRepository {
	private val storedUser : MutableMap<Int, String> = 
    	mutableMapOf()
        
	fun loadAll() : Map<Int, String> {
    	return storedUsers
    }
}

loadAll()의 반환값을 MutableMap -> Map으로 업캐스트하여 가변성을 제어할 수 있다.


후기

내가 개발을 하면서 이러한 부분은 고려하지 않은 것 같다.
var보단 val을 선호하며
mutable컬렉션을 선호하기보단 mutable프로퍼티를 선호해야겠다.

끝.

profile
어제보다 나은 개발자가 되자

0개의 댓글