코틀린은 모듈로 프로그램 설계
- 모듈은 클래스, 객체, 함수, 타입 별칭(type alias), 톱레벨(top-level) 프로퍼티 등 다한 요소로 구성
이러한 요소 중 일부는 상태(state)를 가질 수 있음
-> ex) 읽고 쓸수 있는 프로퍼티(var), mutable 객체를 사용하면 상태를 가짐
var a = 10 var list: MutableList<Int> = mutableListOf()
프로그램을 이해하고 디버그하기 힘듦
-> 상태를 갖는 부분들의 관계를 이해해야 하며, 상태 변경이 많아지면 이를 추적하기 힘들어짐
가변성(mutability)이 있으면, 코드의 실행 추론하기 어려워짐
-> 시점에 따라서 값이 달라질 수 있으므로, 현재 어떤 값을 갖고 있는지 알아야 코드의 실행을 예측 가능
멀티스레드 프로그램일 때는 적절한 동기화가 필요
테스트하기 어려움
-> 변경이 많으면 많을수록 많은 조합을 테스트해야 함
상태 변경이 일어날 때, 이러한 변경을 다른 부분에 알려야하는 경우가 존재
-> 정렬되어 있는 리스트에 가변 요소를 추가한다면, 요소에 변경이 일어날 때마다 리스트 전체를 정렬해야 함
var num = 10 for ( i in 1..1000) { thread { Thread.sleep(10) num += 1 } } Thread.sleep(5000) print(num) // 1000이 아닐 확률이 매우 높고, 실행할 때마다 다른 숫자가 나온다.
suspend fun main() { var num = 0 coroutinScope { for ( i in 1..1000) { launch { delay(10) num += 1 } } } print(num) // 실행할 때마다 다른 숫자가 나온다. }
↪ 실제로 이러한 코드 작성 XX
val lock = Any() var num = 0 for ( i in 1..1000) { thread { Thread.sleep(10) synchronized(lock) { num += 1 } } } Thread.sleep(1000) print(num) // 1000
val a = 10 a = 20 // 오류
읽기 전용 프로퍼티가 완전히 변경 불가능한 것은 ❌
읽기 전용 프로퍼티가 mutable 객체를 담고 있다면, 내부적 변화 ✔️
val list = mutableListOf(1,2,3) list.add(4) print(list) // [1,2,3,4]
- 읽기 전용 프로퍼티는 코드 뒤에 list = mutableListOf(4,5,6) 등을 추가해서 재항당하는 것이 불가능하는 것 뿐
var name: String = "Seung"
var surname: String = "Kim"
val fullName
get() = "$name $surname"
fun main() {
println(fullname) // Seung Kim
name = "Gim"
println(fullname) // Seing Gim
➕ var은 게터와 세터를 모두 제공하지만, val은 변경이 불가능하므로, 게터만 제공
-> val을 var로 오버라이드할 수 있음
interface Element { val active: Boolean } class ActualElement: Element { override var active: Boolean = false }
🛠️ Interface에 관해서 수정할 예정
읽기 전용 val의 값은 변경될 수 있기 하지만, 프로퍼티 레퍼런스 자체를 변경할 수는 없음
-동기화 문제 등을 줄일 수 있음
- 사용빈도 : var < val
val은 읽기 전용 프로퍼티지만, 변경할 수 없음(immutable)을 의미 ❌
-> 게터 or 델리게이트로 정의할 수 있음
변경할 필요가 없다면, final 프로퍼티를 사용하는 것이 좋음
val은 정의 옆에 상태가 바로 적힘 -> 실행 예측 간단
또한 스마트 캐스트(smart-cast)등의 추가적인 기능을 활용 가능
val name: String? = "Seung"
val surname: String? = "Kim"
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(fullName.length) // Seung Kim
}
}
↪ fullName은 게터로 정의했으므로 스마트 캐스트 할 수 없음 ❌
↪ fullName2처럼 지역 변수가 아닌 프로퍼티(non-local property)가 final이고, 사용자 정의 게터를 갖지 않을 경우 스마트 캐스트 할 수 있음 ✔️
왼쪽에 있는 인터페이스는 읽기 전용
오른쪽에 있는 인터페이스는 읽고 쓸 수 있음
mutable이 붙은 인터페이스는 대응되는 읽기 전용 인터페이스를 상속 받아서, 변경을 위한 메서드를 추가
ex) Iterable<T>.map 과 Iterable<T>.filter 함수는 ArrayList를 return
-> 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 }
✒️ inline + Iterable<T>에 대해서 작성할 예정
↪ Iterable<T>.map의 단순한 구현
코틀린이 내부적으로 immutable하지 않은 컬렉션을 외부적으로 immutable하게 보이게 만들어서 얻어지는 안정성임
💣 개발자가 '시스템 해킹'을 시도해서 다운캐스팅할 때 문제
val list = listOf(1,2,3) val mutableList = list.toMutableList() mutableList.add(4)
↪ 어떠한 규약도 어기지 않고, 기존의 객체는 여전히 immutable이라 수정 ❌
-> 안전👍
String이나 Int 처럼 내부적인 상태를 변경하지 않은 immutable 객체를 많이 사용하는데 이유가 존재
immutable 객체를 사용하면 다음과 같은 장점
한 번 정의된 상태가 유지
-> 코드 이해 쉬움
immutable 객체는 공유했을 때도 충돌이 따로 이루어지지 ❌
-> 병렬 처리 안전
immutable 객체에 대한 참조는 변경 ❌
-> 쉽게 캐시 가능
immutable 객체는 방어적 복사본(defensive copy)을 만들 필요 ❌
-> 객체를 복사할 때 깊은 복사를 따로 하지 않아도 됨
immutable 객체는 다른 객체(mutable or immutable 객체)를 만들 때 활용하기 좋음
immutable 객체는 세트(set) 또는 맵(map)의 키로 사용 할 수 있음
-> mutable 객체는 이러한 것 사용 ❌
//ex)
val names: SortedSet<FullName> = TreeSet()
val person = FullName("AAA","AAA")
names.add(person)
names.add(FullName("Seung","Kim")
names.add(FullName("Wan","Kim")
print(names) // [AAA, AAA, Wan Kim, Seung, Kim]
print(person in names) // true
person.name = "ZZZ"
print(names) // [ZZZ, AAA, Wan Kim, Seung, Kim]
print(person in names) // false
↪ 세트 내부에 해당 객체가 있음에도 false를 리턴
-> 객체를 변경했기 때문에 찾을 수 없는 것
mutable 객체는 변경할 수 없다는 단점
immutable 객체는 변경할 수 없다는 단점
-> 🔥따라서 immutable 객체는 자신의 일부를 수정한 새로운 객체를 만들어 내는 메서드를 가져와야 함!
ex) User라는 immutable 객체가 있고, surname을 변경해야 한다면,
withSurname과 같은 메서드를 제공해서, 자신을 수정한 새로운 객체를 만들어 낼 수 있게 해야함
class User( val name: String, val surname: String ) { fun withSurname(surname: String) = User(name, surname) } // var user = User("Seung","Kim") user = user.withSurname("Gim") print(user) // User(name = Seung, surname = Gim)
↪ 모든 프로퍼티를 대상으로 이런 함수 하나하나 만드는 것은 귀찮음
-> data 한정자를 사용하면 됨 !!
data class User( val name: String, val surname: String ) // var user = User("Seung", "Kim") user = user.copy(surname = "Gim") print(user) // User(name = Seung, surname = Gim)
기본적으로 이렇게 만드는게 좋음👍
val list1: MutableList<Int> = mutableListOf() var list2: List<Int> = listOf() // list1 += 1 // list1.plusAssign(1)로 변경 list2 += 1 // list2 = list2.plus(1)로 변경
var list = list<Int>()
for ( i in 1..1000) {
thread {
list = list + 1
}
}
Thread.sleep(1000)
print(list.size) // 1000이 되지 않습니다.
//실행할 때마다 911과 같은 다른 숫자가 나옵니다.
ex) Delegates.observable을 사용하면, 리스트에 변경이 있을 때 로그 출력 ✔️
var names by Delegates.observable(listOf<String>()) { _, old, new -> println("Names changed from $old to $new") } // names += "Seung" // names가 []에서 [Seung]로 변합니다. names += "Kim" // names가 [Seung]에서 [Seung, Kim]로 변합니다.
var announcements = listOf<Announcement>() private set
var list3 = mutableListOf<Int>() // 최악
상태를 나타내는 mutable 객체를 외부에 노출하는 것은 굉장히 위험💣💣
ex)
class UserHolder {
private val user: MutableUser()
fun get(): MutableUser {
return user.copy()
}
// ...
}
ex)
data class User(val name: String)
calss UserRepository {
private val storedUsers: MutableMap<Int, String> =
mutableMapOf()
fun loadAll(): Map<Int, String> {
return storedUsers
}
//...
}