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)
}
- 상태를 적절히 관리하는 것이 생각보다 꽤 어려움
- 프로그램을 이해하고 디버그 하기 힘듬
- 상태 변경이 많을 수록 추적하기 어려움
- 가변성이 있으면, 코드의 실행을 추론하기 어려움
- 시점에 따라 값이 달라짐
- 멀티스레드 프로그래밍일 때는 적절한 동기화가 필요
- 변경이 일어나는 모든 부분에서 충동 가능성 있음
- 테스트하기 어려움
- 모든 상태를 테스트해야 함
- 상태 변경이 일어날 때, 변경을 다른 부분에 알려야 하는 경우 있음
- 프로그램을 이해하고 디버그 하기 힘듬
- 공유 상태를 관리하는 예제
- 충동 문제가 발생
- 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 객체를 사용하면 장점이 있음
- 한 번 정의된 상태가 유지되므로, 코드를 이해하기 쉬움
- immutable 객체는 공유했을 때도 충돌이 따로 이루어지지 않으므로, 병렬 처리를 안전하게 할 수 있다.
- immutable 객체에 대한 참조는 변경되지 않으므로, 쉽게 캐시할 수 있다.
- immutable 객체는 방어적 복사본을 만들 필요가 없다
- 깊은 복사를 따로 하지 않아도 된다
- immutable 객체는 다른 객체를 만들 때 활용하기 좋음
- immutable 객체는 Set, Map의 키로 사용가능
- 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
'코틀린 스터디 > 이펙티브 코틀린' 카테고리의 다른 글
아이템6) 사용자 정의 오류보다는 표준 오류를 사용하라 (0) | 2024.02.16 |
---|---|
아이템5) 예외를 활용해 코드에 제한을 걸어라 (0) | 2024.02.14 |
아이템4) inferred 타입으로 리턴하지 말라 (0) | 2024.02.08 |
아이템3) 최대한 플랫폼 타입을 사용하지 말라 (0) | 2024.02.07 |
아이템2) 변수의 스코프를 최소화하라 (0) | 2024.02.06 |