[이펙티브 코틀린] 1장 안정성 Item 1 가변성을 제한하라

Sdoubleu·2023년 1월 2일
0

이펙코

목록 보기
1/7
post-thumbnail

Item 1. 가변성을 제한하라.

  • 코틀린은 모듈로 프로그램 설계
    - 모듈은 클래스, 객체, 함수, 타입 별칭(type alias), 톱레벨(top-level) 프로퍼티 등 다한 요소로 구성

  • 이러한 요소 중 일부는 상태(state)를 가질 수 있음
    -> ex) 읽고 쓸수 있는 프로퍼티(var), mutable 객체를 사용하면 상태를 가짐

var a = 10
var list: MutableList<Int> = mutableListOf()

상태를 갖는 요소의 단점

  1. 프로그램을 이해하고 디버그하기 힘듦
    -> 상태를 갖는 부분들의 관계를 이해해야 하며, 상태 변경이 많아지면 이를 추적하기 힘들어짐

  2. 가변성(mutability)이 있으면, 코드의 실행 추론하기 어려워짐
    -> 시점에 따라서 값이 달라질 수 있으므로, 현재 어떤 값을 갖고 있는지 알아야 코드의 실행을 예측 가능

  3. 멀티스레드 프로그램일 때는 적절한 동기화가 필요

  4. 테스트하기 어려움
    -> 변경이 많으면 많을수록 많은 조합을 테스트해야 함

  5. 상태 변경이 일어날 때, 이러한 변경을 다른 부분에 알려야하는 경우가 존재
    -> 정렬되어 있는 리스트에 가변 요소를 추가한다면, 요소에 변경이 일어날 때마다 리스트 전체를 정렬해야 함


공유 상태를 관리하는 것이 얼마나 힘든지에 대한 예시

  • 멀티스레드를 활용해서 프로퍼티를 수정, 이 때 출돌에 의해 일부 연산 작동 x
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

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

  • 코틀린은 가변성을 제한할 수 있게 설계
    - immutable(불변) 객체 를 만들거나, 프로퍼티를 변경할 수 없게 막는 것이 쉬움
  1. 읽기 전용 프로퍼티(val)
  2. 가변 컬렉션과 읽기 전용 컬렉션 구분하기
  3. 데이터 클래스의 copy

1. 읽기 전용 프로퍼티(val)

  • 코틀린은 val을 사용해 읽기 전용 프로퍼티를 만들 수 있음
  • 값(value)처럼 동작, 일반적인 방법으로 값이 변하지 않음
    -> 읽고 쓸수 있는 프로퍼티는 var로 만듦
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
  • 코틀린의 프로퍼티는 기본적으로 캡술화되어 있고, 추가적으로 사용자 정의 접근자(게터와 세터)를 가질 수 있음
    -> 이러한 특성으로 코틀린은 API를 변경하거나 정의할 때 굉장히 유연

var은 게터와 세터를 모두 제공하지만, val은 변경이 불가능하므로, 게터만 제공
-> valvar오버라이드할 수 있음

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이고, 사용자 정의 게터를 갖지 않을 경우 스마트 캐스트 할 수 있음 ✔️


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

  • 읽고 쓸 수 있는 컬렉션과 읽기 전용 컬렉션으로 구분

코틀린의 컬렉션 인터페이스 계층

  • 왼쪽에 있는 인터페이스는 읽기 전용

  • 오른쪽에 있는 인터페이스는 읽고 쓸 수 있음

  • 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하게 보이게 만들어서 얻어지는 안정성임
💣 개발자가 '시스템 해킹'을 시도해서 다운캐스팅할 때 문제

  • 컬렉션 다운캐스팅은 추상화를 무시하는 행위
  • 따라서, 읽기 전용에서 mutable로 변경해야 한다면,
    복사(copy)를 통해서 새로운 mutable 컬렉션을 만드는 list.toMutableList를 활용
  val list = listOf(1,2,3)
  val mutableList = list.toMutableList()
  mutableList.add(4)

↪ 어떠한 규약도 어기지 않고, 기존의 객체는 여전히 immutable이라 수정 ❌
-> 안전👍


3. 데이터 클래스의 copy

  • String이나 Int 처럼 내부적인 상태를 변경하지 않은 immutable 객체를 많이 사용하는데 이유가 존재

  • immutable 객체를 사용하면 다음과 같은 장점

    1. 한 번 정의된 상태가 유지
      -> 코드 이해 쉬움

    2. immutable 객체는 공유했을 때도 충돌이 따로 이루어지지 ❌
      -> 병렬 처리 안전

    3. immutable 객체에 대한 참조는 변경 ❌
      -> 쉽게 캐시 가능

    4. immutable 객체는 방어적 복사본(defensive copy)을 만들 필요 ❌
      -> 객체를 복사할 때 깊은 복사를 따로 하지 않아도 됨

    5. immutable 객체는 다른 객체(mutable or immutable 객체)를 만들 때 활용하기 좋음

    6. 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)

기본적으로 이렇게 만드는게 좋음👍


다른 종류의 변경 가능 지점

  • 변경할 수 있는 리스트를 만들어야 한다면
    1. mutable 컬렉션
    2. var 프로퍼티
  val list1: MutableList<Int> = mutableListOf()
  var list2: List<Int> = listOf()
  //
  list1 += 1 // list1.plusAssign(1)로 변경
  list2 += 1 // list2 = list2.plus(1)로 변경
  • 두 가지 모두 변경 가능하지만 방법이 다르고, += 연산자를 활용해 변경할 수 있으나 실질적으로 이루어지는 처리가 다름
    -> 변경 가능 지점의 위치가 다름
  1. 첫 번째 코드는 구체적인 리스트 구현 내부에 변경 가능 지점 존재
  2. 두 번째 코드는 프로퍼티 자체가 변경 가능 지점
    -> 멀티스레드 처리의 안정성이 더 좋음 ( 잘못 만들면 일부 손실 존재 )
  var list = list<Int>()
  	for ( i in 1..1000) {
  		thread {
  		list = list + 1
  	}
  }
  Thread.sleep(1000)
  print(list.size) // 1000이 되지 않습니다.
  //실행할 때마다 911과 같은 다른 숫자가 나옵니다.
  • mutable 리스트 대신 mutable 프로퍼티를 사용하는 형태는 사용자 정의 세터(또는 이를 사용하는 델리게이트)를 활용해서 변경 추적 가능

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]로 변합니다.
  • mutable 컬렉션도 observe할 수 있게 만들려면, 추가적 구현 필요
  • mutable 프로퍼티에 읽기 전용 컬렉션을 넣어 사용하는 것이 쉬움
    ->여러 객체를 변경하는 여러 메서드 대신 세터 사용하면 되고,
    private로 만들 수 있기 때문
var announcements = listOf<Announcement>()
	private set
var list3 = mutableListOf<Int>() // 최악

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

  • 상태를 나타내는 mutable 객체를 외부에 노출하는 것은 굉장히 위험💣💣


코드의 돌발적인 수정이 일어날 때의 위험을 처리하는 방법

  1. return되는 mutable 객체를 복제하는 것
    -> 이를 방어적 복제(*defensive copying) 이라고 부름
    data 한정자로 만들어지는 copy 메서드를 활용하면 좋음👍
ex)
class UserHolder {
	private val user: MutableUser()
  
	fun get(): MutableUser {
  		return user.copy()
  	}
  	// ...
  }
  1. 가변성 제한하기
    -> 컬렉션은 객체를 읽기 전용 슈퍼타입으로 업캐스트하여 가변성을 제한할 수 있음
ex)
data class User(val name: String)
  
calss UserRepository {
	private val storedUsers: MutableMap<Int, String> =
	mutableMapOf()
  
	fun loadAll(): Map<Int, String> {
  		return storedUsers
  	}
  	//...
  }

⭐정리

  1. var 보다는 val을 사용하는 것이 좋음
  2. mutable 프로퍼티보다 immutable 프로퍼티를 사용하는 것이 좋음
  3. mutable 객체/클래스 보다 immutable 객체/클래스를 사용하는 것이 좋음
  4. 변경이 필요한 대상을 만들어야 한다면,
    immutable 데이터 클래스로 만들고, copy를 활용하는 것이 좋음
  5. 컬렉션에 상태를 저장해야 한다면,
    읽기 전용 컬렉션을 사용하는 것이 좋음
  6. 불필요한 변이 지점은 만들지 않는 것이 좋음
  7. mutable 객체를 외부에 노출하지 않는 것이 좋음
  • 예외 존재
    • 효율성 때문에 immutable 객체보다 mutable 객체를 사용하는 것이 좋을 때 존재
      ↪ 이러한 최적화는 코드에서 성능이 중요한 부분에서만 사용하는 것이 좋음
profile
개발자희망자

0개의 댓글