[Kotlin] 불변성(Immutability)과 가변성(Mutability)

케니스·2023년 1월 30일
0

Kotlin

목록 보기
4/5

불변성(Immutability)과 가변성(Mutability)

불변성이란 무엇일까?

함수형 프로그래밍에서는 불변성을 중요하게 생각합니다. 코틀린은 함수형 프로그래밍을 지원하는 언어로 불변성을 강제하지않고 가변을 허용하지만 불변성을 권장하고 있습니다.

불변성(Immutability)이란 함수형 프로그램에서 중요하게 다루는 부분으로 보통의 의미는 상태를 변경하지 않는 것으로 정의됩니다.

그렇다면 상태를 변경하는 것은 프로그램의 변수를 변경하거나 재할당하는 행위라고 볼 수 있지만 더 근본적으로는 컴퓨터에 저장된 메모리의 특정 공간에 저장된 값을 변경하는 행위를 의미합니다. 이런 행위는 어떤 문제가 생기길래 코틀린에서 불변성을 권장할까요?


가변성(Mutability)의 문제

불변성과 반대로 가변성은 상태를 가지는 경우를 얘기합니다. 만약 상태를 가지면 어떤 문제점들이 발생할까요?

앞서 상태를 변경하는 행위는 메모리의 저장된 값을 변경하는 행위라고 언급했습니다. 이렇게 메모리에 저장된 하나의 값을 누구든지 변경할 수 있다는 것은 무분별한 상태가 변경이 된다는 것을 의미합니다. 무분별한 상태가 변경이 되는 것은 다음과 같은 문제를 발생 시킬 수 있습니다.

  • 멀티스레드에서 값을 보장하지 못함
  • 값의 예측이 어렵고 변경에 있어서 위험하다
  • 테스트와 디버깅이 어려움
  • 상태 변경 발생 시 처리를 해주어야함

불변성(Immutability)을 지켜야 하는 이유

불변성이란 값이나 상태를 변경할 수 없는 것으로 정의됩니다. 불변 객체는 생성 시점 이후 한 번 정의된 상태는 계속 유지하며 변경되지 않으므로 스레드 간 안전성이 보장되며 이를 통해 동기화 문제를 해결할 수 있습니다. 그리고 한 번 생성한 값은 변경되지 않으므로 캐시도 수월합니다. 또한 기존 객체에서 프로퍼티가 변경된 객체를 리턴 받고자 할 때 방어적 복사본을 작성하지 않아도됩니다.

  • 스레드 안전성(thread-safe)
  • 캐시가 쉬움
  • 방어적 복사본이나 깊은 복사를 하지 않아도됨
  • 사이드 이펙트를 줄임

Kotlin에서는 가변성을 어떻게 제한하고 있을까?

코틀린에서는 크게 3가지로 가변성을 제한하고 있습니다.

  • 읽기 전용 프로퍼티 val
  • Mutable 컬렉션과 read-only 컬렉션 구분
  • data class의 copy()

Kotlin에서 불변 객체 사용과 데이터의 변경

코틀린에서는 value를 의미하는 읽기 전용 프로퍼티인 val 를 이용해 불변 변수를 사용할 수 있습니다.

class Play {
	val count = 0
  val countStr: String = "Count"
  	get() {
      return "$field ${++count}"
    }

	fun addedCount() {
    println("Added Count: $countStr")
  }
  
  // "Added Count: Count 0"
	// "Added Count: Count 1"
}

그치만 val는 완전한 불변이아닌 불변성에 가깝습니다. 그 이유는 countStr 변수에 backing field를 사용하여 호출할 때 마다 변경된 값을 리턴할 수 있기 때문입니다. 완전한 불변성으로 강제하는 방법은 const val를 사용하여 컴파일 타임 상수를 가지는 것입니다.


코틀린에서 불변성의 종류는 참조 불변성, 불변 값 두 가지가 존재합니다. 불변 값은 const val을 통해 제공합니다.

참조 불변성 같은 경우 val를 제공하여 참조가 할당되었을 때 다시 할당할 수 없게 합니다. Collection의 MutableList를 예로 들어본다면

fun main() {
  val list = mutableListOf(1, 2, 3)
  println(list) // [1, 2, 3]
  list.add(4) 
  println(list) // [1, 2, 3, 4]
  
  val anotherList = mutableListOf(4, 5, 6)
  list = anotherList // 컴파일 에러
}

참조 불변성에 의하면 list는 값이 변경되어야 하지 않아야할텐데 변경이 일어났습니다. 이는 val 속성이 불변 참조를 하므로 실제 List의 인스턴스는 변하지 않았고 MutableList의 내부의 값만 변경되었기 때문에 에러가 발생하지 않습니다.

또한 코틀린은 컬렉션에 대해서 읽기 전용(read-only)와 가변 컬렉션을 엄격하게 구분하고 있습니다.

  • 읽기 전용 : Iterable, Collection, List, Set 인터페이스
  • Mutable : MutableIterable, MutableCollection, MutableSet, MutableList 인터페이스

읽기 전용은 내부에서 값을 변경하는 함수들을 제공하지 않습니다. 만약 데이터를 추가, 삭제, 수정하려는 경우에는 toMutableList() 함수를 이용해서 요소들을 변경 가능한 컬렉션으로 변경하여 사용해야합니다. 만약 list as MutableList와 같이 다운캐스팅을 시도 한다면 코틀린에서 정한 읽기전용 규칙을 무시하기 때문에 이러한 행위는 지양해야 합니다.

또한 코틀린에서 컬렉션을 다룰 때 var와 함께 Mutable Collection을 사용하면 두개의 가변 포인트를 모두 동기화 해주어야 하기 때문에 이렇게 사용해서는 안됩니다.


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를 통해서 기존 객체의 값을 변경하지 않고 프로퍼티를 변경하여 새로운 값을 할당한 객체를 받아 불변성을 유지할 수 있습니다.


마무리

불변성이 항상 장점만 가진다고 생각하지는 않습니다. 가변을 피하기 위해 새로운 객체를 생성하는 것은 비용 증가로 이어지기 때문에 불필요한 인스턴스화나 잦은 복사는 오버헤드로 이어질 수 도있다고 생각합니다. 하지만 불변성이 가지는 장점들이 가변성의 많은 단점들을 해소하기 때문에 코틀린에서도 불변성을 권장했을겁니다.

긴 글 읽어주셔서 감사합니다. 🙇‍♂️


참고

profile
노력하는 개발자입니다.

0개의 댓글