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

ds_chanin

·

2022. 2. 25. 23:16


어떠한 요소가 상태를 갖는 경우, 요소의 동작은 사용 방법뿐만 아니라 그 이력에도 의존하게된다.

상태를 갖게 하는 것은 양날의 검이다. 변하는 요소를 표현할 수 있다는 것은 유용하지만, 상태를 적절하게 관리하기 어렵기 때문이다.

  1. 이해하고 디버그하기 힘들어진다.
  2. 코드의 실행을 추론하기 어려워진다.
  3. 멀티스레드 프로그램일 때 동기화가 필요하다.
  4. 테스트하기 어렵다.
  5. 변경을 다른 부분에 알려야 하는 경우 불편하다.
  6. set, map의 key로 사용할 수 없다. hash 값이 변경되면 요소를 버킷에서 찾지 못할 수 있다.

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

코틀린은 가변성을 제한할 수 있게 설계되어 있습니다.

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

읽기 전용 프로퍼티 (val)

value를 의미하며 읽기전용 프로퍼티를 만들 때 사용한다. 일반적인 방법으로는 값이 변하지는 않으나 완전히 변경이 불가능한 것은 아니다. 예를들어 읽기 전용 프로퍼티가 가변(mutable)객체(ex. MutableList)를 담고 있다면 내부적으로 변할 수 있다.

읽기 전용 프로퍼티는 다른 프로퍼티를 활용하는 사용자 정의 게터로도 정의할 수 있다.

var name: String = "찬인"
var surname: String = "박"
val fullName
	get() = "$name $surname"  // 사용자 정의 getter

// Java 였다면
// public String getFullName() {
//   return name + " " + surname
// }

fullName을 직접 변경할 수 없지만 name, surname의 변경을 통해 읽는 값을 변경할 수 있다.

val은 게터를 제공하지만 세터는 제공하지 않는다. 하지만 var로 오버라이드 하여 사용할 수 있다.

다만 시점에 따라 달라지기 때문에 사용자 게터를 사용한 경우 스마트 캐스트를 활용할 수 없다.

//Java Optional
Optional<Integer> number = Optional.of(1);
if(number.isPersent()) {
	Integer actualNumber =  number.get() // 물론 안티패턴으로 ifPresent를 쓰는 방식으로 풀어야하지만..
}

//Kotlin 스마트 캐스트
val number: Int?

fun isPositive(): Boolean {
	if(number == null) {
			return number > 0 // if 절에서 null 검사가 되면서 Int? -> Int 로 취급
	}
	return false
}
interface Element {
    val active: Boolean // 책에 var로 나와있는데 오타인 것 같아보인다.
}

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

이를 통해 val는 읽기 전용 프로퍼티라는 것이지 변경할 수 없다(immutable)는 것이 아님을 기억해야한다.

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

Iterable, Collection, Set, List 는 읽기만 가능한 읽기 전용 컬렉션이고 여기서 파생된 Mutable이 붙은 인터페이스는 읽기 전용 컬렉션을 상속받아 변경을 위한 메서드를 추가한 것이다.

읽기전용 컬렉션을 가변 컬렉션으로 변경해서 사용해야 한다면 내부적으로 copy를 통해서 새로운 가변 컬랙션을 생성하는 toMutableList() 와 같은 메서드를 사용해야한다.

데이터 클래스의 copy

  1. 한 번 정의된 상태가 유지되므로, 코드를 이해하기 쉽다.
  2. 객체를 공유했을 때 충돌이 발생하지 않아 멀티스레드 처리하기 쉽다.
  3. 참조 변경이 발생하지 않아 쉽게 캐시할 수 있다.
  4. 방어적 복사본을 만들 필요가 없다.
  5. 불변 객체는 set, map 컬렉션의 key로 사용할 수 있다.

따라서 가변 객체는 프로퍼티의 변경이 필요하면 변경할 프로퍼티만 변경되고 나머지 프로퍼티는 그대로인 새로운 객체를 생성할 수 있도록 해야한다.

객체의 모든 프로퍼티에 대해 자기 자신을 수정한 객체를 반환하는 작업이 꽤 귀찮은 작업이 되지만 copy를 사용하면 편리하게 해결할 수 있다.

copy는 data 클래스가 제공해주는데 copy를 활용하면 모든 기본생성자 프로퍼티가 같은 새로운 객체를 만들 수 있다.

다른 종류의 변경 가능 지점

변경할 수 있는 리스트를 만들어야 할 때 두가지 선택지가 있다.

  1. val + 가변 컬렉션의 조합 구현 내부가 변경 가능 지점
  2. var + 읽기 전용 컬렉션의 조합 프로퍼티 자체가 변경 가능 지점

구현 내부에 변경가능 지점이 있는 경우 멀티스레드 처리시 내부적으로 동기화가 되어 있는지 확실하게 알 수 없어 위험하다. 프로퍼티 자체가 변경 가능하다면 비교적 안정적이다.

또한 프로퍼티 자체가 변경 가능하다면 사용자 정의 세터를 이용하여 변경을 추적할 수 있다.(ex. Delegates.observable) 그리고 세터의 접근 제어자를 private 로 변경하고 사용할 수 있다.

최악은 프로퍼티와 컬렉션 모두 변경가능하게 만드는 것이다.

상태를 변경하는 모든 방법은 코드를 이해하고 유지해야 하므로 비용이 발생한다. 그래서 가변성은 제한하는 것이 좋다.

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

상태를 나타내는 mutable 객체를 외부에 노출하는 것은 위험하다.

  1. copy를 이용한 방어적 복제를 통해 객체를 복제하여 반환한다.
  2. 읽기 전용 객체로 업캐스트하여 반환한다.

정리

변경 가능한 객체는 멀티스레드 환경에서 더 많은 주의를 기울여야 하는것 처럼 가능한 가변성을 제한하는 것이 좋다. 다만 효율성 때문에 불변 객체보다 가변 객체를 사용하는 것이 좋을 때가 있으나 성능이 중요할 때 고려하도록 한다.

질문

Data Class의 Copy시 언제나 깊은 복사가 이루어지는가?

→ 프로퍼티가 객체인 경우 객체가 Data 클래스인지와 관계없이 얕은 복사가 이루어진다.