지난 1편에서는 이미지를 더 낮은 해상도로 서브샘플링을 통해 더 효율적인 연산량과 메모리 사용량을 가져가는 방법에 대해 이야기했었습니다.
지난 포스팅에서도 이야기했다시피, 다운샘플링은 그자체로 상당한 컴퓨팅 자원 절약을할 수 있지만 완벽하게 최적화되진 않습니다. 이전에 로드했던 이미지 혹은 같은 이미지라도 매번 개별적으로 디코드작업을 수행해야하기 때문입니다. 이는 불필요한 컴퓨팅 자원 소비가 될 수 있고, 이미지뷰에 지속적으로 set해주는 작업으로인한 불필요한 메모리 할당이될 수 있습니다.
이를 해결하기 위해서는 이전에 로드했던 이미지(비트맵)는 메모리, 혹은 하드디스크상에 따로 저장해두고, 필요할 때마다 꺼내쓸 필요가 있습니다. 이 방식이 오늘 이야기해볼 Cache라는 기술입니다. 안드로이드에서는 이러한 비트맵의 효율적인 캐싱을 위해 LruCache<K, V> 를 제공해줍니다.
Glide를 사용하면서, 내부적으로 LRU Cache를 사용한다고 얘기를 많이 들어봤을것입니다.
Android에서는 이를 Collection의 한 형태로 제공하고 있습니다.
이번 포스팅은 LruCache의 내부동작에 이야기 해보려고 합니다.
한번 내부를 뜯어보도록 합시다.
LRU Cache의 기본적인 개념
LRU는 Least Recently Used, 최근에 사용되지 않는 페이지를 교체하는 알고리즘 입니다.
LRU Cache는 객체를 생성할때, size를 고정적으로 정해야합니다
그리고 데이터가 추가되어 사이즈가 넘었을때,가장 오래된 데이터를 삭제시킵니다.
그렇다면 저장되어 있는 녀석을 읽는다면 어떻게 될까요?
get()을 해서 읽는다면 해당 데이터는 최근으로(가장 뒤) 이동하게 됩니다.
이렇게 주로 쓰이지 않는 데이터는 삭제시키고, 주로 쓰이는 데이터는 계속 머무르게 됩니다.
LRU Cache의 장단점
자주 사용되는 데이터를 캐시에 유지하여 성능을 최적화할 수 있다는 장점이 있습니다. 왜냐하면 자주 사용되는 데이터는 앞으로 사용될 가능성이 높기 때문입니다.
반면, 데이터의 접근 시간을 기록하고 갱신하는데, 추가적인 연산과 메모리가 필요하다는 단점이 있습니다.
LRU Cache의 목적
Cache란 자주 사용하는 데이터나 값을 미리 복사해 놓는 임시 장소를 가리킵니다.
그렇다면 자주 사용되는 데이터를 저장해야겠죠? LRU는 이를 처리하기 위한 알고리즘 입니다.
가장 오랫동안 참조되지 않는 녀석을 캐시에서 제거하고, 반복되는 녀석들만 남겨서 히트율(캐시에서 재사용 된것을 히트했다고 말합니다.)을 높이는것 이죠.
LRU Cache의 내부구조
Android에서 LRU Cache는 LinkedHashMap으로 구현됩니다.
HashMap 이 아닌 LinkedHashMap 을 사용하는 다음 두가지 이유가 있었습니다.
첫 번째는 입력된 순서를 보장합니다.
어떻게 순서가 보장될 수 있을까요?
바로 HashMap과 Double Linked List를 같이 사용하고 있기 때문입니다.
버킷안에 데이터와 Node를 저장하고, 다음 위치를 저장하고 있기때문에 iterate시 순서를 보장하는것이죠.
즉, LinkedHashMap 은 Node가 삽입될 때, 앞 또는 뒤의 노드의 주소값을 연결해주기 때문에, 입력 순서가 보장될 수 있습니다.
다음 코드와 같이, LinkedHashMapEntry 에서before, after를 각각 선언해주어 doubly-Linked 방법을 사용하는 것을 볼 수 있습니다.
static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
LinkedHashMapEntry<K,V> before, after;
LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
두 번째는 LRU의 특성인 엑세스한 Node는 Map의 제일 뒤로 보낼 수 있습니다. LinkedHashMap 원문 주석에는 다음과 같이 마지막에 참조한 Entry는 least-recently-accessed 형태로 정렬할 수 있어 LRU 캐시 구현에 적합하다고 설명하고 있습니다.
cache.put("A", 0) // [A]
cache.put("B", 0) // [A, B]
cache.put("C", 0) // [A, B, C]
cache.put("D", 0) // [A, B, C, D]
cache.put("E", 0) // [A, B, C, D, E] - A부터 E까지 캐싱 완료
cache.put("F", 0) // [B, C, D, E, F] - F를 캐싱하면, A는 제거됨
cache.put("D", 0) // [B, C, E, F, D] - D를 다시 캐싱하면 최근 참조된 상태로 변경
println(cache.snapshot().keys)
cache.get("C") // [B, E, F, D, C] - C를 통해 캐시된 데이터 접근시 최근 참조된 상태로 변경
println(cache.snapshot().keys)
cache.get("B")
println(cache.snapshot().keys)
// result
[B, C, E, F, D]
// C가 뒤로 이동했다.
[B, E, F, D, C]
// B가 뒤로 이동했다.
[E, F, D, C, B]
LinkedHashMap은 동기화를 보장하지 않는다는데..
LinkedHashMap은 HashMap과 구조적으로 같아서, 동기화처리가 되어있지 않기 때문에 Multi Thread 환경에서 사용이 힘들다고 적혀있습니다.
Android의 Lru Cache는 이를 해결하기위해 synchronized() 블록을 이용해 해결하고 있습니다.
이를 통해 쓰레드가 동시 접근해도 하나만 접근이 가능하기 때문에 동기화 문제를 해결할 수 있습니다.
LRU Cache 실사용 예
RecyclerView와 같은 View에서 대량의 이미지를 한번에 로드하고 스크롤하여 재사용되는 경우 LRUCache의 사용은 퍼포먼스 개선에 많은 도움이 됩니다. 많은 Bitmap을 메모리에 캐시하는 것은 부담될 수 있으므로 DiskLruCache와 같은 디스크 캐싱을 같이 사용하면 좀 더 메모리 부담은 줄일 수 있습니다. 실제로 Bitmap캐싱을 최적화하여 제공하고 있는 라이브러리가 Glide같은 라이브러리입니다.
마치며
이번 포스팅에서 LruCache에 내부동작에 대해 이야기해보았습니다. 캐시를 사용할 때 중요한 점은 maxSize를 사용하는 환경에 맞게 효율적으로 지정해야하는 것입니다. 용량이 너무 크면, OOM 위험성이 커지고, 너무 작으면 캐시 히트율이 급격히 떨어지기 때문입니다. 다음 포스팅에서는 비트맵을 직접 캐시해보고 메모리, 수행 시간 측면에서의 효율성에 대해 이야기해볼 예정입니다.
참고자료
[Android] Glide, Picasso 와 같은 라이브러리는 어떻게 이미지 로드를 최적화할까? — 2편 : LRU Cache
지난 1편에서는 이미지를 더 낮은 해상도로 서브샘플링을 통해 더 효율적인 연산량과 메모리 사용량을 가져가는 방법에 대해 이야기했었습니다.
easternkite.medium.com
[Android] LRU Cache 살펴보기
Glide를 사용하면서, 내부적으로 LRU Cache를 사용한다고 얘기를 많이 들어봤을것입니다. Android에서는 이를 Collection의 한 형태로 제공하고 있습니다.
medium.com
https://f-lab.kr/insight/understanding-lru-cache-20240605
LRU 캐시 알고리즘의 이해와 구현
이 글은 LRU(Least Recently Used) 캐시 알고리즘의 동작 원리와 구현 방법을 설명합니다. LRU 알고리즘의 장단점과 다양한 응용 분야를 다루며, 자바로 구현한 간단한 예제를 제공합니다.
f-lab.kr
안드로이드에서 LruCache를 파헤치기 | 찰스의 안드로이드
LruCache란? 면접 또는 코딩테스트에서 흔히 접할 수 있는 주제가 바로 LruCache다. 안드로이드에서는 LruCache가 어떻게 동작하고, 언제 그리고 어디서 사용되는지 한번 알아보도록 하자. LruCache에서 Lr
charlezz.com
'Android > Image Loading' 카테고리의 다른 글
[Android] 이미지 로딩 라이브러리 (Glide vs Picasso / Coil) (0) | 2025.01.22 |
---|---|
[Android] 이미지 로딩에 대한 고찰 (feat. 메모리 캐시 vs 디스크 캐시) (0) | 2025.01.21 |
[Android] 어떻게 이미지 로드를 최적화하면 좋을까?(1) - Down Sampling (1) | 2025.01.21 |
[Android] 비트맵(Bitmap)이란? (0) | 2025.01.20 |