코틀린 스터디/이펙티브 코틀린

아이템1) 가변성을 제한하라

막이86 2024. 2. 5. 14:49
728x90

이펙티브 코루틴을 요약한 내용입니다.

  • 코틀린은 모듈로 프로그램을 설계
    • 클래스, 객체, 함수, 타입 별칭(type alias), 톱레벨(top-level) 프로퍼티 등 다양한 요소로 구성 됨
  • 읽고 쓸수 있는 프로퍼티 var를 사용하거나, mutable 객체를 사용하면 상태를 가질 수 있다
var a = 10
var list: MutableList<Int> = mutableListOf()
  • 계좌에 돈이 얼마나 있는지 나타내는 상태 예제
class BankAccount {
    var balance = 0.0
    	private set
    
    fun deposit(depositAmount: Double) {
        balance += depositAmount
    }
    
    @Throws(InsufficientFunds::class)
    fun withdraw(withdrawAmount: Double) {
        if (balance < withdrawAmount) {
            throw InsufficientFunds()
        }
        balance -= withdrawAmount
    }
    
}

class InsufficientFunds: Exception()

fun main() {
    var account = BankAccount()
    println(account.balance)
    account.deposit(100.0)
    println(account.balance)
    account.withdraw(50.0)
    println(account.balance)
}
  • 상태를 적절히 관리하는 것이 생각보다 꽤 어려움
    1. 프로그램을 이해하고 디버그 하기 힘듬
      1. 상태 변경이 많을 수록 추적하기 어려움
    2. 가변성이 있으면, 코드의 실행을 추론하기 어려움
      1. 시점에 따라 값이 달라짐
    3. 멀티스레드 프로그래밍일 때는 적절한 동기화가 필요
      1. 변경이 일어나는 모든 부분에서 충동 가능성 있음
    4. 테스트하기 어려움
      1. 모든 상태를 테스트해야 함
    5. 상태 변경이 일어날 때, 변경을 다른 부분에 알려야 하는 경우 있음
  • 공유 상태를 관리하는 예제
    • 충동 문제가 발생
    • import kotlin.concurrent.thread fun main() { var num = 0 for (i in 1..1000) { thread { thread { Thread.sleep(10) num += 1 } } } Thread.sleep(5000) print(num) }
    • 코루틴을 활용하면, 적은 스레드가 관여됨으로 문제가 줄어듬
      • 문제가 사라지는 것은 아님
      suspend fun main() {
          var num = 0
          coroutineScope {
              for (i in 1..1000) {
                  launch {
                      delay(10)
                      num += 1
                  }
              }
          }
          print(num)
      }
      
    • 동기화를 추가
suspend fun main() {
    var num = 0
    coroutineScope {
        for (i in 1..1000) {
            launch {
                delay(10)
                num += 1
            }
        }
    }
    print(num)
}

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

  • immutable 객체를 만들어 가변성을 제한할 수 있음
    • 읽기 전용 프로퍼티(val)
    • 가변 컬렉션과 읽기 전용 컬렉션 구분하기
    • 데이터 클래스의 copy

읽기 전용 프로퍼티(val)

  • val을 사용해 읽기 전용 프로퍼티 사용
  • 일반적인 방법으로는 값이 변하지 않음
  • 읽기 전용 프로퍼티가 mutable 객체를 담고 있다면 내부적으로 변경 가능
val list = mutableListOf(1,2,3)
list.add(4)

print(list)
  • 읽기 전용 프로퍼티는 다른 프로퍼티를 사용하는 사용자 정의 게터로 정의 가능
var name: String = "Marcin"
var surname: String = "Moskala"
val fullName
	get() = "$name $surname"

fun main() {
	println(fullName)
	name = "Maja"
	println(fullName)
}
  • 값을 추출할 때마다 사용자 정의 게터가 호출
fun calculate(): Int {
    print("Calculating...")
    return 42
}

val fizz = calculate()
val buzz
    get() = calculate()

fun main() {
    println(fizz) // Calculating...42
    println(fizz) // 42
    println(buzz) // Calculating...42
    println(buzz) // Calculating...42
}
  • var은 getter, setter 모두 제공
  • val은 getter만 제공
    • val을 var로 오버라이드 가능
    interface Element {
        val active: Boolean
    }
    
    class ActualElement: Element {
        override var active: Boolean = false
    }
    
  • val은 읽기 전용 프로퍼티지만, 변경할 수 없음을 의미하는 것은 아님
    • 완전히 변경할 필요가 없다면 final 프로퍼티를 사용하는 것이 좋음
  • val은 정의 옆에 상태가 바로 적히므로, 코드의 실행을 예측하는 것이 간단
    • 스마트 캐스트 등의 추가적인 기능 활용 가능
    • fullName은 getter로 정의했으므로 스마트 캐스트 불가능
    val name: String? = "Marton"
    val surname: String = "Braun"
    
    val fullName: String?
        get() = name?.let { "$it $surname" }
    
    val fullName2: String? = name?.let { "$it $surname" }
    
    fun main() {
        if (fullName != null) {
            println(fullName.length)   // 오류
        }
    
        if (fullName2 != null) {
            println(fullName2.length)
        }
    }
    

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

  • 코틀린은 읽고 쓸수 있는 컬렉션과 읽기 전용 컬렉션으로 구분
  • 읽기 전용 컬렉션
    • Iterable, Collection, Set, List
  • 읽고 쓰기 가능 컬렉션
    • MutableIterable, MutableCollection, MutableSet, MutableList
  • 읽기 전용 컬렉션이 내부의 값을 변경할 수 없다는 의미는 아님
    • 대부분의 경우 변경할 수 있음
    • 읽기 전용 인터페이스가 이를 지원하지 않으므로 변경할 수 없음
    • Iterable<T>.map과 Iterable<T>.filter 함수는 ArrayList를 리턴
      • ArrayList는 변경할 수 있는 리스트
      inline fun <T, R> Iterable<T>.map(transformation: (T) -> R): List<R> {
          val list = ArrayList<R>()
          for (elem in this) {
              list.add(transformation(elem))
          }
          return list
      }
      
  • 읽기 전용 컬렉션을 mutable 컬렉션으로 다운캐스팅하면 안됨
  • fun main() { val list = listOf(1, 2, 3) if (list is MutableList) { list.add(4) } } // 오류 발생
  • 읽기 전용에서 mutable로 변경 해야함 → list.toMutableList
  • fun main() { val list = listOf(1, 2, 3) val mutableList = list.toMutableList() mutableList.add(4) }

데이터 클래스의 copy

  • immutable 객체를 사용하면 장점이 있음
    1. 한 번 정의된 상태가 유지되므로, 코드를 이해하기 쉬움
    2. immutable 객체는 공유했을 때도 충돌이 따로 이루어지지 않으므로, 병렬 처리를 안전하게 할 수 있다.
    3. immutable 객체에 대한 참조는 변경되지 않으므로, 쉽게 캐시할 수 있다.
    4. immutable 객체는 방어적 복사본을 만들 필요가 없다
      1. 깊은 복사를 따로 하지 않아도 된다
    5. immutable 객체는 다른 객체를 만들 때 활용하기 좋음
    6. immutable 객체는 Set, Map의 키로 사용가능
      1. mutable 객체는 사용 불가
      class FullName(var name: String, var surname: String)
      
      fun main() {
          val names: SortedSet<FullName> = TreeSet()
          val person = FullName("AAA", "AAA")
          names.add(person)
          names.add(FullName("Jordan", "Hansen"))
          names.add(FullName("David", "Blanc"))
      
          println(names)
          println(person in names)
      
          person.name = "ZZZ"
          println(names)
          println(person in names)
      }
      // 동작 안하는데??
      
  • mutable 객체는 예측하기 어려우며 위험하다는 단점
  • immutable 객체는 변경할 수 없다는 단점
    • immutable 객체는 자신의 일부를 수정한 새로운 객체를 만들어 내는 메서드를 추가
    class User(val name: String, val surname: String) {
        fun withSurname(surname: String) = User(name, surname)
    }
    
    fun main() {
        var user = User("Maja", "Markiewicz")
        user = user.withSurname("Moskala")
        print(user)
    }
    
    // 출력이 원하는 데로 안나왐
    
  • data 한정자를 사용하면 copy라는 메서드를 만들어 줌
  • data class User(val name: String, val surname: String) fun main() { var user = User("Maja", "Markiewicz") user = user.copy(surname = "Moskala") print(user) }
  • immutable 객체로 만드는 것이 더 많은 장점을 가지므로 기본적으로 이렇게 만드는 것이 좋음

다른 종류의 변경 가능 지점

  • 변경할 수 있는 리스트를 만들 경우
    • var, val 프로퍼티를 만들 경우 로직이 달라질 수 있음
    • 내부 로직에 따라 동기화 처리 방법이 다를수 있음
    fun main() {
        val list1: MutableList<Int> = mutableListOf()
        var list2: List<Int> = listOf()
    
        list1.add(1)
        list2 = list2 + 1
    
        list1 += 2      // list1.plusAssign(2)
        list2 += 2      // list = list2.plus(2)
    
        println(list1)
        println(list2)
    }
    
  • mutable 리스트 대신 mutable 프로퍼티를 사용하는 형태는 사용자 정의 세터를 활용해서 변경을 추적할 수 있음
fun main() {
    var names by Delegates.observable(listOf<String>()) { _, old, new ->
        println("Names changed from $old to $new")
    }
    
    names += "Fabio"
		// Names changed from [] to [Fabio]
    names += "Bill"
		// Names changed from [Fabio] to [Fabio, Bill]
}
  • 최악의 방식은 프로퍼티와 컬렉션을 모두 변경 가능한 지점으로 만드는 방법
var list3 = mutableListOf<Int>()

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

  • 상태를 나타내는 mutable 객체를 외부에 노출하는 것은 굉장히 위험
data class User(val name: String)

class userRepository {
	private val storedUsers: MutableMap<Int, String> = mutableMapOf()

	fun loadAll(): MutableMap<Int, String> {
		return storedUsers
	}
}
  • loadAll을 사용해서 private 상태인 UserRepository를 수정할 수 있음
val userRepostitory = UserRepository()
val storedUsers = userRepository.loadAll()
storedUsers[4] = "Kirill"

print(userRepostiory.loadAll())
  • 리턴되는 객체를 복제하는 방법 (방어적 복제)
    • data 한정자로 만들어지는 copy 메서드를 활용하면 좋음
    class UserHolder {
    	private val user: MutableUser()
    
    	fun get(): MutableUser {
    		return user.copy()
    	}
    }
    
  • 가능하다면 무조건 가변성을 제한하는 것이 좋음
data class User(val name: String)

class userRepository {
	private val storedUsers: MutableMap<Int, String> = mutableMapOf()

	fun loadAll(): Map<Int, String> {
		return storedUsers
	}
}

정리

  • var 보다는 val을 사용하는 것이 좋음
  • mutable 프로퍼티보다는 immutable 프로퍼티를 사요앟는 것이 좋음
  • muutable 객체와 클래스보다는 immutable 객체와 클래스를 사용하는 것이 좋음
  • 변경이 필요한 대상을 만들어야 한다면, immutabe 데이터 클래스로 만들고 copy를 활요하는 것이 좋음
  • 컬렉션에 상태를 저장해야 한다면, mutable 컬렉션보다는 읽기 전용 컬렉션을 사용하는 것이 좋음
  • 변이 지점을 적절하게 설계하고 불필요한 변이 지점은 만들지 않는 것이 좋음
  • mutable 객체를 외부에 노출하지 않는 것이 좋음
728x90