공부/Kotlin

이펙티브 코틀린 1장 아이템1 - 가변성을 제한하라

데자와 맛있다 2024. 6. 9. 01:31

1장의 주제 : 오류를 덜 일으키는 코드 작성법

아이템 1의 주제: 가변성을 제한한 immutable이 좋은 이유, 최대한 변경되는 부분을 제한하자

 

가변성의 단점

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

- 모듈을 이루는 요소: 클래스, 객체, 함수, 타입 별칭 등

 

- 상태: var, mutableList 처럼 변경 가능한 변수를 사용하게 되면 가지게 됨

상태를 가진 요소는 그 상태가 변경되는 이력에 따라 동작이 영향받게된다. (변경된 값을 사용할때와 아닐때의 동작이 달라지기 때문임)

 

- 상태를 가진 요소에서 일어날수있는 리스크들

1. 프로그램 이해, 디버그 어려워짐: 상태들의 관계를 이해해야 함, 상태 변경을 추적해야 함, 이해하기 어려운 클래스가된다, 수정하기도 여려워짐, 예상못한 에러를 일으킬 가능성이 높다

2. 코드 실행을 추론하기 어렵다: 현재 어떤 값을 가지고있는지 알아야 실행을 예측할수있음, 확인한 값이 계속 동일하게 유지되는지 알수없음

3. 멀티스레드에서 동기화 필요: 변경이 일어나는 모든 부분에서 충돌 가능성이 있다.

4. 테스팅 어려움: 상태가 있으면 상태 변경마다 테스팅 해야 하므로, 테스트할 경우의수가 늘어난다

5. 상태 변경시 변경에 영향을 받을 수 있는 모든 부분에 변경을 알려야 함

그래서 결론은? 변경 가능한 상태는 코드 일관성을 낮추고 복잡성을 올린다

하지만 필요한 부분임 그래서 변경이 일어나야 하는 부분을 신중하고 확실히 결정하고 사용해야 함

 

- 순수 함수형 언어

* 가변성을 완전히 제한하는 프로그래밍 언어

* 종류: 하스켈, F#, Clojure 등

 

- 코틀린은 불변성을 강요하지는 않으나 불변성을 권장하며 되도록이면 자동으로 불변을 제공하려한다.

 

코틀린에서 가변성 제한하는법

코틀린에서 가변성을 제한하는 방법

1. val 읽기 전용 프로퍼티 사용

- val 로 프로퍼티를 선언하면 읽기 전용 프로퍼티가 된다

- 읽기전용과 불변(immutable)은 다르다

  • 불변성: 변경될수 없음을 의미함
  • val 읽기전용: 불변성에 가깝다, 하지만 초기화 이후 어느 시점에 따라 다른 값이 들어갈수는 있다. 예시는 아래에 있음

- val 프로퍼티에 mutable 한 객체가 있는 경우 내부적으로 변경이 가능하다, 재할당이 불가능함

val list = mutableListOf(1,2,3)
list.add(4)
print(list) // [1,2,3,4]

- var과 게터를 활용하여 상황에 따라 변경되는 val 을 만들수도 있다

var name: String = "Marcin"
var surname: String = "Moskala"
val fullName
	get() = "$name $surname"
    
fun main() {
    println(fullName) //Marcin Moskala
    name = "kim"
    println(fullName) //kim Moskala

}

- val 을 var로 오버라이드 할수있다.

    val 은 게터만 제공하고 var은 게터와 세터를 모두 제공하기때문에 오버라이드 가능함

interface Element {
	val active: Boolean
}

class ActualElement: Element {
	override var active: Boolean = false
}

- val 의 레퍼런스 자체를 변경할수는 없기 때문에 동기화 문제 등을 줄일 수 있음

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

- var val 처럼 읽기전용 프로퍼티가 아닌것과 그런것으로 프로퍼티가 나뉘듯 컬렉션도 읽기전용과 아닌것으로 구분됨

- 컬렉션들 중에서 mutable 이 붙은애들은 읽고 쓸수있고 아닌것은 읽기전용이다

- 읽기전용 컬렉션을 mutable 컬렉션으로 변경하고싶다면 toMutableList와 같은 방법을 통해 새로운 컬렉션을 만들어야 한다.

3. 데이터 클래스의 copy

  • immutable 객체
    • String, Int 처럼 내부적인 상태를 변경하지 않는 객체
    • 장점
      • 상태가 유지되므로 코드 이해 쉽다
      • 공유하는 경우에도 충돌이 일어나지 않아 병렬 처리를 안전하게 할수있다
      • 객체에 대한 참조가 변경되지 않으므로 쉽게 캐시할수있다
      • 방어적 복사본을 만들 필요가 없다 , 복사를 할때 깊은 복사를 따로 하지 않아도 된다
      • 실행 예측이 쉽다
      • set, map의 키로 쓸수있다. 
    • immutable객체에 값 변경이 필요한 경우엔 변경값이 들어간 새로운 객체를 리턴하는 함수가 필요
      • Int의 경우 plus, minus 메서드로 변경사항을 적용한 새로운 객체를 리턴한다
      • Iterable도 map, filter 로 변경사항을 적용한 새로운 객체를 만들어 리턴한다
      • 직접 immutable 객체를 만들고 싶은 경우에도 이러한 메서드를 만들어줘야 한다
class User (
	val name: String,
    val surname: String 
) {
	fun withSurname(surname: String) = User(name, surname)
}

 

위처럼 하나하나 변경사항을 적용해 새로운 변수를 리턴하도록 메서드를 만들면 매우 귀찮다

이 경우에는 data 한정자로 클래스를 만들면 해당 클래스에는 copy 메서드가 생긴다

copy 메서드를 사용하면 특정 프로퍼티만 변경하여 새로운 객체를 리턴받을수있다

 

mutable 객체보단 immutable 객체로 만드는것이 더 많은 장점이 있으므로 기본적으로는 이렇게 만드는것이 좋다

 

다른종류의 변경 가능 지점

변경할 수 있는 리스트를 만들때 두가지 방법이 있다.

1. MutableList 처럼 변경가능한 컬렉션을 이용한다

val list1: MutableList<Int> = mutableListOf()
list1.add(1)

2. immutable 컬렉션을 이용한다, var로 프로퍼티 자체를 수정한다

var list2: List<Int> = listOf()
list2 = list2+1

 

1의 경우 

-> 변경되는 시점이 MutableList 내부적으로 구현되어있음

따라서 멀티스레드와 같은 처리가 이루어지는 경우에 내부적으로 적절히 동기화 되어있는지 알수없어 변경시점을 명확히 알아야 하는 경우나 동기화되어야 하는 경우에 위험

 

2의 경우

-> 변경되는 시점이 명확, 멀티스레드 처리의 안정성이 더 좋다

-> Delegates.observable 을 사용하여 변경 추적이 가능

var names by Delegates.observable(listOf<String>()) { _, old, new ->
	println("변경됨")
}

 

따라서 2처럼 mutable 컬렉션을 사용하기보단 var을 통해 프로퍼티 자체를 바꾸는것이 낫다

 

최악의 방법은 둘을 섞어 쓰는것이다

var list3 = mutableListOf<Int>()

위처럼 var을 쓰는동시에 mutable 컬렉션을 같이 사용하는것이 최악이다

이렇게되면 누구는 list3 변경할때 프로퍼티 자체를 바꾸고 누구는 mutableList 안에 메서드통해서 바꾸고 중구난방

그리고 두 경우 모두에 동기화 처리를 해줘야된다

그래서 최악!

 

변경 가능지점을 노출하지 말자

변경가능한 mutable 객체를 외부에 그대로 내보낼수있도록 만드는것은 위험하다

data class User(val name: String)

class UserRepository{
    private val storedUsers: MutableMap<Int, String> = mutableMapOf()
    
    fun loadAll(): MutableMap<Int, String>{
        return storedUsers
    }
    
    //...
}

fun main(){
    val userRepository = UserRepository()
    
    val storedUsers = userRepository.loadAll()
    
    storedUsers[4] = "Kirill"
    print(userRepository.loadAll()) //{4=Kirill}
    
}

위 코드 loadAll() 은 private 변수인 storedUsers를 고대로 리턴한다

그 리턴 받은 값은 mutable컬렉션이다 그래서 변경이 가능하다

결과적으로 private변수임에도 불구하고 외부에서 맘대로 변경해버리는게 가능해진다

만약 예전에 코드짜던 사람이 UserRepository를 위처럼 만들고 storedUsers는 private이니깐 외부에서 변경할수있다는것을 생각안하고 코드를 짰다고 치자

그리고 그사람이 퇴사하고 ^^ 다음사람이 storedUsers가 외부에서 변경되면 안되는걸 모르거나 (코드가 너무 많거나 혹은 읽을 시간이 없거나 등) loadAll로 리턴받은 값에 변경을했을때 원본도 같이 바뀌는것을 모를때 문제가 됨

 

이렇게 mutable 컬렉션 이면서 private 변수인것을 클래스 외부로 내보내야 할때 두가지 처리 방법이 있다

1. 방어적 복제

객체 그대로를 노출하는게 아니고 리턴되는 mutable 객체를 복제해서 복제된 값을 넘겨준다

class UserHolder {
	private val user: MutableUser()
    fun get(): MutableUser {
    	return user.copy()
    }
}

// 위에서 보여준 UserRepositoy 예시를 방어적 복제 적용하면 아래와 같음
data class User(val name: String)

class UserRepository{
    private val storedUsers: MutableMap<Int, String> = mutableMapOf()
    
    fun loadAll(): MutableMap<Int, String> {
        return storedUsers.toMutableMap()
    }
    
    
    //...
}

fun main(){
    val userRepository = UserRepository()
    
    val storedUsers = userRepository.loadAll()
    
    storedUsers[4] = "Kirill"
    print(userRepository.loadAll()) 
    
}

2. 읽기전용 컬렉션으로 업캐스팅해서 리턴

class UserRepository {
    private val storedUsers = mutableMapOf<Int, String>()
    // immutable up-casting
    fun loadAll(): Map<Int, String> = storedUsers
}

 이러면 이제 변경을 해줘야될때는 앞에서 본것처럼 var로 받아서 새로 Map을 만들어주면됨

 

정리

  • var 보다 val 을 쓰자
  • mutable 프로퍼티 보단 immutable프로퍼티를 쓰자
  • mutable 객체와 클래스보단 immutable 객체와 클래스를 쓰자
  • 변경이 꼭 필요하다면 val 변수로 만들어서 immutable 클래스로 만들되 data 키워드로 만들어서 copy를 쓰도록하자
  • 컬렉션에 변경되는값을 저장해야 된다면 mutable컬렉션(MutableList..) 보단 var로 프로퍼티 자체를 바꿔넣자
  • 값을 바꾸는 시점을 적절히 설계하고 불필요하게 값이 변경되지 않도록 한다
  • mutable 객체를 외부에 고대로 노출하면 안된다

 

참고사이트

https://readystory.tistory.com/105

 

코틀린(kotlin)과 불변성(immutability)

불변성(immutability)은 함수형 프로그래밍에서 가장 중요한 부분입니다. 왜 중요한 것일까요? 불변성이란 무엇을 의미할까요? 코틀린에서 불변성을 어떻게 구현할 수 있을까요? 이번 포스팅에서는

readystory.tistory.com

https://augustin26.tistory.com/22

 

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

읽기, 쓰기가 가능한 프로퍼티나 mutable 객체는 상태를 가집니다. 상태를 갖게 한다는 것은 변한다는 것이고, 변하는 요소를 관리하는 것은 어렵습니다. 프로그램을 이해하고 디버그하기 힘들어

augustin26.tistory.com

https://uzun.dev/194

 

[이펙티트 코틀린] 1장 🏰 안정성 Safety (item 01..10)

item 1 : 가변성을 제한하라 상태를 제어하는 것은 양날의 검 장점 : 시간의 변화에 따라서 변하는 요소를 표현 단점 : 상태를 관리하는 것은 어려움이 따른다 프로그램을 이해하고 디버그하기 힘

uzun.dev

https://bottom-to-top.tistory.com/63

 

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

어떠한 요소가 상태를 갖는 경우, 요소의 동작은 사용 방법뿐만 아니라 그 이력에도 의존하게된다. 상태를 갖게 하는 것은 양날의 검이다. 변하는 요소를 표현할 수 있다는 것은 유용하지만, 상

bottom-to-top.tistory.com

https://velog.io/@woga1999/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%BD%94%ED%8B%80%EB%A6%B0-item1-%EA%B0%80%EB%B3%80%EC%84%B1%EC%9D%84-%EC%A0%9C%ED%95%9C%ED%95%98%EB%9D%BC

 

이펙티브 코틀린 item1: 가변성을 제한하라

간간히 올라올 이펙티브 코틀린을 읽고 정리하는 게시글은 간결하게만 적을 예정이다. (물론 내 입장에서만 간결할수도)그러므로 내용이 더 궁금하거나 공부하고 싶다면 해당 책을 구매 후 직

velog.io

https://velog.io/@sunjoo9912/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%BD%94%ED%8B%80%EB%A6%B0-1%EB%B6%80-%EC%A2%8B%EC%9D%80%EC%BD%94%EB%93%9C-1%EC%9E%A5-%EC%95%88%EC%A0%95%EC%84%B1

 

[이펙티브 코틀린] 1부 좋은 코드 1장 안정성 1. 가변성을 제한하라✍

[이펙티브 코틀린] 1부 좋은 코드 1장 안정성 1. 가변성을 제한하라

velog.io