본문은 Effective Kotlin을 읽고 간단하게 정리한 글입니다. 필요에 따라 생략/수정된 부분이 있을 수 있으며, 내용이 추후 변경될 수 있습니다.
1. mutable의 단점 + immutable의 장점
먼저 살펴보기에 앞서 mutable의 단점과 immutable의 장점을 살펴보자. mutable의 단점을 뒤집으면 immutable의 장점이 된다.
mutable 객체의 문제
- 상태들 간의 관계를 이해하고, 상태의 변경을 추적해야 하므로 프로그램을 이해하고 디버그하기 힘들어진다.
- 시점에 따라 값이 달라질 수 있으므로 코드의 실행을 추론하기 어렵다
- 멀티스레드 프로그램일 때 적절한 동기화가 필요하다
- 상태 변경이 많을수록 더 많은 조합을 테스트해야 하므로 테스트하기 어렵다
- 컬렉션 내의 객체가 가변 객체로 이루어져 있으면 컬렉션의 일관성이 깨질 수 있다
immutable 객체의 장점
- 상태가 바뀌지 않으므로 코드를 이해하기 쉬우며, 코드의 실행을 추론하기 어렵다.
- 병렬 처리가 쉽다
- 참조가 바뀌지 않으므로 쉽게 캐시할 수 있다
- 방어적 복사본을 만들 필요도 없고, 깊은 복사도 필요없다
- set 또는 map의 key로 사용할 수 있다
2. 코틀린에서 가변성을 제한하는 방법
1) 읽기 전용 프로퍼티(val)
일반적으로는 값을 바꾸지 못하지만, 아래와 같은 경우엔 바꿀 수가 있다. 참고로 읽기 전용은 재할당만 불가능하다는 의미이다. 프로퍼티를 읽을 수만 있다는 것(읽기 전용)과 변경할 수 없음(불변)을 구분지어서 생각해야 한다.
- mutable 객체를 담고 있으면 바꿀 수 있다
val list = mutableListOf(1, 2, 3)
list.add(4)
- var 프로퍼티를 사용하는 val 프로퍼티
var name: String = "John"
val fullName: String
get() = "King $name" // name이 바뀌면 같이 바뀜
println(fullName) // King John
name = "Sam"
println(fullName) // King Sam
또한, val을 사용하면 스마트 캐스팅이 가능하다.
- var은 안됨 (바뀔 수 있으므로)
- 커스텀 게터를 사용하는 경우에도 안됨 (var 프로퍼티를 사용할 수 있으므로)
var name: String? = "John"
val fullName: String?
get() = name?.let { "King $name" }
println(greeting) // King John
name = null
println(geeting) // null --> null일지 아닐지 모른다!
이외에도 val을 var로 오버라이딩 하는 것도 가능하다. 오버라이딩을 할 때 더 넓은 범위로만 접근 제어자를 설정할 수 있는 것과 같은 이치다.
interface Element {
val active: Boolean // getter만 있음
}
class ActualElement: Element {
override var active: Boolean = false // getter와 setter 모두 있음
}
2) 가변 컬렉션과 읽기 전용 컬렉션 구분하기
코틀린 컬렉션 인터페이스 계층을 살펴보면 읽기 전용과 가변 컬렉션이 아예 나눠지는 것을 알 수 있다.

val과 마찬가지로 읽기 전용 컬렉션이라고 해서 내부의 값을 못 바꾸는 것은 아니다. 아래는 map의 구현 예시인데, 이처럼 mutable한 구현체를 immutable한 인터페이스로 업캐스팅해서 반환하기 때문에 그렇게 보이는 것이다.
inline fun <T, R> Iterable<T>.map(
transformation: (T) -> R
): List<R> {
val list = ArrayList<R>() // mutable함
for (elem in this) {
list.add(transformation(elem))
}
return list // immutable한 타입으로 리턴됨
}
즉, 코틀린은 내부적으로 immutable하지 않은 컬렉션을 외부적으로 immutable하게 보임으로써 안정성을 얻고 있다. 따라서 절대로 임의로 다운캐스팅을 해서는 안 되며, 읽기 전용 컬렉션을 가변으로 바꾸려면 toMutableList와 같은 메서드를 사용해야 한다.
// Bad
val list = listOf(1,2,3)
if (list is MutableList) {
list.add(4) // AbstractList.add()에서 UnsupportedOperationException 발생
}
// Good
val list = listOf(1, 2, 3)
val mutableList = list.toMutableList() // 복제를 통해 새로운 mutable 컬렉션을 만듦
mutableList.add(4)
데이터 클래스의 copy
immutable 객체는 장점이 많지만, 변경할 수 없다는 단점이 있다. 따라서 immutable 객체는 자신의 일부를 수정한 객체를 만들어내는 메서드를 가져야 한다.
class User(
val name: String,
val surname: String
) {
fun withSurname(surname: String) = User(name, surname)
}
그런데, 모든 프로퍼티를 대상으로 이런 함수를 모두 만들어주는 것은 너무 귀찮으므로 data class의 copy 메서드를 사용하는 편이 더 낫다. 이와 같이 immutable한 데이터 모델 클래스를 만들 수 있다.
data class User(
val name: String,
val surname: String
)
val user = User("Wayne", "Rooney")
user = user.copy(surname = "Park")
print(user) // User(name=Wayne, surname=Park)
mutable 객체보단 이렇게 immutable 데이터 모델 클래스를 만드는 것이 나으므로 기본적으로는 이렇게 만드는 것이 좋다. 웬만하면 data class로 만드는 편이 좋지만, JPA Entity를 선언할 때는 절대로 data class로 만들지 말자. (참고)
3. 다른 종류의 변경 가능 지점
변경할 수 있는 리스트를 만들어야 할 때는 다음과 같은 선택지가 있다.
1) mutable 컬렉션
val list: MutableList<Int> = mutableListOf()
- 컬렉션의 구현 내부에 변경 가능 지점이 있음
- 멀티스레드 처리 시 동기화 문제가 발생할 수 있음
2) mutable 프로퍼티
val list: List<Int> = listOf()
- 프로퍼티 자체가 변경 가능 지점임
- mutable 컬렉션보다 멀티스레드 처리의 안정성이 좋음
- 이렇게 사용하면 프로퍼티를 Observable하게 만들 수 있음 (Delegates.observable를 사용해서)
이 중에서 두 번째 방법인 mutable 프로퍼티를 사용하는 편이 객체 변경을 제어하기에 더 좋다.
이때, private set을 함께 활용할 수 있다.
var announcements = listOf<Announcement>()
private set
절대로 mutable 프로퍼티 + mutable 컬렉션을 함께 쓰지 말자.
이렇게 되면 변경 가능한 지점이 두 군데나 생긴다.
// Never...
val list = mutableListOf<Int>()
4. 변경 가능 지점 노출하지 말기
상태를 나타내는 mutable한 객체를 외부에 노출하는 것은 굉장히 위험하다. 참조를 통해 값이 바뀌어버릴 수 있기 때문이다. 이러한 노출을 막는 방법엔 다음 두 가지가 있다.
1) 방어적 복제
class UserHolder {
private val user: MutableUser()
fun get(): MutableUser {
return user.copy()
}
//...
}
- 리턴되는 mutable 객체를 복제한다
- mutableUser가 data class로 선언되었다면 copy() 메서드를 활용하면 된다
2) 타입 캐스팅으로 가변성 제한
data class User(val name: String)
class UserRepostiory {
private val storedUsers: MutableMap<Int, String> =
mutableMapOf()
fun loadAll(): Map<Int, String> {
return storedUsers
}
//...
}
- map, filter 등과 동일하게 읽기 전용 슈퍼타입으로 업캐스트하여 가변성을 제한한다
5. 정리
가변성을 제한하기 위한 방법으로 다음과 같은 규칙들이 있다.
- mutable 프로퍼티(=var)보다는 immutable 프로퍼티(=val)를 사용하는 것이 좋다
- mutable 객체와 클래스보다는 immutable 객체와 클래스를 사용하는 것이 좋다
- 변경이 필요하다면 immutable data class로 만들고 copy를 활용하자
- 컬렉션에 상태를 저장해야 한다면 mutable 컬렉션보다는 읽기 전용 컬렉션을 사용하는 것이 좋다
- 변경 가능 지점을 최소화하자
- mutable 객체는 노출하지 않는 것이 좋다
이와 별개로 효율성의 문제로 mutable 객체를 사용하는 편이 더 좋을 때가 있으며, 그리고 immutable 객체를 사용할 땐 멀티스레드 환경에서 주의해야 한다.
'책 > Effective Kotlin' 카테고리의 다른 글
[이펙티브 코틀린] 아이템7: 결과 부족이 발생할 경우 null과 Failure를 사용하라 (0) | 2023.07.04 |
---|---|
[이펙티브 코틀린] 아이템5: 예외를 활용해 코드에 제한을 걸어라 (0) | 2023.07.03 |
[이펙티브 코틀린] 아이템4: inferred 타입으로 리턴하지 말자 (0) | 2023.07.02 |
[이펙티브 코틀린] 아이템3: 최대한 플랫폼 타입을 사용하지 말라 (0) | 2023.07.01 |
[이펙티브 코틀린] 아이템2: 변수의 스코프를 최소화하라 (0) | 2023.06.30 |
본문은 Effective Kotlin을 읽고 간단하게 정리한 글입니다. 필요에 따라 생략/수정된 부분이 있을 수 있으며, 내용이 추후 변경될 수 있습니다.
1. mutable의 단점 + immutable의 장점
먼저 살펴보기에 앞서 mutable의 단점과 immutable의 장점을 살펴보자. mutable의 단점을 뒤집으면 immutable의 장점이 된다.
mutable 객체의 문제
- 상태들 간의 관계를 이해하고, 상태의 변경을 추적해야 하므로 프로그램을 이해하고 디버그하기 힘들어진다.
- 시점에 따라 값이 달라질 수 있으므로 코드의 실행을 추론하기 어렵다
- 멀티스레드 프로그램일 때 적절한 동기화가 필요하다
- 상태 변경이 많을수록 더 많은 조합을 테스트해야 하므로 테스트하기 어렵다
- 컬렉션 내의 객체가 가변 객체로 이루어져 있으면 컬렉션의 일관성이 깨질 수 있다
immutable 객체의 장점
- 상태가 바뀌지 않으므로 코드를 이해하기 쉬우며, 코드의 실행을 추론하기 어렵다.
- 병렬 처리가 쉽다
- 참조가 바뀌지 않으므로 쉽게 캐시할 수 있다
- 방어적 복사본을 만들 필요도 없고, 깊은 복사도 필요없다
- set 또는 map의 key로 사용할 수 있다
2. 코틀린에서 가변성을 제한하는 방법
1) 읽기 전용 프로퍼티(val)
일반적으로는 값을 바꾸지 못하지만, 아래와 같은 경우엔 바꿀 수가 있다. 참고로 읽기 전용은 재할당만 불가능하다는 의미이다. 프로퍼티를 읽을 수만 있다는 것(읽기 전용)과 변경할 수 없음(불변)을 구분지어서 생각해야 한다.
- mutable 객체를 담고 있으면 바꿀 수 있다
val list = mutableListOf(1, 2, 3)
list.add(4)
- var 프로퍼티를 사용하는 val 프로퍼티
var name: String = "John"
val fullName: String
get() = "King $name" // name이 바뀌면 같이 바뀜
println(fullName) // King John
name = "Sam"
println(fullName) // King Sam
또한, val을 사용하면 스마트 캐스팅이 가능하다.
- var은 안됨 (바뀔 수 있으므로)
- 커스텀 게터를 사용하는 경우에도 안됨 (var 프로퍼티를 사용할 수 있으므로)
var name: String? = "John"
val fullName: String?
get() = name?.let { "King $name" }
println(greeting) // King John
name = null
println(geeting) // null --> null일지 아닐지 모른다!
이외에도 val을 var로 오버라이딩 하는 것도 가능하다. 오버라이딩을 할 때 더 넓은 범위로만 접근 제어자를 설정할 수 있는 것과 같은 이치다.
interface Element {
val active: Boolean // getter만 있음
}
class ActualElement: Element {
override var active: Boolean = false // getter와 setter 모두 있음
}
2) 가변 컬렉션과 읽기 전용 컬렉션 구분하기
코틀린 컬렉션 인터페이스 계층을 살펴보면 읽기 전용과 가변 컬렉션이 아예 나눠지는 것을 알 수 있다.

val과 마찬가지로 읽기 전용 컬렉션이라고 해서 내부의 값을 못 바꾸는 것은 아니다. 아래는 map의 구현 예시인데, 이처럼 mutable한 구현체를 immutable한 인터페이스로 업캐스팅해서 반환하기 때문에 그렇게 보이는 것이다.
inline fun <T, R> Iterable<T>.map(
transformation: (T) -> R
): List<R> {
val list = ArrayList<R>() // mutable함
for (elem in this) {
list.add(transformation(elem))
}
return list // immutable한 타입으로 리턴됨
}
즉, 코틀린은 내부적으로 immutable하지 않은 컬렉션을 외부적으로 immutable하게 보임으로써 안정성을 얻고 있다. 따라서 절대로 임의로 다운캐스팅을 해서는 안 되며, 읽기 전용 컬렉션을 가변으로 바꾸려면 toMutableList와 같은 메서드를 사용해야 한다.
// Bad
val list = listOf(1,2,3)
if (list is MutableList) {
list.add(4) // AbstractList.add()에서 UnsupportedOperationException 발생
}
// Good
val list = listOf(1, 2, 3)
val mutableList = list.toMutableList() // 복제를 통해 새로운 mutable 컬렉션을 만듦
mutableList.add(4)
데이터 클래스의 copy
immutable 객체는 장점이 많지만, 변경할 수 없다는 단점이 있다. 따라서 immutable 객체는 자신의 일부를 수정한 객체를 만들어내는 메서드를 가져야 한다.
class User(
val name: String,
val surname: String
) {
fun withSurname(surname: String) = User(name, surname)
}
그런데, 모든 프로퍼티를 대상으로 이런 함수를 모두 만들어주는 것은 너무 귀찮으므로 data class의 copy 메서드를 사용하는 편이 더 낫다. 이와 같이 immutable한 데이터 모델 클래스를 만들 수 있다.
data class User(
val name: String,
val surname: String
)
val user = User("Wayne", "Rooney")
user = user.copy(surname = "Park")
print(user) // User(name=Wayne, surname=Park)
mutable 객체보단 이렇게 immutable 데이터 모델 클래스를 만드는 것이 나으므로 기본적으로는 이렇게 만드는 것이 좋다. 웬만하면 data class로 만드는 편이 좋지만, JPA Entity를 선언할 때는 절대로 data class로 만들지 말자. (참고)
3. 다른 종류의 변경 가능 지점
변경할 수 있는 리스트를 만들어야 할 때는 다음과 같은 선택지가 있다.
1) mutable 컬렉션
val list: MutableList<Int> = mutableListOf()
- 컬렉션의 구현 내부에 변경 가능 지점이 있음
- 멀티스레드 처리 시 동기화 문제가 발생할 수 있음
2) mutable 프로퍼티
val list: List<Int> = listOf()
- 프로퍼티 자체가 변경 가능 지점임
- mutable 컬렉션보다 멀티스레드 처리의 안정성이 좋음
- 이렇게 사용하면 프로퍼티를 Observable하게 만들 수 있음 (Delegates.observable를 사용해서)
이 중에서 두 번째 방법인 mutable 프로퍼티를 사용하는 편이 객체 변경을 제어하기에 더 좋다.
이때, private set을 함께 활용할 수 있다.
var announcements = listOf<Announcement>()
private set
절대로 mutable 프로퍼티 + mutable 컬렉션을 함께 쓰지 말자.
이렇게 되면 변경 가능한 지점이 두 군데나 생긴다.
// Never...
val list = mutableListOf<Int>()
4. 변경 가능 지점 노출하지 말기
상태를 나타내는 mutable한 객체를 외부에 노출하는 것은 굉장히 위험하다. 참조를 통해 값이 바뀌어버릴 수 있기 때문이다. 이러한 노출을 막는 방법엔 다음 두 가지가 있다.
1) 방어적 복제
class UserHolder {
private val user: MutableUser()
fun get(): MutableUser {
return user.copy()
}
//...
}
- 리턴되는 mutable 객체를 복제한다
- mutableUser가 data class로 선언되었다면 copy() 메서드를 활용하면 된다
2) 타입 캐스팅으로 가변성 제한
data class User(val name: String)
class UserRepostiory {
private val storedUsers: MutableMap<Int, String> =
mutableMapOf()
fun loadAll(): Map<Int, String> {
return storedUsers
}
//...
}
- map, filter 등과 동일하게 읽기 전용 슈퍼타입으로 업캐스트하여 가변성을 제한한다
5. 정리
가변성을 제한하기 위한 방법으로 다음과 같은 규칙들이 있다.
- mutable 프로퍼티(=var)보다는 immutable 프로퍼티(=val)를 사용하는 것이 좋다
- mutable 객체와 클래스보다는 immutable 객체와 클래스를 사용하는 것이 좋다
- 변경이 필요하다면 immutable data class로 만들고 copy를 활용하자
- 컬렉션에 상태를 저장해야 한다면 mutable 컬렉션보다는 읽기 전용 컬렉션을 사용하는 것이 좋다
- 변경 가능 지점을 최소화하자
- mutable 객체는 노출하지 않는 것이 좋다
이와 별개로 효율성의 문제로 mutable 객체를 사용하는 편이 더 좋을 때가 있으며, 그리고 immutable 객체를 사용할 땐 멀티스레드 환경에서 주의해야 한다.
'책 > Effective Kotlin' 카테고리의 다른 글
[이펙티브 코틀린] 아이템7: 결과 부족이 발생할 경우 null과 Failure를 사용하라 (0) | 2023.07.04 |
---|---|
[이펙티브 코틀린] 아이템5: 예외를 활용해 코드에 제한을 걸어라 (0) | 2023.07.03 |
[이펙티브 코틀린] 아이템4: inferred 타입으로 리턴하지 말자 (0) | 2023.07.02 |
[이펙티브 코틀린] 아이템3: 최대한 플랫폼 타입을 사용하지 말라 (0) | 2023.07.01 |
[이펙티브 코틀린] 아이템2: 변수의 스코프를 최소화하라 (0) | 2023.06.30 |