본문 바로가기

안드로이드/Kotlin

5. Effective Kotlin - 효율성

효율성

과거에 비해 메모리의 값이 저렴해지고 개발자의 몸 값이 오르면서, 메모리 효율적인 코드를 작성하는 것에 대해 관대함이 생겼다. 하지만, 효율성을 고려한 코드는 대부분의 경우에서 옳다. 그 과정에서 가독성과 효율성의 trade-off도 고려한다면 최적이다.

불필요한 객체 생성을 피하라

객체 생성에는 언제나 비용이 들어간다. 다양한 레벨에서 객체 생성을 피할 수 있다. 예를들어 JVM에서는 하나의 가상 머신에서 동일한 문자열을 처리하는 코드가 여러 개가 존재할 경우, 기존 문자열을 재활용한다. 또한, Integer, Long과 같은 박싱 타입도 작은 수에서는 재활용한다.

val str1 = "Kotlin"
val str2 = "Kotlin"
print(str1 == str2)  //true
print(str1 === str2) //true

val num1: Int = 111
val num2: Int = 111
print(num1 == num2)  //true
print(num1 === num2) //true, num2를 캐시에서 읽어들인다.

val num1: Int? = 130
val num2: Int? = 130
print(num1 == num2)  //true
print(num1 === num2) //false, num2를 캐시에서 읽어들이지 않는다.

(참고로, Int? 는 Integer로 컴파일되고, Int는 integer로 컴파일된다.)

캐싱을 활용하기

모든 함수는 캐싱을 활용할 수 있다. 이를 Memoization이라고 한다. 피보나치를 캐싱하면 다음과 같다.

private val FIB_CACHE = mutableMapOf<Int, BigInteger>()

fun fib(n: Int): BigInteger = FIB_CACHE.getOrPut(n) {
  if (n<=1) BigInteger.ONE else fib(n-1) + fib(n-2)
}

이 때, 시간 효율은 높지만 캐시를 위한 Map을 저장하기 때문에 메모리를 많이 사용한다. 만약, 메모리 문제로 크래시가 생긴다면 이를 자동으로 해제해주는 SoftReference를 사용하라.

  • WeakReference: 다른 레퍼런스가 이를 사용하지 않으면 가비지 컬렉터가 값을 정리한다.
  • SoftReference: 가비지 컬렉터가 값을 정리할 수도 있고, 하지 않을 수도 있다. JVM에서는 메모리가 부족할 경우에만 정리한다.

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

컬렉션 처리에서 이루어지는 무거운 연산은 컬렉션 내부에서 외부로 옮기는 것이 좋다. 예를들어 "Iterator의 최대값의 수를 세는 확장 함수"를 만드는 경우를 생각해보자.

fun <T: Comparable<T>> Iterable<T>.countMax(): Int = count { it == this.max() }

위 코드를 조금 더 수정하여 max()countMax의 레벨로 옮길 수 있다.

fun <T: Comparable<T>> Iterable<T>.countMax(): Int {
  val max = this.max()
  return count { it == max }
}

이렇게 될 경우, 처음에 max 값을 한 번만 찾아두고 count를 순회하여 성능 이점이 있다.

이와 비슷한 이슈가 정규표현식에도 있다. 정규표현식 패턴을 컴파일 하는 과정은 매우 복잡하여 성능을 저하시킨다. 따라서, 이를 호출할 때마다 생성하기보다는 top-level로 이동시켜 문제를 방지할 수 있다.

private val IS_VALID_EMAIL_REGEX. "\\A(?:(?:25[0-5]..."
fun String.isValidAddress(): Boolean = matches(IS_VALID_EMAIL_REGEX)

지연 초기화

무언 클래스를 만들 때는 지연되게 만드는 것이 좋을 때가 있다. 예를 들어 A 클래스에 B, C, D라는 무거운 인스턴스가 존재한다고 가정해보자. A를 생성할 때, B, C, D도 모두 생성된다면 생성 과정이 매우 무거울 것이다.

class A {
  val b = B()
  val c = C()
  val d = D()
}
class A {
  val b by lazy { B() }
  val c by lazy { C() }
  val d by lazy { D() }
}

이 경우 단점도 있다. 메서드의 호출은 빨아야되는 경우가 있다. 그 경우, lazy 초기화 후 메서드 호출이 이뤄지기 때문에 느리다.

기본 자료형 사용하기

JVM은 숫자/문자 등의 기본 요소를 처리하기 위해 primitive 자료형을 갖고 있다. 코틀린/JVM에서는 내부적으로 이를 활용한다. 다만, primitive 자료형을 warp한 자료형이 다음과 같은 경우에 사용된다.

  • nullable 타입이 필요할 때 (primitive는 null이 불가능하다)
  • Generic을 사용할 때