LRU 캐시를 이용한 유즈케이스

KSang·2024년 7월 7일
0

TIL

목록 보기
101/101

LRU 캐시란?

LRU(Least Recently Used)는 가장 오랫동안 참조되지 않은 페이지를 교체하는 방식,
사용된지 가장 오래된 페이지는 앞으로도 사용될 확률이 낮다는 가설에 의해 만들어진 알고리즘이다.

이를 통해 불필요한 서버와 통신을 줄여, 사용성을 개선할 생각이다.

data class CacheEntry<VALUE>(
    val value: VALUE,
    val expiryTime: Long,
)


class LruCache<KEY, VALUE>(
    private val maxSize: Int,
    private val expiryDuration: Long
) : LinkedHashMap<KEY, CacheEntry<VALUE>>(maxSize, 0.75f, true) {

    override fun removeEldestEntry(eldest: MutableMap.MutableEntry<KEY, CacheEntry<VALUE>>?): Boolean {
        return size > maxSize
    }

    fun getValidValue(key: KEY): VALUE? {
        if (this[key] != null && this[key]?.expiryTime!! > System.currentTimeMillis()) {
            return this[key]?.value
        } else {
            remove(key)
            return null
        }
    }

    fun putValue(key: KEY, value: VALUE) {
        this[key] = CacheEntry(value, System.currentTimeMillis() + expiryDuration)
    }
}

캐시는 이렇게 구성했다.

키와 결과 값을 타입으로 저장하고 size와 유효기간을 작성한다.

캐시 데이터가 size보다 많이 쌓이면 오랫동안 안 쓴 데이터를 삭제하고

유효기간이 지난 데이터도 삭제한다.

키와 값을 타입으로 받게 했는데, 이게 usecase와 잘 어울린다.

abstract class SuspendUseCase<PARAMS, RESULT> {
    protected abstract suspend fun build(params: PARAMS? = null): RESULT
    open suspend operator fun invoke(params: PARAMS? = null): RESULT = build(params)
}

기존에 쓰던 UseCase의 base다

캡슐화를 해놨는데, 파라미터를 클래스로 만들어서 받는다.

이를 이용해서 캐싱기능이 있는 UseCase를 만들어 보았다.

abstract class CachedSuspendUseCase<PARAMS, RESULT>(
    cacheSize: Int,
    cacheDuration: Long = Long.MAX_VALUE,
) : SuspendUseCase<PARAMS, RESULT>() {

    private val cache = LruCache<Any, RESULT>(cacheSize, cacheDuration)
    private val mutex = Mutex()
    @Volatile
    private var isExecuting = false

    override suspend fun invoke(params: PARAMS?): RESULT =
        mutex.withLock {
            if (isExecuting) throw IllegalStateException("Already executing")
            isExecuting = true

            try {
                val key = params?.toString() ?: this::class
                cache.getValidValue(key) ?: build(params).also {
                    cache.putValue(key, it)
                }
            } finally {
                isExecuting = false
            }
        }
}

파라미터가 null일때는 유즈 케이스를 키로 사용하고, Lru캐시를 이용해 캐싱 기능을 적용했다.


@Singleton
class GetDayWeatherForecastUseCase @Inject constructor(
    private val repository: WeatherForecastRepository
): CachedSuspendUseCase<String, WeatherForecastEntity>(1, 3 * 60 * 60 * 1_000) {
    override suspend fun build(params: String?) = repository.get()
}

프로젝트에서 날씨 예보를 가져오는 유즈케이스 인데,

잘 변하지 않는데, 자주 불리는 api이기 때문에

3시간에 한번 씩 변하도록 캐싱 기능을 넣었다.

결론

캐싱 기능을 사용해, 불필요한 서버와 통신을 줄여 사용성을 향상 시킬 수 있다.

특히, 자주 호출되지만 빈번히 변하지 않는 데이터의 경우에는 매우 유용하게 사용할 수 있다.

커뮤니티 같은 여러 유저들이 사용하며, 데이터가 자주 변할 경우에는 주의가 필요 하다.

Room 같은 데이터베이스 솔루션에 비해 가볍고 메모리 효율적이기 때문에 빠른 응답성이 요구되는 애플리케이션에서 큰 장점을 보일 것 이라고 생각한다.


여담으로.

공모전과 일 때문에 너무 바빠서 그 동안 블로그를 작성하지 못했다...

0개의 댓글