반응형

불변성(immutability)은 함수형 프로그래밍에서 가장 중요한 부분입니다.
왜 중요한 것일까요? 불변성이란 무엇을 의미할까요? 코틀린에서 불변성을 어떻게 구현할 수 있을까요?
이번 포스팅에서는 이 세 가지 질문에 답해보도록 하겠습니다.

불변성(Immutability)이란?

본질적으로 함수형 프로그래밍은 스레드 안전(thread-safe)입니다. 그리고 불변성은 스레드를 안전하게 만드는 데 큰 역할을 합니다. 사전적인 정의로 불변성은 무언가가 변할 수 없다는 것을 의미합니다. 따라서 불변 변수는 변경될 수 없는 변수를 말합니다.

주의해야할 점은 불변성을 클래스를 생성하고 모든 변수를 읽기 전용으로 만드는 것 정도로 생각하면 안 됩니다. Clojure, Haskell, F# 등과는 달리 코틀린은 불변성이 강제되는 순수 함수형 프로그래밍 언어가 아닙니다. 코틀린은 함수평 프로그래밍과 객체지향 프로그래밍(OOP) 언어의 조화가 이루어진 언어입니다. 즉, 이 두 패러다임의 주요 이점을 모두 가집니다. 코틀린은 불변성을 강요하는 대신 권장하며, 가능하면 자동으로 불변을 제공하려 합니다.

불변성의 장점

불변성이 가져오는 장점은 아래와 같습니다.

  • 스레드 안전성(Thread-safe)
  • 낮은 커플링(Loosely-coupling)
  • 참조 투명성(Reference transparency)
  • 컴파일러 최적화
  • 순수 함수

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 사용 가능 클래스/오브젝트의 최상위 멤버여야만 함
델리게이트 작성 가능 델리게이트 작성 불가능
모든 타입에 대한 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을 통해 제공하지만 융통성이 부족해 실제로는 잘 사용하지 않습니다.

 

반응형
반응형