불변성(Immutability)과 가변성(Mutability)
불변성이란 무엇일까?
본질적으로 함수형 프로그래밍은 스레드 안전(thread-safe)입니다. 그리고 함수형 프로그래밍에서는 불변성을 중요하게 생각하며, 불변성은 스레드를 안전하게 만드는 데 큰 역할을 합니다. 사전적인 정의로 불변성은 무언가가 변할 수 없다는 것을 의미합니다.(상태를 변경하지 않는 것) 따라서 불변 변수는 변경될 수 없는 변수를 말합니다.
주의해야할 점은 불변성을 클래스를 생성하고 모든 변수를 읽기 전용으로 만드는 것 정도로 생각하면 안 됩니다. Clojure, Haskell, F# 등과는 달리 코틀린은 불변성이 강제되는 순수 함수형 프로그래밍 언어가 아닙니다. 코틀린은 함수평 프로그래밍과 객체지향 프로그래밍(OOP) 언어의 조화가 이루어진 언어입니다. 즉, 이 두 패러다임의 주요 이점을 모두 가집니다. 코틀린은 불변성을 강요하는 대신 권장하며, 가능하면 자동으로 불변을 제공하려 합니다.
그렇다면 상태를 변경하는 것은 프로그램의 변수를 변경하거나 재할당하는 행위라고 볼 수 있지만 더 근본적으로는 컴퓨터에 저장된 메모리의 특정 공간에 저장된 값을 변경하는 행위를 의미합니다. 이런 행위는 어떤 문제가 생기길래 코틀린에서 불변성을 권장할까요?
가변성(Mutability)의 문제
불변성과 반대로 가변성은 상태를 가지는 경우를 얘기합니다. 만약 상태를 가지면 어떤 문제점들이 발생할까요?
앞서 상태를 변경하는 행위는 메모리의 저장된 값을 변경하는 행위라고 언급했습니다. 이렇게 메모리에 저장된 하나의 값을 누구든지 변경할 수 있다는 것은 무분별한 상태가 변경이 된다는 것을 의미합니다. 무분별한 상태가 변경이 되는 것은 다음과 같은 문제를 발생 시킬 수 있습니다.
- 멀티스레드에서 값을 보장하지 못함
- 값의 예측이 어렵고 변경에 있어서 위험하다
- 테스트와 디버깅이 어려움
- 상태 변경 발생 시 처리를 해주어야함
불변성(Immutability)을 지켜야 하는 이유
불변성이란 값이나 상태를 변경할 수 없는 것으로 정의됩니다. 불변 객체는 생성 시점 이후 한 번 정의된 상태는 계속 유지하며 변경되지 않으므로 스레드 간 안전성이 보장되며 이를 통해 동기화 문제를 해결할 수 있습니다. 그리고 한 번 생성한 값은 변경되지 않으므로 캐시도 수월합니다. 또한 기존 객체에서 프로퍼티가 변경된 객체를 리턴 받고자 할 때 방어적 복사본을 작성하지 않아도됩니다.
- 스레드 안전성(thread-safe)
- 캐시가 쉬움
- 방어적 복사본이나 깊은 복사를 하지 않아도됨
- 사이드 이펙트를 줄임
Kotlin에서는 가변성을 어떻게 제한하고 있을까?
코틀린에서는 크게 3가지로 가변성을 제한하고 있습니다.
- 읽기 전용 프로퍼티 val, const val
- 참조 불변성, 불변 값
- data class의 copy()
var, val, const val
코틀린은 불변성을 장려하지만 개발자에게 선택권을 주기 위해 두 종류의 변수를 소개합니다.
첫 번째는 var 입니다. 흔히 Java 언어에서 변수를 선언하는 것과 같이 가변적인 변수를 선언할 때 사용됩니다.
이에 비해 두번째로 소개할 val은 불변성에 좀 더 가깝습니다.
'불변성에 좀 더 가깝다'라고 표현하는 이유는 이 역시 완전한 불변성을 보장하지는 않기 때문입니다. val 변수는 읽기 전용을 강제하며, 초기화 이후에 val 변수에 새로운 값을 대입할 수 없습니다. 자바의 final 키워드 정도로 이해하면 되겠습니다.
fun main(args: Array<String>) {
val x: String = "kotlin"
x += "immutability" // compile error!
}
이 코드는 컴파일되지 않습니다. x 변수를 val로 선언했으므로 x를 초기화한 후에는 읽기 전용이 됩니다.
그렇다면 왜 val이 완전한 불변성을 보장하지 않는건지 궁금증이 생길 것입니다. 다음 예제를 통해 살펴보겠습니다.
object MutableVal {
var count = 0
val myString: String = "mutable"
get() {
return "$field ${++count}"
}
}
fun main(args: Array<String>) {
println("first call ${MutableVal.myString}") // first call mutable 1
println("second call ${MutableVal.myString}") // first call mutable 2
println("third call ${MutableVal.myString}") // first call mutable 3
}
이 코드에서는 myString을 val로 선언했지만 커스텀 get() 함수도 구현했습니다. 이렇게 커스텀 getter 함수를 구현하게 될 경우 myString 변수를 요청할 때마다 count가 증가하고 결과적으로 매 호출마다 다른 값을 얻게 됩니다. 이는 val 속성의 불변적인 동작을 파괴한 것입니다.
어떻게 이를 극복할 수 있을까요? 코틀린에서 불변성을 강제하는 방법으로 const val 속성이 있습니다. const val로 수정하면 커스텀 getter를 구현할 수 없습니다. 흔히 val을 읽기 전용 변수, const val을 컴파일 타임 상수라고 합니다. 둘의 차이점을 조금 더 자세히 알아보겠습니다.
val | const val |
읽기 전용 변수 | 컴파일 타임 상수 |
커스텀 getter 가능 | 커스텀 getter 불가능 |
함수 내부, 클래스 멤버 등 어디서나 val 사용 가능 | object내 선언 또는 companionObject내 선언 또는 최상위 멤버여야만 함 |
델리게이트 작성 가능 | 델리게이트 작성 불가능 |
모든 타입에 대한 val 속성 가능 | 기본 데이터 타입과 문자열만이 const val 속성 가능 |
nullable | non-nullable |
결과적으로 const val 속성은 값의 불변성은 보장하지만 유연성에서 떨어집니다. 또한 const val은 기본 타입과 문자열만 사용해야 하므로 제한이 꽤나 있습니다.
불변성의 종류
기본적으로 불변성에는 다음과 같은 두 가지 타입이 있습니다.
- 참조 불변성
- 불변 값
참조 불변은 일단 참조가 할당되면 다른 것에 할당할 수 없게 하는 것입니다. 대표적으로 Kotlin Collection 프레임워크의 MutableList의 val 속성을 예로 들어 보겠습니다.
fun main(args: Array<String>) {
val list = mutableListOf(1, 2, 3, 4, 5)
println(list) // [1, 2, 3, 4, 5]
list.add(6)
println(list) // [1, 2, 3, 4, 5, 6]
}
위 코드에서 list 변수는 val로 선언 되었기 때문에 '읽기 전용'이어야 할 것 같은데 list.add(6)을 통해서 값의 변경이 일어난 것을 확인할 수 있습니다. 이게 어떻게 된 일일까요? 이는 val 속성이 "불변 참조"이기 때문입니다. list에 내부적으로 저장하고 있는 값들에 변경이 일어나더라도 list가 참조하는 MutableList의 인스턴스가 변한 것이 아니기 때문에 컴파일 에러가 발생하지 않습니다. 만약 다음과 같이 코드를 작성했다면 컴파일 에러가 발생했을 것입니다.
fun main(args: Array<String>) {
val list = mutableListOf(1, 2, 3, 4, 5)
list = listOf(1,2,3) // compile error!
}
반면 불변 값은 값을 변경하지 못하게 합니다. 따라서 유지 관리가 다소 복잡해집니다. 코틀린에서는 이러한 불변 값을 const val을 통해 제공하지만 융통성이 부족해 실제로는 잘 사용하지 않습니다.
또한 코틀린은 컬렉션에 대해서 읽기 전용(read-only)와 가변 컬렉션을 엄격하게 구분하고 있습니다.
- 읽기 전용 : Iterable, Collection, List, Set 인터페이스
- Mutable : MutableIterable, MutableCollection, MutableSet, MutableList 인터페이스
읽기 전용은 내부에서 값을 변경하는 함수들을 제공하지 않습니다. 만약 데이터를 추가, 삭제, 수정하려는 경우에는 toMutableList() 함수를 이용해서 요소들을 변경 가능한 컬렉션으로 변경하여 사용해야합니다. 만약 list as MutableList와 같이 다운캐스팅을 시도 한다면 코틀린에서 정한 읽기전용 규칙을 무시하기 때문에 이러한 행위는 지양해야 합니다.
또한 코틀린에서 컬렉션을 다룰 때 var와 함께 Mutable Collection을 사용하면 두개의 가변 포인트를 모두 동기화 해주어야 하기 때문에 이렇게 사용해서는 안됩니다.
data class의 copy()
data class Fruit(
val name: String,
val price: Int
)
fun main() {
val banana = Fruit("banana", 500)
val strawberry = banana.copy(name = "strawberry")
println("banana ${banana.hashCode()} $banana")
println("strawberry ${strawberry.hashCode()} $strawberry")
//banana -337338577 Fruit(name=banana, price=500)
//strawberry 986991237 Fruit(name=strawberry, price=500)
}
data class의 copy를 통해서 기존 객체의 값을 변경하지 않고 프로퍼티를 변경하여 새로운 값을 할당한 객체를 받아 불변성을 유지할 수 있습니다.
퀴즈
List와 MutableList 차이는?
Kotlin은 많은 변수들을 Mutable 여부로 나누고 있다는 것을 잊지맙시다.
List Collcection에서도 List와 MutableList의 차이는 Read-Only인지 아닌지의 차이입니다.
그러나, Compile Time에는 둘다 List로써 인식됩니다. 이는 MutableList가 List를 상속하여 만들어졌기 때문입니다.
결국 둘 다 컴파일 결과로 java.util.List 이 됩니다.
public interface MutableList<E> : List<E>, MutableCollection<E> {
...
}
MutabeList와 ArrayList 차이는?
MutableList와 ArrayList의 차이는 사실 클래스명 그대로의 차이입니다.
MutableList = 추가, 삭제 등 수정이 가능한 리스트
ArrayList = 내부에 Array로 구현되어 있는 추가, 삭제 등 수정이 가능한 리스트
즉, ArrayList는 MutableList 중에서도 ArrayList를 사용하겠다 라고 명시한 것이고
MutableList는 Mutable한 List를 사용하겠다 라는 의미입니다.
그러나 현재 Kotlin에서는 mutableList나 arrayListOf나 둘 다 ArrayList를 반환합니다.
/**
* Returns an empty new [MutableList].
* @sample samples.collections.Collections.Lists.emptyMutableList
*/
@SinceKotlin("1.1")
@kotlin.internal.InlineOnly
public inline fun <T> mutableListOf(): MutableList<T> = ArrayList()
/**
* Returns an empty new [ArrayList].
* @sample samples.collections.Collections.Lists.emptyArrayList
*/
@SinceKotlin("1.1")
@kotlin.internal.InlineOnly
public inline fun <T> arrayListOf(): ArrayList<T> = ArrayList()
아무래도 미래를 위해서(?) ArrayList말고도 또다른 Mutable한 List이 생길 수 있으니, 이 때 형변환의 자유로움을
위하여 상위 클래스로 만들어 놓은게 아닐까 추측해봅니다.
마무리
불변성이 항상 장점만 가진다고 생각하지는 않습니다. 가변을 피하기 위해 새로운 객체를 생성하는 것은 비용 증가로 이어지기 때문에 불필요한 인스턴스화나 잦은 복사는 오버헤드로 이어질 수 도있다고 생각합니다. 하지만 불변성이 가지는 장점들이 가변성의 많은 단점들을 해소하기 때문에 코틀린에서도 불변성을 권장했을겁니다.
긴 글 읽어주셔서 감사합니다. 🙇♂️
참고자료
[Kotlin] 불변성(Immutability)과 가변성(Mutability)
함수형 프로그래밍에서는 불변성을 중요하게 생각합니다. 코틀린은 함수형 프로그래밍을 지원하는 언어로 불변성을 강제하지않고 가변을 허용하지만 불변성을 권장하고 있습니다. 불변성(Immut
velog.io
https://readystory.tistory.com/105
코틀린(kotlin)과 불변성(immutability)
불변성(immutability)은 함수형 프로그래밍에서 가장 중요한 부분입니다. 왜 중요한 것일까요? 불변성이란 무엇을 의미할까요? 코틀린에서 불변성을 어떻게 구현할 수 있을까요? 이번 포스팅에서는
readystory.tistory.com
https://zladnrms.tistory.com/140
[Android/kotlin] List vs MutableList vs ArrayList vs LinkedList
📌 List vs MutableList vs LinkedList vs ArrayList 🐳 List와 MutableList의 차이 Kotlin은 많은 변수들을 Mutable 여부로 나누고 있다는 것을 잊지말자. List Collcection에서도 List와 MutableList의 차이는 Read-Only인지 아
zladnrms.tistory.com
https://charliezip.tistory.com/27
[Kotlin] var vs val, immutable vs mutable 차이
가변성과 읽기 전용 프로퍼티 코틀린을 처음 공부하다보니 가변성, 읽기 전용 프로퍼티라는 키워드를 많이 보게되는데 계속 보다보니 점점 더 헷갈려서 한번 정리를 해보려고 합니다. 이번 글
charliezip.tistory.com
'Kotlin' 카테고리의 다른 글
[Kotlin] 확장(Extension) 함수 (5) | 2024.11.06 |
---|---|
[Kotlin] 코틀린에서의 Null 처리 방법 (4) | 2024.11.05 |
[Kotlin] 컬렉션(Collection) 함수 (9) | 2024.11.04 |
[Kotlin] by란? (0) | 2024.11.02 |
[Kotlin] open class와 abstract class (0) | 2024.11.02 |