본문 바로가기

Reading Record/이펙티브 코틀린

[이펙티브 코틀린] Item 45. 불필요한 객체 생성을 피하라

불필요한 객체 생성을 피하는 것은 최적화의 관점에서 좋다.

  • JVM에서는 하나의 가상 머신에서 동일한 문자열을 처리하는 코드가 여러개 있다면, 기존의 문자열을 재사용
  • Interger와 Long 처럼 박스화한 기본 자료형도 작은 경우에 재사용
    • Int 의 경우 -128 ~ 127 까지 캐싱
  • nullable 타입은 int 자료형 대신 Integer 자료형을 사용하게 강제
    • Int를 사용하면 일반적으로 기본 자료형 int 로 컴파일
    • 하지만 nullable로 만들거나, 타입 아규먼트로 사용할 경우에는 Interger로 컴파일된다.

어떠한 객체를 wrap 할 경우 크게 세 가지의 비용이 발생한다.

  • 객체는 더 많은 용량을 차지
  • 요소가 캡슐화되어 있다면, 접근에 추가적인 함수 호출이 필요
  • 객체는 생성되어야하고, 메모리 영역에 할당되고, 이에 대한 레퍼런스를 만드는 등의 작업이 필요

객체 선언

  • 객체를 재사용하는 간단한 방법은 객체 선언을 사용하는 것(싱글톤)
sealed class LinkedList<T>

class Node<T>(
        val head: T,
        val tail: LinkedList<T>
): LinkedList<T>()

class Empty<T>: LinkedList<T>()

fun main() {
    val list: LinkedList<Int> =
            Node(1, Node(2, Node(3, Empty())))
    val list2: LinkedList<String> =
            Node("a", Node("b", Empty()))
}
  • 위 예제의 문제점은 매번 Empty 인스턴스를 새로 만들어야 한다는 점
  • Empty 인스턴스를 미리 하나만 만들고, 다른 리스트에서 활용할 수 있게 한다하여도 제네릭 타입이 일치하지 않아서 문제가 될 수 있다
sealed class LinkedList<out T>

class Node<T>(
        val head: T,
        val tail: LinkedList<T>
): LinkedList<T>()

object EmptyWithNothing: LinkedList<Nothing>()

fun main() {
    val list1: LinkedList<Int> =
            Node(1, Node(2, EmptyWithNothing))

    val list2: LinkedList<String> =
            Node("a", EmptyWithNothing)
}
  • 빈 리스트는 다른 모든 타입의 서브타입이어야 함.
  • 따라서 Nothing 리스트를 만들어서 사용하면 해결
  • Nothing은 모든 타입의 서브타입이므로 리스트가 covariant(out 한정자)면, LinkedList<Nothing>은 모든 LinkedList의 서브타입이 됨
  • 이러한 방식은 immutable sealed 클래스를 정의할 때 자주 사용

캐시를 활용하는 팩토리 함수

  • 팩토리 함수는 캐시를 가질 수 있다, 따라서 팩토리 함수는 항상 같은 객체를 리턴하게 만들 수도 있다

emptyList 구현

  • 코루틴의 Dispatchers.Default 는 쓰레드 풀을 가지고 있으며, 어떤 처리를 시작하라고 명령하면 사용하고 있지 않은 쓰레드 하나를 사용해 명령을 수행
    • 데이터베이스도 비슷한 형태로 커넥션 풀 사용
  • 객체 생성이 무겁거나, 동시에 여러 mutable 객체를 사용해야 하는 경우에는 객체 풀을 사용하는 것이 도움이 됨
  • Parameterized 팩토리 메소드도 캐싱을 활용할 수 있음
class ParameterizedFactory {
    private val connections = mutableMapOf<String, String>()
    
    fun getConnection(host: String) =
            connections.getOrPut(host) { "createConnection" }
}
  • 모든 순수 함수는 캐싱을 활용할 수 있음. 이를 메모이제이션(memoization)이라고 부름
  • 하지만 캐시는 더 많은 메모리를 사용한다는 단점이 있음.
    • 메모리 문제로 크래시가 생긴다면 메모리를 해제해 주면 된다

SoftReference

  • SoftReference는 가비지 컬렉터가 값을 정리할 수도 있고, 정리하지 않을 수도 있음
  • 메모리가 부족해서 추가로 필요한 경우에만 정리
  • 캐시를 만들 때는 SoftReference를 사용하는 것이 좋다.

WeakReference

  • WeakReference는 가비지 컬렉터가 값을 정리하는 것을 막지 않는다.
  • 따라서 다른 레퍼런스가 이를 사용하지 않으면 곧바로 제거

무거운 객체를 외부 스코프로 보내기

  • 컬렉션 처리에서 이루어지는 무거운 연산은 컬렉션 처리 함수 내부에서 외부로 빼는 것이 좋다
fun <T: Comparable<T>> Iterable<T>.countMax(): Int = 
	count { it == this.max() }
  • 위 코드는 매 반복마다 max 값을 확인하므로 성능에 좋지 않기에 이를 외부로 빼야한다
fun <T: Comparable<T>> Iterable<T>.countMax(): Int {
	val max = this.max()
	return count { it == max }
}

지연 초기화

  • 만약 A 라는 클래스에 B,C,D 라는 무거운 인스턴스가 필요하다고 가정하면, A를 생성하는 과정이 굉장히 무거워질 것.
  • 내부에 있는 인스턴스들을 지연 초기화 하면, A라는 객체를 생성하는 과정을 가볍게 만들 수 있다.
class A {
    val b by lazy { B() }
    val c by lazy { C() }
    val d by lazy { D() }
}

기본 자료형 사용하기

  • 기본적인 요소를 나타내기 위한 특별한 기본 내장 자료형
  • nullable 타입을 연산할 때나 타입을 제네릭으로 사용할 때 기본 자료형을 wrap 한 자료형이 사용
  • 굉장히 큰 컬렉션을 처리할 때 기본 자료형과 wrap한 자료형의 성능차이가 크다