불변성(immutability)은 함수형 프로그래밍에서 가장 중요한 부분이다. 불변성이 왜 중요하고 무엇을 의미하는지에 대해서 알아보자.
본질적으로 함수형 프로그래밍은 Thread-Safe
하다. 그리고 불변성은 스레드를 안전하게 만드는데 큰 역할을 한다. 사전적인 정의로 불변성은 무언가가 변할 수 없다는 것을 의미한다. 따라서 불변 변수는 변경될 수 없는 변수를 말한다.
주의해야 할 점은 불변성을 클래스를 생성하고 모든 변수를 읽기 전용으로 만드는 것 정도로 생각하면 안된다는 것이다. Clojure, Haskell, F# 등과는 달리 코틀린은 불변성이 강제되는 순수 함수형 프로그래밍 언어가 아니다.
코틀린은 함수형 프로그래밍과 객체지향 프로그래밍(OOP) 언어의 조화가 이루어진 언어이다. 즉, 이 두 패러다임의 주요 이점은 모두 가진다. 코틀린은 불변성을 강요하는 대신 권장하며, 가능하면 자동으로 불변을 제공하려고 한다.
코틀린은 불변성을 장려하지만, 개발자에게 선택권을 주기 위해 두 종류의 변수를 소개한다.
첫번째는 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 사용 가능 | 클래스/오브젝트의 최상위 멤버여야만 함 |
델리게이트 작성 가능 | 델리게이트 작성 불가능 |
모든 타입에 대한 val 속성 가능 | 기본 데이터 타입과 문자열만이 const val 속성 가능 |
nullable | non-nullable |
결과적으로 const val 속성은 값의 불변성은 보장하지만, 유연성에서 떨어진다. 또한, const val는 기본 타입과 문자열만 사용해야 하므로 제한이 있다.
기본적으로 불변성에는 2가지 타입이 있다.
참조 불변은 일단 참조가 할당되면 다른 것에 할당할 수 없게 하는 것이다. 대표적으로 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를 통해 제공하지만, 융통성이 부족해서 실제로는 잘 사용하지 않는다.