관리 메뉴

나만을 위한 블로그

[Android] 이미지 캐시 본문

Android

[Android] 이미지 캐시

참깨빵위에참깨빵_ 2022. 10. 3. 18:18
728x90
반응형

이미지 캐시에 관해선 Glide 등 성능이 검증된 여러 좋은 오픈소스 라이브러리들이 많이 있고 나도 Glide를 위주로 사용하고 있다. 그러나 이미지 캐시라는 근본적인 개념을 모른 채 개발하는 건 아니라고 생각되서 쓴다.

 

먼저 이미지 캐시에 관해선 2013년도에 네이버 D2에 작성된 글을 먼저 보고 가는 게 좋겠다. 내용 중 지금은 deprecated된 AsyncTask가 간혹 쓰였던 걸로 추정되는 9년 전 글이지만 읽어볼 가치는 충분하다고 생각한다.

 

https://d2.naver.com/helloworld/429368

 

이미지 로딩은 안드로이드 개발에서 가장 뜨거운 지점이다. 네트워크로 읽어온 여러 이미지를 동시에 보여 주는 화면은 안드로이드의 전형적인 UI다. 그런 화면은 SNS의 최신 글 목록처럼 앱의 핵심 UI인 경우가 많고 이미지 로딩을 어떻게 구현하냐에 따라 사용자 경험의 질이 좌우된다. 그러나 로딩하는 화면을 안정적으로 빠르게 동작하도록 만들기는 어렵다. 캐시, 병렬 처리, 실패 처리 등 개발할 요소가 많다...(중략)...네트워크를 통한 이미지 로딩을 구현할 때는 여러 과제를 해결해야 한다.

- 불안한 HTTP 클라이언트 실행 환경 : 원본 이미지는 대부분 HTTP 클라이언트 라이브러리를 써서 읽어 온다. 안드로이드의 HTTP 통신은 라이브러리, 네트워크 환경에서 불안정한 요소가 많기 때문에 이를 충분히 대비해야 한다. 재시도 처리, 실패 처리도 필요하다. 재시도 횟수와 시도 간격을 깊이 고민해야 한다. 불필요해진 호출은 빠른 시점에 취소해야 한다. 화면 회전이나 이동으로 이미 요청된 호출이 의미가 없어졌는데도 끝까지 수행한다면 메모리, 성능, 배터리가 낭비된다

- 메모리가 넘치거나 새기 쉬운 비트맵 디코딩 : 비트맵의 크기가 클 때는 OOM 에러가 발생하지 않도록 유의해야 한다. 이미지 파일은 디코딩을 거쳐야 화면에 출력된다. 비트맵이 차지하는 메모리 용량은 이미지 크기에 비례한다. 따라서 작은 크기로 변환하거나 품질을 낮춰서 디코딩해야 한다. 안드로이드는 BitmapFactory.Options 클래스로 그런 기능을 제공한다

- AsyncTask만으로는 불충분한 병렬 처리 : 네트워크 호출, 디코딩 처리 등 대기 시간이 긴 작업은 백그라운드 쓰레드에서 수행돼야 한다. 그래서 이미지 로딩에서 비동기 처리, 병렬 처리는 필수다. 이런 작업에는 AsyncTask가 많이 쓰이지만 이 클래스는 버전에 따라 다르게 동작하고 요청 취소는 작업 특성에 맞게 구현해야 하는 등 고려할 요소가 많다. 여러 AsyncTask가 실행될 때 버전에 따라 병렬 또는 직렬로 실행된다...(중략)...여러 이미지를 로딩하는 작업이 직렬로 실행되면 비효율적이고 느리다. 이미지 10개를 한 화면에서 보여줘야 하는데 이미지 1개를 불러올 때 1초가 걸린다면 총 10초 기다려야 한다...(중략)

- 이미지 캐시, View 재활용의 어려움 : 이미지가 들어간 화면을 만들 때 이미지 캐시와 View를 재활용하지 않는다면 앱은 느리게 반응하고 자원을 많이 소모한다. 따라서 이미지 캐시와 View 재활용이 필수인데 이를 구현하려면 많은 코드가 들어가고 매번 구현하기도 번거롭다. 화면이 회전될 때는 액티비티 객체가 재생성되고 onCreate() 등이 다시 호출된다. 회전 후에는 회전하기 전의 이미지를 다시 화면에 그려줘야 한다. 리사이클러뷰에서 스크롤 중 이미지가 사라졌다가 다시 나타날 때도 마찬가지다. 이럴 때 처음과 똑같이 네트워크 호출, 디코딩 과정을 반복한다면 큰 낭비다. 캐싱된 이미지를 메모리나 디스크에서 읽어야 한다...(중략)

요약하면 이미지 로딩을 구현할 때는 HTTP 통신을 안정되게 구현하고 비트맵으로 디코딩하면서 메모리가 넘치거나 새지 않도록 주의해야 한다. 네트워크 호출과 디코딩은 단순히 백그라운드 쓰레드에서 동작하는 것만으로는 충분하지 않고 더 적극적으로 병렬성을 활용해야 한다. 화면 회전, 전환, 스크롤 시 반복적인 요청이 가지 않도록 이미지를 캐시하고 불필요해진 요청은 취소해서 더 나은 UI 반응을 제공하면서 자원을 절약해야 한다. 이 과제들을 모두 해결하려다 보면 처리 흐름은 복잡해지고 비슷한 코드가 반복되기 쉽다

 

그리고 아래는 안드로이드 디벨로퍼에서 말하는 비트맵 캐싱이다.

 

https://developer.android.com/topic/performance/graphics/cache-bitmap

 

비트맵 캐싱  |  Android 개발자  |  Android Developers

단일 비트맵을 사용자 인터페이스(UI)에 로드하는 것은 간단하지만 한 번에 더 큰 이미지의 집합을 로드해야 하면 더 복잡해집니다. 많은 경우(ListView, GridView 또는 LruCache 클래스와 같은 구성요소

developer.android.com

단일 비트맵을 UI에 로드하는 건 간단하지만 한 번에 더 큰 이미지 집합을 로드해야 하면 더 복잡해진다. 화면에 곧 스크롤될 수 있는 이미지 수가 합산된 화면 내 총 이미지 수는 기본적으로 제한되지 않는다. 이미지가 화면에서 사라질 때 하위 뷰를 재활용해서 이런 구성요소를 사용해 메모리 사용량을 줄인다. 또한 가비지 컬렉터는 개발자가 오랫동안 활성화된 참조는 유지하지 않는 것으로 간주하며 로드된 비트맵을 해제한다. UI를 끊김 없이 빠르게 로드하도록 유지하려면 이미지가 스크린으로 다시 표시될 때마다 매번 계속 처리하는 걸 피해야 한다. 메모리와 디스크 캐시를 사용하면 구성요소가 처리된 이미지를 빨리 다시 로드해서 이 부분에 도움을 줄 수 있다

메모리 캐시는 중요한 애플리케이션 메모리를 쓰는 대신 비트맵에 빠르게 액세스할 수 있다. LruCache 클래스는 비트맵을 캐싱하는 작업과 최근 참조된 객체를 강한 참조 LinkedHashMap에 유지하는 작업, 캐시가 지정된 크기를 초과하기 전에 가장 오래전에 사용된 항목을 제거하는 작업에 적합하다

LruCache에 적합한 크기를 선택하려면 몇 가지 요인을 고려해야 한다

- 액티비티, 애플리케이션의 나머지 부분이 얼마나 많은 메모리를 쓰는가?
- 한 번에 몇 개의 이미지가 화면에 표시되는가? 화면에 표시할 준비가 돼야 할 이미지 수는 몇 개인가?
- 기기의 화면 크기, 밀도는 어떻게 되는가? Nexus와 같은 초고밀도(xhdpi) 화면 기기는 메모리에 동일한 수의 이미지를 저장하려면 Nexus S(hdpi)와 같은 기기와 비교해 더 큰 캐시가 필요하다
- 비트맵의 크기, 구성은 어떻게 되며 각각의 메모리 사용량은 어떻게 되는가?
- 이미지는 얼마나 자주 액세스되는가? 다른 이미지보다 더 자주 액세스되는 이미지가 있는가? 그런 경우 특정 항목을 항상 메모리에 두거나 서로 다른 그룹의 비트맵에 여러 LruCache 객체를 두고 싶을 수 있다
- 품질, 수량의 균형을 맞출 수 있는가? 때로 더 낮은 품질의 비트맵을 다수 저장해서 잠재적으로 다른 백그라운드 작업에 더 높은 품질 버전을 로드할 수 있게 하는 게 더 유용할 수 있다

모든 앱에 적합한 특정 크기, 수식은 없고 사용량을 분석해 적합한 해결책을 찾아야 한다. 캐시가 너무 작으면 아무 이점 없이 추가 오버헤드가 발생하고 너무 크면 OOM이 다시 발생해 앱의 나머지 부분에 사용할 메모리가 거의 남아 있지 않을 수 있다

 

위의 안드로이드 디벨로퍼 링크에서는 캐시의 종류를 2가지로 나눠 놓았다.

 

  • 메모리 캐시
  • 디스크 캐시

 

두 캐시에 대해선 위 링크에서 모두 간단하게 설명하고 있다. 디스크 캐시에 대해선 아래와 같이 설명하고 있다.

 

메모리 캐시를 통해 최근에 본 비트맵에 더 빨리 액세스할 수 있지만 이 캐시에서 제공되는 이미지는 사용할 수 없다. 데이터 세트 크기가 큰 GridView 같은 구성요소는 메모리 캐시를 쉽게 채울 수 있다. 앱은 통화와 같은 다른 작업에 의해 중단될 수 있으며 백그라운드에서 애플리케이션이 종료되고 메모리 캐시가 삭제될 수 있다. 사용자가 계속하면 앱에서 각 이미지를 다시 처리해야 한다

이 경우 디스크 캐시를 사용해서 처리된 비트맵을 유지하고 메모리 캐시에서 이미지가 더 이상 사용 가능하지 않을 때 로드 시간을 줄일 수 있다. 물론 디스크에서 이미지를 가져오는 건 메모리에서 로드하는 것보다 느리며 디스크 읽기 시간을 예측할 수 없기 때문에 백그라운드 쓰레드에서 실행돼야 한다

참고) 이미지가 갤러리 앱 같은 곳에서 자주 액세스된다면 캐시된 이미지를 저장하기에 ContentProvider가 더 적합한 장소가 될 수 있다

 

이것을 정리하면 아래의 스택오버플로우 답변처럼 될 수 있다.

 

https://stackoverflow.com/questions/35697847/android-disk-cache-vs-memory-cache

 

android disk cache vs memory cache

I didn't fully understand when should I use memory cache (LruCache) and when to pick disk caching. or should I use them both together? I looked here

stackoverflow.com

메모리 캐시
- 이 캐시에 더 빠르게 접근
- 애플리케이션 메모리를 차지하기 때문에 대용량 데이터를 저장하지 마라
- 앱이 백그라운드로 전환되면 메모리 캐시가 파괴되고 리소스 절약을 위해 시스템이 종료시킴

디스크 캐시
- 메모리 캐시보다 느림
- 대용량 캐시 데이터에 사용하라
- 앱이 백그라운드로 전환된 후에도 데이터가 존재함

 

그럼 메모리 캐시를 구현하려면 LruCache라는 클래스를 써서 구현할 수 있고, 디스크 캐시를 구현하려면 백그라운드 쓰레드에서 어떤 로직을 실행하면 된다는 걸로 대충 이해했다. LruCache가 무엇인지와 사용예제에 대한 내용은 다음 포스팅에 작성한다.

반응형
Comments