Hanbit the Developer

Effective Kotlin | 7장. 비용 줄이기 본문

Mobile/Kotlin

Effective Kotlin | 7장. 비용 줄이기

hanbikan 2023. 9. 11. 21:13

아이템 45: 불필요한 객체 생성을 피하라

객체 생성 비용

  • 실제로 필요한 데이터보다 더 많은 용량을 차지한다.
  • 추가적인 함수 호출이 필요하다.
  • 생성되는 과정에 비용이 있다. 객체가 생성되고 메모리 영역에 할당되고 래퍼런스를 만드는 등의 작업이 필요하다.

객체 생성 줄이기

  • 싱글톤 활용
    • 예시: LinkedList 클래스에서 Empty가 자주 사용되기 때문에 object를 붙여 싱글톤으로 활용한다.
  • 캐시 활용(Memoization): 메모리를 활용하여 시간을 줄인다.
    • 메모리 관리를 위해 SoftReference 사용하면 좋다.
    *WeakReference vs SoftReference: WeakReference는 래퍼런스가 끊기고 GC 타이밍이 되면 GC를 곧바로 하지만, SoftReference는 다 같으나 메모리에 여유가 있으면 GC를 하지 않는다.(참고: https://aroundck.tistory.com/477)
  • 무거운 객체(작업)를 외부 스코프로 보내기
    • 예시:
    // Before
    return count { it == this.max() }
    
    // After
    val max = this.max()
    return count { it == max }
    
  • 지연 초기화
    • by lazy 등 활용
    • 다만 빠르게 실행되어야 하는데 이로 인해 지연되는 경우나 성능 테스트가 복잡해지는 경우 등 부적절한 상황이 있음에 유의한다.
  • 기본 자료형 사용하기: nullable 지양하기Kotlin Java
    Int int
    Int? Integer
    List<Int> List<Integer>

아이템 46: 함수 타입 파라미터를 갖는 함수에 inline 한정자를 붙여라

장점

  • 타입 아규먼트를 reified로 사용할 수 있다.
  • 함수 타입 파라미터를 가진 함수가 훨씬 빠르게 동작한다.
    • 함수 호출과 리턴을 위해 점프하는 과정과 백스택을 추적하는 과정이 없어지기 때문이다.
  • non-local return을 사용할 수 있다.
    • 예시:
    fun getSomeMoney(): Money? {
    	// repeat에 inline 한정자가 있기 때문에 가능하다.
    	repeat(100) {
    		val money = searchForMoney()
    		if (money != null) return money
    	}
    	return null
    }
    

단점

  • inline 함수는 재귀적으로 사용할 수 없으며, IntelliJ가 오류를 잡아 주지도 못하므로 매우 위험하다.
  • private, internal 가시성에 접근할 수 없다.
    • 예시:
    internal inline fun read() {
    	val reader = Reader() // 오류
    	// ..
    }
    
    private class Reader {
    	// ...
    }
    
  • 컴파일 시 코드가 매우 증가할 우려가 있다.

crossinline과 noinline

  • crossinline: inline을 적용하되, non-local return을 하는 함수를 받지 못하게 한다.
  • noinline: 해당 함수의 인라인화를 방지한다.

*관련 질문 답변 by ChatGPT: https://chat.openai.com/share/169fb63b-98f6-427e-b662-76783d76d8b9

*책에서 “아규먼트로 인라인 함수를 받을 수 없게 만듭니다.”라는 표현이, 저에게는 ‘inline 한정자가 붙어있는 함수를 넣을 수 없다’라고 읽혔습니다. inline 함수를 noinline이 붙은 파라미터로 넣어보았는데 문제가 없었습니다.

결론

inline 주요 사례는 다음과 같다:

  • print 함수처럼 매우 많이 사용되는 경우
  • filterIsInstance 함수처럼 타입 아규먼트로 reified 타입을 전달받는 경우

  • 함수 타입 파라미터를 갖는 톱레벨 함수를 정의해야 하는 경우

API를 정의할 때 인라인 함수를 사용하는 경우는 거의 없다.

아이템 47: 인라인 클래스의 사용을 고려하라

inline class Name(private val value: String) { /* ... */ }

// Kotlin
val name: Name = Name("Marcin")

위 코드를 컴파일하면, 다음과 같이 프로퍼티로 교체된다.

// After compile
val name: String = "Marcin"

inline 클래스의 메서드는 정적 메서드로 만들어진다.

inline class Name(private val value: String) {
	fun greet() {
		print("Hi, I'm $value")
	}
}

// Kotlin
val name: Name = Name("Marcin")
name.greet()
// After compile
val name: String = "Marcin"
Name.'greet-impl'(name)

inline 클래스가 사용되는 경우

  • 측정 단위를 표현할 때
    • 예시: fun callAfter(time: Int, callback: () → Unit)보다는 fun callAfter(time: Millis, callback: () → Unit)처럼 래핑하는 것이 낫다.
  • 타입 오용으로 발생하는 문제를 막을 때
    • 예시:
    class Grades(
    	val studentId: Int
    )
    
    위 코드를 아래처럼 바꿈으로써 잘못된 값을 넣는 상황을 방지한다.
  • class Grades( val studentId: StudentId )

인라인 클래스와 인터페이스

인라인 클래스가 인터페이스를 구현할 수 있으나, inline으로 동작하지 않는다. 따라서 인터페이스 구현 시에는 inline이 불필요하다.

typealias

아래와 같이 인라인 클래스처럼 활용할 수 있다.

typealias Millis = Int

하지만 서로 다른 요소를 혼용하기 쉽다. 아래처럼 혼용하여도 컴파일 오류가 발생하지 않는다.

typealias Millis = Int
typealias Seconds = Int

fun main() {
	val seconds: Seconds = 10
	val millis: Millis = seconds // 컴파일 오류가 발생하지 않습니다.
}

아이템 48: 더 이상 사용하지 않는 객체의 레퍼런스를 제거하라

쓸데없는 최적화가 모든 악의 근원이라는 말이 있으나, null로 설정하는 것은 그리 어렵지 않으므로 무조건 하는 것이 좋다.(이로 인해 가비지 컬렉터가 이를 처리하게 된다.)

특히, Any 또는 제네릭 타입과 같이, 어떤 것이 오는지 알 수 없는 경우 더욱 신경써야 한다.

SoftReference나 WeakReference를 사용하는 것도 권장된다.

heap profiler나 LeakCanary를 사용하여 메모리 누수를 체크하는 것도 좋다.

사실 스코프가 정의되어 있는 것이 일반적이기에 레퍼런스가 제거될 때 객체가 자동으로 해제된다.(객체를 수동으로 해제하는 경우는 드물다.) 따라서 가장 좋은 방법은, 변수의 스코프를 최소화하고, 톱레벨 프로퍼티 또는 객체(object, companion object)로 큰 데이터를 저장하지 않는 것이다.