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

woga·2023년 1월 28일
0

코틀린 공부

목록 보기
4/54
post-thumbnail

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

가변성을 제한하라

코틀린은 읽고 쓸 수 있는 프로퍼티(read-write property) var를 사용하거나 mutable 객체를 사용하면 상태를 가질 수 있다.

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

이처럼 요소가 상태를 갖는 경우, 해당 요소의 동작은 사용 방법뿐만 아니라 history에도 의존하게 된다.

그래서 가변 상태를 갖고 이를 관리하는 꽤나 어려운데, 그 이유는 아래와 같다.

  • 프로그램을 이해하고 디버하기 힘들다. 상태를 갖는 부분들의 관계를 이해해야 하고 상태 변경이 많아지면 추적하기 힘들기 때문이다. 클래스가 예상하지 못한 상황 또는 오류를 발생시키는 경우에 큰 문제가 된다(사이드 이펙)

  • 가변성이 있으면, 코드의 실행을 추론하기가 어렵다. 시점에 따라 값이 달라질 수 있으므로, 현재 어떤 값을 갖고 있는지 알아야 코드의 실행을 예측할 수 있기 때문이다. 한 시점에 확인한 값이 계속 동일할거라는 확신도 없다.

  • 멀티스레드 프로그램일 때는 적절한 동기화가 필요하다. 변경이 일어나는 모든 부분에서 충돌이 발생할 수 있기 때문이다.

  • 테스트하기 어렵다. 변경이 많을수록 많은 조합으로 테스트를 해야하기 때문이다.

  • 상태 변경이 일어날 때, 다른 부분에 이런 변경을 알려야하는 경우가 있다. 정렬 리스트에 가변 요소를 추가하면 다시 재정렬해야하는 것을 예시로 들 수 있다.

대규모 팀에서 일하고 있는 개발자라면 이런 가변적인 부분에 일관성 문제, 복잡성 증가와 관련된 문제가 익숙할 것이다. 공유 상태를 관리하는 것은 힘들다.

만약 아래 코드처럼 멀티스레드를 활용해서 프로퍼티를 수정하면 제대로된 연산이 되질 않는다.

var num = 0
for (i in 1..1000) {
	thread {
    	Thread.sleep(10)
        num += 1
    }
}
Thread.sleep(5000)
print(num) //1000이 아닐 확룔이 매우 높다.
//실행할때마다 다른 숫자가 나옴

그래서 이를 방지하기 위해 적절하게 동기화를 구현해야하는데, 또 이게 어려운 일이다. 또한 변할 수 있는 지점이 많으면 더 어려워진다. 그래서 변할 수 있는 지점은 줄이는게 좋다!

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

그러나 우리의 멋진 코틀린은 가변성을 제한할 수 있게 설계되어 있다. 그래서 immutable(불변) 객체를 만들거나, 프로퍼티를 변경할 수 없게 막는 것이 굉장히 쉽다.

  • 읽기 전용 프로퍼티 (val)
  • 가변 컬렉션과 읽기 전용 컬렉션 구분하기
  • 데이터 클래스의 copy

이 3가지 방법으로 제한을 할 수 있다.

읽기 전용 프로퍼티 - val

이 키워드를 이용해 선언된 프로퍼티는 value처럼 동작하면 일반적인 방법으로는 값이 변하지 않는다.
그러나 읽기 전용 프로퍼티가 완전히 변경 불가능한 것은 아니라는 것을 알자. 읽기 전용 프로퍼티가 mutable 객체를 담고 있으면 내부적으로 변할수 있다.

val list = mutableListOf(1,2,3)
lsit.add(4)

print(list)

그리고 var 프로퍼티를 사용하는 val 프로퍼티는 var 프로퍼티가 변할 때 변할 수 있다.

var name: String = "Marchin"
var surname: String = "Moskala"
val fulName
	get() = "$name $surname"

fun main() {
	println(fullName) // Marcin Moskala
    name = "Maja"
    println(fullName) // Maja Moskala
}

코틀린의 프로퍼티는 기본적으로 캡슐화되어 있고 추가적으로 게터, 세터를 가질 수 있다. 그래서 이 특징으로 코틀린은 api를 변경하거나 정의할 때 유연하다.
추가적으로 var은 게터 세터를 모두 제공하지만 val은 게터만 제공한다는 특징이 있다.
그래서 아래처럼 val은 var로 오버라이드가 가능하다!

interface Element {
	val active: Boolean
}

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

그러나 val 값은 변경될 수 있긴 하지만, 프로퍼티 레퍼런스 자체를 변경할 수는 없어서 동기화 문제 등을 줄일 수 있다. 그래서 일반적으로 val을 많이 사용한다.

참고로! val 게터를 이용해서 스마트 캐스트는 안된다 프로퍼티가 final이고 사용자 정의 게터를 갖지 않는다면 스마트 캐스트 할 수 있다.

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

지금까지 살펴본 것처럼, 코틀린은 읽고 쓸 수 있는 프로퍼티와 읽기 전용 프로퍼티를 구분했는데 컬렉션도 마찬가지다.

  • 읽기 전용

Iterable, Collection, Set, List

  • 읽고 쓰는 컬렉션

MutableIterable, MutableCollection, MutablSet, MutablList

mutabl이 붙은 인터페이스는 읽기 전용 인터페이스를 상속받아서 변경을 위한 메서드를 추가한 것이다. 마치 읽기 전용 프로퍼티가 게터만 갖고 읽고쓰기는 게터와 세터만 갖는 것과 비슷하게 동작한다.

이처럼 코틀린에서 가변과 읽기 전용으로 나뉘어 안정성을 갖고 있는데 다운캐스팅을 할 때 문제가 된다.

리스트를 읽기 전용으로 리턴하면, 이를 읽기 전용으로만 사용해야 한다.

val list = listOf(1,2,3)

// do not this!
if (list is MutableList) {
	list.add(4)
}

이 코드는 플랫폼에 따라 실행결과가 다르다.
JVM에서는 listOf는 자바의 List 인터페이스를 구현한 Array.ArrayList 인스턴스를 리턴한다. 그러나 Array.ArrayList는 add나 set 같은 메서드를 제공하지 않아 오류를 발생한다.
java.lang.UnsupportOperationException

따라서 코틀린에서 읽기 전용 컬렉션을 mutable 컬렉션으로 다운캐스팅하면 안된다. 읽기 전용에서 변경하고 싶다면 copy를 통해 새로운 mutable 컬렉션을 만드는 toMutableList()를 활용해야한다.

val list = listOf(1,2,3)

val mutableList = list.toMutableList()
mutableList.add(4)

이렇게 하면 규약도 어기지 않고 기존의 객체는 여전이 immutable이라 수정할수도 없어 안전하다고 할 수 있다.

데이터 클래스의 copy

immutable 객체를 사용하면 아래와 같은 장점이 있다.

  • 한 번 정의된 상태가 유지되므로 코드 이해도 향상

  • immutable 객체는 공유했을 때도 충돌이 따라 이루어지지 않아 병렬 처리에 대해 안전

  • immutable 객체에 대해 참조는 변경되지 않으므로 쉽게 캐싱 가능

  • immutable 객체는 방어적 복사본을 만들 필요 없음, 그래서 깊은 복사도 따로 하지 않아도 된다

  • immutable 객체는 다른 객체를 만들 때 활용하기 좋다.

  • immutable 객체는 Set 또는 Map의 키로 사용 가능

그러나 immutable 객체는 변경할 수 없다는 단점이 있는데, 이때문에 자신의 일부를 수정한 새로운 객체를 만들어 내는 메서드가 필요하다.

Int로 예를 들면 plus() minus() 메서드로 자신을 수정한 새로운 Int를 리턴하고, Iterable도 map() filter() 메서드로 자신을 수정한 새로운 Iterable 객체를 만들어서 리턴한다.

이처럼 새로운 객체를 반환하는 메서드를 만들면 되는데, 매번 이런 객체에 메서드를 만들기 불편하니깐 data 한정자를 사용하면 된다. 기본적으로 copy라는 메서드를 만들어준다.

다른 종류의 변경 가능 지점

val list1: MutablList<Int> = mutableListOf()
var list2: List<Int> = listOf()

// 1
list1.add(1)
list2 = list2 + 1

//2
list1 += 1
list2 += 2

다음과 같은 프로퍼티와 코드가 있다고 생각해보자.
첫 번째 코드는 구체적인 리스트 구현 내부에 변경 가능 지점이 있다. 멀티스레드 처리가 이루어지면 내부적으로 적절하게 동기화 되어 있는지 확실하게 알 수 없다.

그러나 2 번째 코드는 프로퍼티 자체가 변경 가능 지점이라 멀티스레드 처리의 안정성이 더 좋다.

또한, 리스트 대신 프로퍼티를 사용하는 형태는 Delegates.observable을 사용해서 변경을 추적할 수 있다.
아래 코드는 리스트에 변경이 있을때마다 로그를 출력할 수 있다.

var names by Delegates.observable(listOf<String>()) { _, old, new -> println("Names changed from $old to $new")
}

names += "Fabio"
// Names changed from [] to [Fabio]
name += "Bill"
// Names changed from [] to [Fabio, Bill]

mutable 컬렉션을 사용하는 것이 처음에는 간단하게 보이겠지만 mutable 프로퍼티를 사용하면 객체 변경을 제어하기가 더 쉽다

참고로 최악은 var list3 = mutablListOf<Int>() 프로퍼티와 컬렉션을 모두 변경 가능한 지점으로 만드는 것이다!!

이런 프로퍼티는 모호성이 발생해서 "+="를 사용할 수 없다.

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

객체에서 가변 객체나 컬렉션을 반환하는게 아니라 방어적 복제를 하거나 객체를 읽기 전용 슈퍼타입으로 업캐스트하여 가변성을 제한하는게 좋다.

  • 첫번째 예제 : 방어적 복제
class UserRepository {
	private val user: MutableUser()
    
    fun get(): MutableUser {
    	return user.copy()
    }
}
  • 두번째 예제 : 읽기 전용 슈퍼타입으로 업캐스트
class UserRepository {
	private val storedUsers: MutableMap<Int, String> = mutableMapOf()
    
    fun loadAll(): Map<Int, String> {
    	return storedUsers
    }
}

마무리

코틀린에서는 가변성을 제한하기 위해 다양한 방법과 메서드와 도구들을 제공한다! 이를 활용해 가변 지점을 제한하며 안전한 프로그래밍을 하자! 이펙티브 코틀린이란 명성답게 첫 판부터 가변과 불변에 다루니 흥미로웠다. 앞으로 개발할 때도 참고해서 코드리뷰와 개발을 해야겠다는 생각이 들었다.

물론 이 책에서 말하길 가끔은 효율성 때문에 immutable 객체보다 mutable 객체를 사용하는 것이 좋을때가 있다는데 그것 또한 판단을 잘해서 개발을 해야할 듯 하다. 끝!

profile
와니와니와니와니 당근당근

0개의 댓글