[번역] 코틀린에서의 늦은 초기화

명이·2023년 1월 7일
1
post-thumbnail

원문 : Lazy initialization in kotlin

기초

필요할때만 고비용의 객체를 생성하는 것, 필요할때만 데이터베이스를 로드하는것, 필요할때만 캐시를 로드하는 것. 이것들은 리소스 집약적인 작업을 수행하면서 최적화된 코드를 작성하려는 모든 프로그래머를 위한 가이드라인입니다. 성능상의 이유로 프리패치 혹은 즉시 로딩이 필요한 특수한 경우에만 그 반대입니다.

많은 사용 사례는 한번 초기화하고 재사용하는 것입니다. Java에는 이러한 패턴을 지원하는 final 멤버 변수가 있습니다. final 멤버 변수를 가지는 것은 제 마음 속에 매우 중요하기 때문에 항상 코드 리뷰에 final이 없는 이유를 찾습니다. final의 단점은 final 변수는 생성될 때 초기화 되어야한다는 것입니다. 만약 final 변수 참조 비용이 많이 든다면, 그 비용은 선불입니다. 또 다른 선택지는 필요할 때 생성하는 것이지만, 이렇게 된다면, 변수는 final하지 못하고 변경할수 있게 되어 많은 경우에 바람직하지 않습니다. 원하는 결과를 얻기 위해 Java에서 사용할 수 있는 복잡한 해결 방법이 있습니다.

Java에서 사용할 수 있는 복잡한 해결 방법:

  • ConcurrentMapcomputeIfAbsent 사용
  • on-demand 방식으로 final 변수를 추상화하는 객체를 사용하거나 생성

Kotlin에서는 이러한 상황에서 lazy를 사용할 수 있습니다. 간단한 예를 살펴보겠습니다.

class LazyCreator {
    val mammoth by lazy(LazyThreadSafetyMode.PUBLICATION) {
        // 프로퍼티에 접근될때만 생성됨
        Mammoth()
    }
}

// Mammoth 생성은 비싼 연산임
class Mammoth {
    init {
        println("Mammoth is born")
    }

    fun wakeup() {
        println("Mammoth is awake")
    }
}

누군가가 LazyCreator 클래스를 생성한 후 val mammoth에 접근할 때만 이는 생성됩니다. 어떻게 lazy가 이러한 변수들을 생성할까요? 당신은lazy에 대해 더 할기 위해 공식 문서를 볼 수 있습니다. 기본적으로 lazy 실행을 위해 여러개의 전략들이 있습니다. (두개의 thread-safe한 방법과 한가지 thread-safe하지 않은 방법) 저는 그것에 깊에 다가가지 않을 것 입니다. 하지만, 언제 그것을 사용할 지 남겨두겠습니다.

PUBLICATION: 이 전략으로 인해 lazy 블록이 여러 번 실행될 수 있습니다. 그러나 이것을 스레드를 차단하지 않습니다. 블록을 실행하는 첫 번째 스레드의 값은 원자적으로 설정되며 변수에 의해 재사용 됩니다. 이것은 블록이 비용이 많이 들지 않고 멱등성인 경우에 SYNCHRONIZED보다 선호 됩니다.

SYNCHRONIZED: 이 전략은 lazy 블록이 한번만 실행될 것을 보장합니다. 하지만, 이것은 스레드를 차단합니다. 결과는 변수에 의해 캐시되고 재사용됩니다. 이 전략은 lazy 블록이 비용이 많이 드는 연산이거나 멱등적이지 않거나 리소스를 낭비하는 경우 선호됩니다.

NONE: 이 전략은 lazy 블록을 아무런 잠금이나 스레드 안전 전략 없이 실행하고 값을 캐시합니다. 하나의 스레드에서만 변수에 액세스 할수 있다고 확신하는 경우에만 이 전략을 주의해서 사용하세요.

PUBLICATION 모델에 대한 저 나은 정신 모델을 구축하려면, Java Concurrency in Practice 책에 나오는 Memorizer 디자인 패턴을 읽으세요. 이 패턴은 Java ConcurrentMapcomputeIfAbsent API의 뼈대가 되는 패턴입니다.

비동기 lazy

지금까지 우리가 본 lazy 사용법은 리소스 집약적인 작업의 단순한 경우입니다. 그러나 현실 세게에서는 상황이 그렇게 간단하지 않습니다. 네트워크 호출 또는 데이터베이스 로드와 같은 대부분의 리소스 집약적인 작업은 백그라운드 스레드에서 발생해야 합니다. Kotlin에서는 이를 위해 flowssuspend 함수를 사용합니다. lazysuspend 함수를 호출하는 데 사용할 수 없습니다. 여기에는 다른 전략이 필요합니다.

비용이 많이 드는 데이터베이스 작업을 정확히 한번 수행하고 결과를 캐시하고 재사용하는 시나리오를 생각해 봅시다. 좋은 효과를 위해 Deferred객체를 사용할 수 있습니다.

// Model 클래스는 데이터베이스 개체를 의미합니다
data class Model(val name: String)

// 인터페이스는 data access layer (dao)를 의미합니다.
interface ModelDao {
    suspend fun getModels(): List<Model>
}

class AsyncLazy(private val dao: ModelDao) {
    @Volatile private var databaseJob: Deferred<List<Model>>? = null

    suspend fun getModels(): List<Model> {
        return loadFromDatabaseAsync().await()
    }

    suspend fun getModelForName(name: String): Model {
        return loadFromDatabaseAsync().await().first {
            it.name == name
        }
    }

    suspend fun getModelsStartingWith(name: String): List<Model> {
        return loadFromDatabaseAsync().await().filter {
            it.name.startsWith(name)
        }
    }

    /**
     * 데이터베이스 호출을 캡슐화하고 실행하는 메서드
     * 정확히 한번 실행됨
     */
    private suspend fun loadFromDatabaseAsync(): Deferred<List<Model>> {
        val job = databaseJob
        if (job != null) {
            return job
        }

        // Deferred 객체는 여러번 생성될 수 있지만, 첫 번째만 살아남을 것.
        // Deffered를 생성하는 것은 비싼 연산이 아님.
        val deferred = coroutineScope {
            async(Dispatchers.IO) {
                dao.getModels()
            }
        }

        databaseJobValueUpdater.compareAndSet(this, null, deferred)
        return databaseJobValueUpdater.get(this) as Deferred<List<Model>>
    }

    companion object {
        private val databaseJobValueUpdater = AtomicReferenceFieldUpdater.newUpdater(
            AsyncLazy::class.java,
            Deferred::class.java,
            "databaseJob"
        )
    }
}

이 예제에서 데이터베이스 작업이 정확히 한번 발생하고 AsyncLazy에 있는 공용 메서드들이 실행될 때 값을 반복해서 재사용한다는 것을 알수 있습니다. 데이터베이스 값이 필요할 때 Deferred 작업이 생성된 다음 재사용됩니다.

이 접근 방식은 Memoizer 디자인 패턴만큼 효과적입니다. 이 예제에서, 여러 스레드가 Deferred 객체를 생성하게 될 수 있지만 원자적으로 변수에 설정된 스레드가 우선적으로 실행됩니다.


마무리하며

이 글에서 우리는 lazy가 어떻게 Kotlin에서 사용될 수 있는지, 여러 스레딩 옵션과 그것들의 제한을 알아봤습니다. 우리는 또한 비동기 시나리오들을 lazy하게 로딩하는 전략들에 대해서도 배웠습니다. 행복한 lazy 코딩 즐기세요!

profile
tech explorer | https://duge.space/myounJAwobV

0개의 댓글