관리 메뉴

나만을 위한 블로그

[이펙티브 코틀린] 아이템 1. 가변성을 제한하라 본문

책/Effective Kotlin

[이펙티브 코틀린] 아이템 1. 가변성을 제한하라

참깨빵위에참깨빵_ 2022. 5. 7. 14:33
728x90
반응형

모듈 : 클래스, 객체, 함수, 타입 별칭(typealias), Top-level property 등 여러 요소로 구성됨

 

타입 별칭?

https://kotlinlang.org/docs/type-aliases.html

 

Type aliases | Kotlin

 

kotlinlang.org

 

코틀린은 모듈로 프로그램을 설계한다.

 

요소 중 일부는 상태(state)를 가질 수 있다. 읽고 쓸 수 있는 var 프로퍼티를 쓰거나 mutable 객체를 쓰면 상태를 가질 수 있다.

상태를 갖게 하는 건 양날의 검이다. 시간 변화에 따라 변하는 요소를 표현할 수 있다는 건 유용하지만 상태를 적절하게 관리하는 게 어렵다.

 

  1. 프로그램 이해, 디버그가 힘들어짐 : 상태 변경이 많아지면 추적이 힘들다 -> 이런 클래스는 이해하기 어렵고 코드 수정하기도 힘들다 -> 클래스가 예상못한 상황 or 오류를 발생시키는 경우 문제가 된다
  2. 가변성(mutability)이 있으면 코드 실행 추론이 어렵다 : 현재 어떤 값을 갖고 있는지 알아야 코드 실행을 예측할 수 있다. 또한 한 시점에 확인한 값이 계속 동일하게 유지된다고 확신할 수 없다
  3. 멀티쓰레드 프로그램일 때는 적절한 동기화가 필요하다
  4. 테스트가 어렵다
  5. 상태 변경이 일어날 때 이런 변경을 다른 부분에 알려야 하는 경우가 있다

 

가변성은 시스템의 상태를 나타내기 위한 중요한 방법이다. 하지만 변경이 일어나야 하는 부분을 신중하고 확실하게 결정하고 사용해야 한다.

 

코틀린에서 가변성 제한하기


코틀린은 가변성을 제한할 수 있게 설계됐다 -> 불변 객체 생성, 프로퍼티 변경을 막는 것이 쉽다. 이걸 위한 방법이 많은데 그 중 가장 많이 쓰이는 중요한 것들

 

  • 읽기 전용 프로퍼티 val
  • 가변 컬렉션, 읽기 전용 컬렉션 구분하기
  • data class의 copy()

 

읽기 전용 프로퍼티 val

 

val을 써서 읽기 전용 프로퍼티를 만들 수 있다. 값(value)처럼 동작하고 일반적인 방법으론 값이 변하지 않는다. 읽고 쓸 수 있는 프로퍼티는 var로 만든다.

 

읽기 전용 프로퍼티가 mutable 객체를 담고 있다면 내부적으로 변할 수 있다.

 

fun main() {
    val list = mutableListOf(1, 2, 3)
    list.add(4)
    print(list)
}

 

  • 읽기 전용 프로퍼티가 완전히 변경 불가능한 것은 아니다
  • 읽기 전용 프로퍼티는 다른 프로퍼티를 활용하는 커스텀 getter로도 정의 가능하다. var 프로퍼티를 쓰는 val 프로퍼티는 var 프로퍼티가 변할 때 변할 수 있다

 

코틀린 프로퍼티는 기본적으로 캡슐화되어 있고 사용자 정의 접근자(getter, setter)를 가질 수 있다 -> API 변경, 정의 시 유연하다

 

  • var : getter, setter 모두 제공
  • val : 변경할 수 없기 때문에 getter만 제공 -> var로 오버라이드 가능

 

val의 값은 바뀔 수 있지만 프로퍼티 레퍼런스 자체를 바꿀 수는 없어서 동기화 문제 등을 줄일 수 있다. 그래서 일반적으로 val을 많이 쓴다.

val은 읽기 전용 프로퍼티지만 불변을 의미하진 않는다. 완전히 변경할 필요가 없으면 final 프로퍼티를 쓰는 게 좋다.

 

가변 컬렉션과 읽기 전용 컬렉션 구분하기

 

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

Mutable이 붙은 인터페이스는 대응되는 읽기 전용 인터페이스를 상속받아서 변경을 위한 메서드를 추가한 것이다.

 

읽기 전용 컬렉션이 내부 값을 변경할 수 있다는 의미는 아니다. 대부분 변경 가능하지만 읽기 전용 인터페이스가 이를 지원하지 않아서 변경할 수 없다.

 

List를 읽기 전용으로 리턴하면 이걸 읽기 전용으로만 써야 한다. 읽기 전용에서 mutable로 바꿔야 한다면 복제(copy())를 통해서 새 mutable 컬렉션을 만드는 list.toMutableList()를 써야 한다.

 

fun main() {
    val list = listOf(1, 2, 3)
//    list.add(4)   // 컴파일 에러 발생
    val mutableList = list.toMutableList()
    mutableList.add(4)
}

 

data class의 copy()

 

String, Int처럼 내부적인 상태를 바꾸지 않는 불변 객체를 많이 쓰는 데는 이유가 있다. 불변 객체를 쓰면 아래의 장점이 있다.

 

  1. 한 번 정의된 상태가 유지됨 -> 코드 이해가 쉬움
  2. 불변 객체는 공유했을 때도 충돌이 따로 이뤄지지 않음 -> 안전한 병렬 처리 가능
  3. 불변 객체에 대한 참조는 변경되지 않음 -> 캐시 쉬움
  4. 불변 객체는 방어적 복사본(defensive copy)을 만들 필요가 없다. 또한 객체 복사 시 깊은 복사를 따로 하지 않아도 된다.
  5. 불변 객체는 다른 객체를 만들 때 활용하기 좋다. 또한 실행을 더 쉽게 예측할 수 있다.
  6. 불변 객체는 Set 또는 Map의 key로 쓸 수 있다. Set, Map이 내부적으로 해시 테이블을 쓰고 해시 테이블은 첫 요소를 넣을 때 요소의 값을 기반으로 버킷을 결정하기 때문이다. 수정이 발생하면 해시 테이블 안에서 요소를 못 찾게 된다

 

  • mutable 객체 : 예측하기 어렵고 위험하다
  • immutable 객체 : 변경할 수 없다 -> 자신의 일부를 수정한 새 객체를 만드는 메서드를 가져야 한다
ex) Int는 불변이다 -> 내부적으로 plus(), minus()로 자신을 수정한 새로운 Int를 리턴할 수 있다
ex2) Iterable은 불변이다 -> map(), filter()로 자신을 수정한 새로운 Iterable 객체를 만들어 리턴한다

 

예를 들어 User 불변 객체가 있고 성(surName)을 바꿔야 한다면 withSurname()같은 메서드를 제공해서 자신을 수정한 새로운 객체를 만들 수 있게 해야 한다.

 

fun main() {
    data class User(
        val name: String,
        val surName: String
    ) {
        fun withSurname(surName: String) = User(name, surName)
    }

    var user = User("Maja", "Markiewicz")
    user = user.withSurname("Moskala")
    print(user)
}

 

모든 프로퍼티를 대상으로 이런 함수를 만드는 건 귀찮다. 이 때 data 한정자를 사용한다. data 한정자는 copy()를 만들어주는데 이걸 쓰면 모든 기본 생성자 프로퍼티가 같은 새로운 객체를 만들 수 있다.

 

fun main() {
    data class User(
        val name: String,
        val surName: String
    )

    var user = User("Maja", "Markiewicz")
    user = user.copy(surName = "Moskala")
    print(user)
}

 

변경할 수 있다는 측면만 보면 mutable 객체가 더 좋아 보이지만, 데이터 모델 클래스를 만들어 immutable(불변) 객체로 만드는 게 더 많은 장점을 가지기 때문에 기본적으로 이렇게 만드는 게 좋다.

 

다른 종류의 변경 가능한 지점

 

변경 가능한 리스트를 만들어야 할 때 2가지 선택지가 있다.

 

  • mutable collection 생성
  • var로 읽고 쓸 수 있는 프로퍼티 생성

 

둘 다 변경 가능하지만 그 방법이 다르고 실질적으로 이뤄지는 처리가 다르다.

mutable list 대신 mutable 프로퍼티를 쓰는 형태는 사용자 정의 setter(또는 이걸 쓰는 delegate)를 써서 변경을 추적할 수 있다.

최악의 방식은 프로퍼티, 컬렉션을 모두 변경 가능한 지점으로 만드는 것이다.

 

// 이렇게 하지 마라
val list3 = mutableListOf<Int>()

 

변경 가능 지점 노출하지 말기

 

상태를 나타내는 mutable 객체를 외부에 노출하는 건 굉장히 위험하다. 돌발적인 수정이 일어날 때 위험할 수 있다.

이걸 처리하는 방법은 2가지다.

 

  • 리턴되는 mutable 객체를 복제하는 것 = 방어적 복제(defensive copying)
  • 가능하다면 무조건 가변성을 제한한다. 컬렉션은 객체를 읽기 전용 슈퍼타입으로 업캐스트해서 가변성을 제한할 수도 있다
반응형
Comments