Better Kotlin — 가변성 제약

Jaeho Choe
11 min readJan 27, 2021

--

Summary
1. 가급적 val , immutable-collections 을 사용하세요.
2.
varval 의 커스텀 getter를 사용하면 변경 가능한 것과 같은 val 프로퍼티를 표현 할 수 있습니다.
3. Collection 을 Mutable Collection 으로 Down-casting 하는것은 위험합니다. 필요한 경우엔
toMutableList() 와 같은 함수를 사용하세요.
4. 변경 가능한 Collection은
val + Mutable Collection 그리고 var + Immutable Collection 이 있습니다. var + Mutable Collection 은 가급적 사용하지 마세요.
5. 많은 속성을 지닌 불변 클래스를 계속 생성하는 것이 힘들다면 data class의
copy() 를 고려해보세요.

삶은 mutable 을, 코드는 immutable 을 지향하고 있습니다

Mutability의 위협

Kotlin의 프로퍼티는 value를 의미하는 val 과 variable을 의미하는 var 로 선언할 수 있습니다. 그리고 아시다시피 val은 read-only 값으로 그 값을 변경할 수 없고 var는 읽고 쓰는 것이 가능합니다. var 를 사용하면 상태가 변하는 것을 표현하는 것에 있어서 val 보다 수월합니다. 예를 들어 신체 사이즈를 표현한 class 를 살펴봅시다.

class Body {
var weight = 70
fun exercise() {
weight -= 10
}
fun eat() {
weight += 10
}
}
val me = Body()
println(me.weight) // 70
me.exercise()
println(me.weight) // 60
me.eat()
println(me.weight) // 70

Body 클래스는 몸무게가 얼마인지를 표현하는 상태를 갖고 있습니다. 몸무게를 나타내는 weightvar로 선언하여 시간이 지남에 따라서 변하는 몸무게를 표현할 수 있는 점은 굉장히 유용합니다. 하지만 시시각각 변하는 상태를 관리하는 것은 쉬운 일이 아닙니다. 그 이유는 다음과 같습니다.

  1. 변경되는 값이 많은 코드는 디버깅하거나 이해하기 어렵습니다. 변경되는포인트를 이해해야하고 변경 되는 값이 많은 만큼 값이 변경 되었을때 그 값을 추적하는 것이 어렵기 때문입니다.
  2. 멀티 쓰레드 환경에서 적절한 동기화가 필요합니다. 모든 Mutable 값들은 멀티쓰레드 환경에서 잠재적인 충돌 가능성을 가지고 있습니다.
  3. Mutability는 테스트를 어렵게 만듭니다. 가능한 모든 상태에 대해 테스트를 해야할 필요가 있기 때문에 변경 가능한 상태가 많을 수록 테스트 해야하는 상태도 많아집니다.

Haskell 과 같은 실험적인 언어는 가변성이 갖는 단점 때문에 가변성을 전혀 허용하지 않습니다. 이런 언어는 가변성을 전혀 허용하지 않는 불편함때문에 실제로 거의 사용되지 않습니다. 실제로 상용 서비스를 만들때 상태 변경은 필수 불가결한 존재입니다. 다만 꼭 필요한 곳에 꼭 필요할 때만 신중하고 현명하게 적용해야 합니다. 다행히도 Kotlin은 다양한 방법으로 가변성을 제한할 수 있습니다.

val 을 사용한 프로퍼티 선언

앞서 말했듯 val 로 선언한 프로퍼티는 Java의 final 과 같이 한 번 설정된 값을 변경 할 수 없습니다. 하지만 val 값을 마치 변하는 값과 같이 사용하는 방법도 있습니다.

val me = Body()
me.weight = 10
println(me.weight) // 10

val 로 선언된 me의 값은 동일하지만 me 의 속성은 변경 되었습니다. 참조하고 있는 값이 변경 되진 않았지만 그 값의 속성이 변경된 경우입니다. 이런 개념을 조금 확장해서 커스텀 getter에 반영해봅시다. val 프로퍼티의 커스텀 getter는 프로퍼티가 호출될때마다 동작합니다. 이때 다른 var 프로퍼티를 사용 한다면 그 값은 가변적으로 변할 수 있습니다.

var name: String? = "Jaeho"
var weight: Int = 65
val hisWeight
get() = "$name: $weight"
fun main() {
println(hisWeight) // "Jaeho: 65"
weight = 70
println(hisWeight) // "Jaeho: 70"
}

이런 Kotlin의 캡슐화는 코드에 유연성을 제공해주고 더 많은 자유를 주지만 실제 호출될 타이밍이 되서야 값을 추론하는 것이 가능하므로 smart cast 의 대상이 되지 못합니다. 따라서 가능한 최종 값으로 val 을 사용하는 것이 좋습니다.

살펴본 바와 같이val 프로퍼티의 값도 결과적으로 변경 될 수 있지만 val 프로퍼티는 일반적으로 동기화나 타입 추론의 문제에서 비교적 자유롭기 때문에 var 보다 권장됩니다.

Read-only Collection 사용

Kotlin은 수정 가능한 MutableCollection과 더불어 값을 변경 할 수 없는 읽기 전용 Collection 을 지원합니다. 둘다 Iterable 인터페이스를 구현하고 있지만 MutableCollection은 추가적으로 add() remove() 등 함수를 정의하고 있는 것이 다릅니다. 읽기 전용 Collection을 사용하면 값의 가변성을 제한 할 수 있습니다.

val list = listOf<String>("Kotlin", "is")
list.add("awesome") // Error! Unresolved reference: add
val mutableList= mutableListOf<String>("Kotlin", "is")
mutableList.add("awesome")
println(mutableList) // ["Kotlin", "is", "awesome"]

이런 Collection 디자인은 Kotlin에서 매우 중요한 부분입니다. Kotlin은 JVM 뿐 아니라 다양한 플랫폼에서 동작하도록 설계 되었습니다. 이때 Mutable, Immutable한 Collection이 존재한다는 것은 Kotlin이 동작하는 특정 플랫폼의 어떤 Collection이든 사용할 수 있도록 더 많은 자유를 줍니다. 다만 이 경우 down-casting 을 주의해야합니다. 구체적으로 어떤 경우인지는 다음 코드를 같이 살펴 봅시다.

val list = listOf<String>("Kotlin", "is")if(list is MutableList) {
list.add("awesome")
}

이 코드의 listOf() 결과는 위에 설명했던대로 플랫폼 마다 다릅니다. 그 중 우리가 가장 많이 접하게 될 JVM 에서 listOf()Arrays.ArrayList 를 반환합니다. (java.util.ArrayLis 가 아닙니다!) Arrays.ArrayListCollection 의 구현체이기 때문에 add()set() 과 같은 함수를 포함하고 있기 때문에 Kotlin의 MutableList 로 간주됩니다. 하지만 실제로 Arrays.ArrayListadd() 함수를 구현하지 않았기 때문에 위와 같은 코드를 작성한다면 list.add()를 호출 할때 UnsupportedOperationException 이 발생하게 됩니다. 따라서 위와 같이 읽기 전용 Collction을 변경 가능한 Collection으로 down-casting 해서는 안됩니다. 만약 필요하다면 읽기 전용 Collection의 toMutableList() 와 같은 함수를 사용하여 수정할 수 있는 사본을 만들어 사용해야 합니다.

변경 가능한 객체들은 불변 객체에 비해 더 위험하고 예측하기 어렵기 때문에 잠재적인 위험이라고 볼 수 있습니다. 가능하면 불변객체를 이용해야겠지만 실제로 프로그램을 작성할땐 데이터가 변경되어야할 필요가 있습니다. 지금부터는 값을 변경하면서도 가급적 안전하게 데이터를 보호 할 수 있는 방법에 대해 알아봅시다.

값이 변경되야 하는 리스트가 필요하다면?

그런 경우에는 다음 두가지 방법중 하나를 골라봅시다. 하나는 val 로 선언된 Mutable Collection 을 사용하는 것, 다른 하나는 var 로 선언된 Read-Only Collection을 사용하는 방법입니다. 아래 코드를 참고하세요.

val list1: MutableList<String> = mutableListOf()
var list2: List<String> = listOf()
list1.add("kotlin")
list2 = list2 + "is good"
println(list1) // ["kotlin"]
println(list2) // ["is good"]

위 코드의 list1과 list2는 모두 새로운 값으로 변경되었습니다. 다만 값이 반영되는 과정은 조금 다른데요, 두 방식 모두 원하는 결과를 얻을 수 있지만 동작 하는 방식이 다른 만큼 각각의 장단점이 있습니다.

  • Mutable Collection:

Collection 내부에 가변 포인트가 존재합니다. 따라서 Collection 이 제공하는 동기화 기능에 의존할 수 있지만 멀티 스레드 환경에서 값을 보장해줄만큼 안전하진 않습니다.

  • Read-Only Collection with var :

동기화를 직접 구현해야하지만 변경 지점이 뚜렷하여 전자의 방법보다 보안에 유리합니다. 하지만 적절한 동기화를 구현하지 않는다면 역시나 멀티 스레드 환경에서 위험할 수 있습니다.
프로퍼티 setter 를 지정하거나 delegate를 구현하여 값이 변경되는 것을 추적할 수 있습니다. 아래 코드를 참고해 주세요.

var words by Delegates.observable(listOf<String>()) { _, o, n ->
println("Changed $o to $n")
}
words = words + "kotlin"
words = words + "is"
words = words + "awesome"
// output
"Changed [] to [kotlin]"
"Changed [kotlin] to [kotlin, is]"
"Changed [kotlin, is] to [kotlin, is, awesome]"

값을 변경 할 수 있는 Collection을 사용해야한다면 두 가지 방법중 원하는 하나를 선택하세요. 다만 단 한가지, var 를 사용한 Mutable Collection 은 피해야합니다. 가변 값인 변경 가능한 Collection은 프로퍼티 값의 변화와 Collection 내부 값의 변화 두 가지 가변 포인트를 모두 동기화해야하기 때문이죠. 거기에 컴파일러가 값을 추론하는 것도 어려워 += 연산자도 사용할 수 없습니다.

Data Class 의 copy() 함수 사용

불변객체를 사용하며 데이터를 변경해야할 경우 해결책은 새로운 객체 생성이 될 수 있습니다. 예를 들어 불변 객체 Int의 plus()minus() 함수는 각각의 Int를 계산하고 새로운 Int 객체를 반환하는 함수를 가지고 있습니다. CollectiontoMutableList() 와 같은 함수도 변경가능한 새로운 컬렉션을 반환합니다. 이렇듯 데이터를 변경하여 새로운 객체를 생성하는 함수를 작성하면 불변객체의 데이터를 변경하는 것과 같은 효과를 가질 수 있습니다. 하지만 객체가 가진 속성이 많고 각각의 함수를 모두 작성하는 일은 번거롭고 효율적이지 않습니다. 이럴때 우린 data class 를 사용할 수 있습니다. 데이터 클래스의 copy() 함수는 기존의 객체에서 변경하고 싶은 속성만 지정하여 새로운 객체를 생성할 수 있습니다.

data class AppState(
val data1: String,
val data2: String,
val data3: Int
)
val state = AppState("Kotlin", "is", 100)
val changedState = state.copy(data3 = 1)
println(changedState) // AppState(data1=Kotlin, data2=is, data3=1)

data class의 copy() 를 사용하여 불변 객체를 활용하고 있는 예시는 이런 글이나 이런 코드에서도 확인할 수 있습니다.

Kotlin 에서 가변성을 제한하는 방법과 가변 포인트를 어떻게 제한해야 하는지에 대해서 간단하게 살펴봤습니다. 여러 내용이 있었지만 다른 내용은 잊어도 하나만 기억하시면 됩니다.

불필요한 가변성을 만들면 안됩니다. 가변성은 그 자체로 비용입니다.

물론 이런 규칙에는 예외가 있을 수 있습니다. 때로는 더 효율적이기 때문에 가변 객체를 사용합니다. 하지만 꼭 사용해야하는 경우에도 가변 객체를 외부로 노출하는것은 피해야하며 안전하게 사용하기 위해서는 보다 많은 주의를 기울여야 합니다.

참고 자료:
https://leanpub.com/effectivekotlin

--

--