Posts 이펙티브 코틀린 간단 정리
Post
Cancel

이펙티브 코틀린 간단 정리

Image

‘이펙티브 코틀린’의 학습 내용을 간단하게 정리하기 위한 목적의 TIL 포스팅입니다🙆‍♂️

1부: 안정성

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

  • 상태를 가지는 것은 장단점이 있다.
    • 장점: 시간에 따라 변화하는 요소를 표현 가능
    • 단점: 이해하고 디버깅 및 추론하기 어려움, 공유 상태 관리 필요, 테스트하기 어려움

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

1. 읽기 전용 프로퍼티 val

  • var보다는 val을 사용하자.
  • var은 게터이자 세터이므로 val을 var로 오버라이드 가능하다.
1
2
3
4
5
6
interface Element {
  val active: Boolean
}
class ActualElement : Element {
  override var active: Boolean = false // 오버라이드
}
  • 스마트캐스팅이란 코틀린 컴파일러가 자동으로 타입을 좁혀주는 기능이다.
1
2
3
4
5
val str: String? = "Hello"

if (str != null) {
    println(str.length) // ✅ 가능
}
  • 커스텀 게터는 스마트 캐스팅이 불가능하다.
  • 값을 사용하는 시점의 name에 따라 다른 결과가 나올 수 있기 때문
  • 반면 fullName2 논로컬 프로퍼티는 final이면서 사용자 정의 게터가 없기에 스마트 캐스팅이 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
val name: String? = "Marton"
val surname: String = "Braun"

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(fullName2.length)
    }
}

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

  • 코틀린은 읽기 전용 컬렉션과 읽고 쓸 수 있는 컬렉션으로 구분된다.
    • 읽기 전용 : Iterable, Collection, Set, List 인터페이스
    • 읽고 쓸 수 있는 : MutableInterable, MutableCollection, MutableSet, MutableList 인터페이스
  • 읽기 전용 컬렉션은 내부 값을 변경할 수 없는 게 아니라, 인터페이스가 변경 기능을 제공하지 않기 때문에 못 바꾸는 것처럼 보일 뿐이다.
1
2
3
4
5
6
val mutable = mutableListOf(1, 2, 3)
val readOnly: List<Int> = mutable

mutable.add(4)

println(readOnly) // 👉 [1, 2, 3, 4] 읽기 전용 컬렉션 List도 인터페이스에서 변경 메서드가 없을뿐 실제로 변경될 수 있음!
  • Iterable<T>.map 과 Iterable<T>.filter 함수는 ArrayList를 반환한다.
1
2
3
4
5
6
7
8
9
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
}
  • 코틀린에서 읽기 전용 컬렉션을 가변 컬렉션으로 다운캐스팅하면 절대로 안된다.(p11 참고)
    • 만약, 읽기 전용 컬렉션 -> 가변 컬렉션으로 변경해야한다면 List.toMutableList 함수 사용

3. 데이터 클래스의 copy

  • 불변 객체의 데이터 변경 필요시 변경사항을 반영한 새로운 사본을 생성하는 메서드를 사용해야 한다.
  • data 한정자는 copy 메서드를 만들어준다. 물론 프로퍼티에 새로운 값을 지정하는 것도 가능하다.
1
2
3
4
5
6
7
8
data class(
    val name: String,
    val surname: String,
)

var user = User("Maja", "Markiewicz")
user = user.copy(usrname = "Moska") // 새로운 프로퍼티 지정 가능
print(user) // User(name=Maja, surname=Moska)
  • 도메인 모델, DTO, 응답(Response) 모델 생성시 유용할 것 같다.

다른 종류의 변경 지점

1
2
3
4
5
6
7
8
val list1: MutableList<Int> = mutableListOf()
var list2: List<Int> = listOf()

list1.add(1)
list2 = list2 + 1

list1 += 1 // list1.plusAssign(1)
list2 += 1 // list2 = list2.plus(1)
  • list1은 “자기 자신을 변경” 한다.(mutable) => 빠름(메모리 재사용), 사이드 이펙트 있음, 적절한 동기화 기법 필요
  • list2는 “새로운 리스트를 만들어서 교체” 한다. (immutable처럼 동작) => 새로운 리스트 생성, 안전함(불변처럼 동작), 복사 비용 발생

아이템1 요약

  • var 보다는 val을 선호하자.
  • 가변 프로퍼티보다는 불변 프로퍼티를 선호하자.
  • 가변 객체와 클래스보다 불변 객체와 클래스 사용을 선호하자.
  • 불변 객체를 변경해야 한다면 data 클래스로 만들고 copy 사용하는 것을 고려하자.
  • 상태를 저장해야 한다변, 가변 컬렉션보다는 읽기 전용 컬렉션을 선호하자.
  • 변경 지점을 현명하게 설계하고 꼭 필요한 경우에만 생성하자.

아이템2: 임계 영역을 제거하라

  • 아래와 같은 코드는 ‘동시성 문제’가 발생한다.
1
2
3
4
5
6
7
8
9
10
var num = 0
for (i in 1..1000) {
  thread {
    Thread.sleep(10)
    num += 1
  }
}

Thread.sleep(5000)
print(num) // 1000이 출력될 가능성이 거의 없다. 매번 다른 숫자가 출력된다. 동시성 문제..
  • 또한 기본 컬렉션은 순회하는 동안 요소가 수정되는 것을 지원하지 않기에 ConcurrentModificationException 예외가 발생한다.(p19 참고)

코틀린/JVM의 동기화

  • 자바와 동일하게 synchronized 함수를 사용한다.
1
2
3
4
5
6
7
8
9
10
11
val lock = Any()
for (i in 1..1000) {
  thread {
    Thread.sleep(10)
    synchronized(lock) {
      num += 1
    }
  }
}
Thread.sleep(1000)
print(num) // 1000

아토믹 객체

  • 자바에서 AtomicInter, AtomicLong, AtomicBoolean 등의 원자성 클래스로 원자적으로 실행되도록 보장하는 메서드를 제공하며 동시성 문제를 해결 가능하다.

동시성 컬렉션

  • ConcurrentHashMap, ConcurrentHashMap.newKeySet, ConccurrentLinkedQueue를 활용할 수 있다.
  • AtomicFU와 같은 코틀린 멀티플랫폼 라이브러리도 있다.

변경 가능한 지점을 유출하지 마세요

  • 공개 상태를 나타내는데 가변 객체를 사용하여 노출하는 것은 위험하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
data class User(val name: String)

class UseRepository {
  private val users: MutableList<User> = mutableLlistOf()
  
  fun loadAll(): MutableList<User> = users
  //...
}

val users = userRepo.loadAll()
users.add(User("Jeon"))
// ...
println(userRepo.loadAll()) // User(name=Jeon)
  • 이를 해결하기 위한 두 가지 방안이 있다.
    • 1)실제 참조 대신 객체의 복사본을 반환한다.(방어적 복사, 컬렉션은 toList, 데이터 클래스는 copy 메서드)
    • 2)읽기 전용 리스트를 사용한다.(멀티스레드 접근을 헐용하려면 리스트 수정 연산만 synchronized로 동기화, p27 참고)

아이템2 요약

  • 여러 스레드가 공유 자원에 접근시 예외 및 예상치 못한 동작이 발생할 수 있다.
  • 동시 수정으로부터 상태 보호를 위해 동기화를 사용할 수 있다.(synchronized)
  • 동시 수정을 처리하기 위해 자바는 Atomic 클래스와 동시성 컬렉션을 제공한다.
  • 클래스는 내부 상태를 보호해야 하며, 밖으로 노출해서는 안된다. 읽기 전용 객체로 작업을 수행하거나 방어적 복사를 이용하여 동시 수정으로부터 상태를 보호할 수 있다.

아이템3: 가능한 한 빨리 플랫폼 타입을 제거하라

  • 코틀린에서는 자바의 널 여부가 확인되지 않는 타입을 널 가능 타입으로 처리하는 대신 특별한 타입으로 처리하고 이를 ‘플랫폼 타입’이라 한다.
  • 코드에 직접 플랫폼 타입을 정의할 수 없으며, 널 가능 혹은 널 불가능 타입으로 지정하는 방식으로 해당 값을 처리해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
== Java ==
public class UserRepo {
  public User getUser() {
    // ...
  }
}

== Kotlin ==
val repo = UserRepo()
val user1 = repo.user // 플랫폼 타입, nonnull 선언 가능
val user2: User = repo.user // 플랫폼 타입, nonnull 선언 가능
val user3: User? = repo.user // 플랫폼 타입, nullable 선언 가능
  • 플랫폼 타입은 아래와 같은 문제가 발생할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
== Java ==
public class JavaClass {
  public String getValue() {
    return null;
  }
}

== Kotlin ==
fun statedType() {
  val value: String = JavaClass().value // 자바에서 값을 가져오는 라인에서 NPE 발생
  println(value.length)
}

fun platformType() {
  val value = JavaClass().value // nonnull로 선언되었지만 null이 반환되었으므로 여기서 NPE 발생
  println(value)
}
  • 플랫폼 타입은 다른 코드나 실행 흐름으로 더 확산될 수 있다.(p34 참고)
  • 이러한 문제로 인해 코틀린과 상호운용해야 하는 자바 코드에 @Nullable, @NotNull 어노테이션을 적용하자.(javax.annotaion, org.jetbrains.annotations 등 p32 참고)
1
2
3
4
5
public clas UserRepo {
  public @NotNull User getUser() {
    // ...
  }
}
  • 그리고 애플리케이션 안정성을 위해 플랫폼 타입을 가능한 제거하자.

아이템3 요약

  • 다른 언어(ex. Java)에서 왓으며 널 가능성을 알 수 없는 타입을 플랫폼 타입이라 한다.
  • 플랫폼 타입은 위험하므로 전파되지 않도록 가능한 빨리 제거해야 한다.
  • 또한 자바 생성자, 메서드, 필드에 널 간으성을 지정하는 어노테이션을 지정하는 것이 좋다.
This post is licensed under CC BY 4.0 by the author.

자바 개발자를 위한 코틀린 입문 내용 정리

-