val은 읽기 전용이지 불변이 아닌 이유
val은 읽기 전용이지 불변이 아닌 이유
Kotlin에서 val 키워드는 불변성(immutability)을 보장한다고 흔히 오해받지만, 실제로는 초기화 이후 참조 재할당만 금지할 뿐입니다. 참조가 가리키는 객체 자체의 변경까지 막아 주지는 않습니다. 이 참조 불변성(reference immutability)과 객체 가변성(object mutability)의 차이는 Kotlin 타입 시스템을 정확히 이해하고, 동시성 코드나 상태에 민감한 코드에서 미묘한 버그를 예방하는 데 매우 중요합니다. 면접에서도 val이 "불변"이 아닌 "읽기 전용"임을 명확히 구분하여 설명할 수 있으면 기본기에 대한 깊은 이해를 보여줄 수 있으므로, 반드시 숙지해 두시길 권장합니다. 이번 면접 질문을 통하여 아래 내용들을 학습하실 수 있습니다.
- 읽기 전용 참조(read-only reference)와 불변 객체(immutable object)의 차이를 명확히 설명할 수 있습니다.
val로 선언한 가변 객체가 자체 API를 통해 어떻게 수정될 수 있는지 보여줄 수 있습니다.- 커스텀 getter가
val프로퍼티의 반환값을 매번 다르게 만들 수 있는 원리를 설명할 수 있습니다. - Kotlin에서 진정한 불변성을 달성하기 위한 전략을 파악할 수 있습니다.
참조 불변성 vs 객체 가변성
val로 변수를 선언하면, Kotlin 컴파일러는 해당 참조의 재할당을 금지합니다. 참조 자체는 고정되지만, 참조가 가리키는 객체가 내부 상태를 변경하는 메서드를 노출하고 있다면 객체 내용은 얼마든지 바뀔 수 있습니다.
val mutableList = mutableListOf(1, 2, 3)
mutableList.add(4) // 허용: 리스트 내용이 변경됨
// mutableList = mutableListOf(5, 6) // 컴파일 오류: 참조 재할당 불가
mutableList 참조는 항상 동일한 MutableList 인스턴스를 가리킵니다. 하지만 해당 인스턴스에 add()를 호출하면 리스트 내용이 변경됩니다. 즉, val 키워드는 객체 내부에서 무슨 일이 벌어지는지에 대해서는 어떤 제약도 두지 않습니다.
이 동작은 Java의 final과 본질적으로 동일합니다. final 변수는 재할당할 수 없지만, 참조하는 객체는 공개 API를 통해 여전히 수정할 수 있습니다. 면접에서 Java와의 유사성을 함께 언급하면 더 넓은 시야를 가진 답변이 됩니다.
동시성 코드에서는 이 구분이 더욱 중요해집니다. val 참조는 참조 자체가 변경되지 않으므로 스레드 간 공유가 안전합니다. 하지만 참조가 가리키는 객체가 가변이고, 여러 스레드가 동시에 객체를 수정한다면 val로 선언했더라도 경쟁 조건(race condition)이 발생할 수 있습니다.
val sharedMap = mutableMapOf("key" to "value")
// Thread 1: sharedMap["key"] = "updated"
// Thread 2: sharedMap["key"] = "conflict"
// val 선언에도 불구하고 경쟁 조건 발생 가능
불변 맵이나 ConcurrentHashMap과 같은 스레드 안전 구현체를 사용하면 이 문제를 해결할 수 있지만, val 키워드만으로는 객체의 동시 수정에 대한 보호를 전혀 제공하지 못합니다. 따라서 동시성 환경에서는 val 선언 여부와 별개로, 객체 자체의 스레드 안전성을 반드시 확보해야 합니다.