Android 앱 이미지 처리 최적화 — Glide 캐시, RecyclerView 이미지 로딩, WebP·AVIF와 OOM 방지

Android 앱에서 이미지는 사용자 경험에 큰 영향을 미칩니다.

특히 생물도감, 여행 정보, 쇼핑, 사진 갤러리처럼 이미지를 많이 사용하는 앱에서는 이미지 처리 방식이 앱의 성능을 좌우할 수 있습니다.

처음에는 다음과 같이 이미지를 불러오는 것만으로 충분해 보입니다.

Glide.with(imageView)
    .load(imageUrl)
    .into(imageView)

하지만 이미지 수가 증가하면 여러 문제가 발생할 수 있습니다.

  • RecyclerView를 빠르게 스크롤할 때 이미지가 늦게 표시됨

  • 이전 항목의 이미지가 잠시 나타나는 현상

  • 상세 화면에서 고해상도 이미지를 열면 메모리 사용량 증가

  • ViewPager2에서 여러 이미지를 넘길 때 화면 끊김

  • 동일한 이미지를 반복 다운로드

  • 앱에 포함된 이미지 때문에 AAB 용량 증가

  • 오래된 이미지가 캐시에 남아 갱신되지 않음

  • 저사양 기기에서 OutOfMemoryError 발생

이러한 문제를 해결하려면 이미지 크기, 메모리, 디스크 캐시, 네트워크 요청, RecyclerView 재사용 구조를 함께 고려해야 합니다.

이번 글에서는 Kotlin + XML 기반 Android 앱에서 Glide를 중심으로 이미지 처리 성능을 개선하는 방법을 살펴보겠습니다.

작성 기준: 2026년 10월. 예제는 AndroidX, Glide 4 계열 API를 중심으로 작성했습니다. 일부 코드는 프로젝트의 리소스와 데이터 모델에 맞게 조정해야 하며, 실제 성능 개선 효과는 기기와 이미지 데이터에 따라 달라집니다.


1. Android 이미지 처리에서 발생하는 대표적인 문제

이미지 처리 문제는 크게 네 가지로 나눌 수 있습니다.

문제대표적인 원인
느린 로딩큰 이미지 다운로드, 캐시 미활용
스크롤 끊김이미지 디코딩 부담, 과도한 UI 작업
메모리 부족큰 Bitmap, 이미지 중복 보관
이미지 불일치RecyclerView 재사용, 잘못된 캐시 정책

이 문제들은 서로 연결되어 있습니다.

예를 들어 서버에서 6000×4000 크기의 이미지를 다운로드해 120×120 크기의 썸네일에 표시한다면 불필요한 네트워크 사용과 이미지 디코딩 비용이 발생할 수 있습니다.

따라서 이미지 최적화는 화면에 필요한 크기의 이미지를 적절한 방식으로 불러오는 것에서 시작합니다.

2. Bitmap 메모리 사용량 이해하기

Android에서 이미지 메모리 사용량은 파일 크기와 다릅니다.

JPEG 파일이 2MB라고 해서 메모리에서도 반드시 2MB를 사용하는 것은 아닙니다.

예를 들어 ARGB_8888 Bitmap은 픽셀당 일반적으로 4바이트를 사용합니다.

계산식은 다음과 같습니다.

Bitmap 메모리 사용량
= 가로 픽셀 × 세로 픽셀 × 픽셀당 바이트
이미지 해상도ARGB_8888 픽셀 데이터 크기
400×300약 0.46MiB
1280×720약 3.52MiB
1920×1080약 7.91MiB
4000×3000약 45.78MiB
6000×4000약 91.55MiB

이는 픽셀 데이터만 계산한 값이며 실제 메모리 사용량에는 객체 및 기타 오버헤드가 추가될 수 있습니다.

6000×4000 이미지를 여러 장 동시에 디코딩하면 메모리 사용량이 급격히 증가할 수 있습니다.


3. 이미지 로딩 라이브러리를 사용하는 이유

Android에서 이미지를 직접 처리하려면 다음 작업을 구현해야 합니다.

  1. 네트워크 요청

  2. 이미지 파일 다운로드

  3. 이미지 디코딩

  4. 화면 크기에 맞는 리사이징

  5. 메모리 캐시 관리

  6. 디스크 캐시 관리

  7. 요청 취소

  8. 화면 생명주기 관리

  9. 오류 및 재시도 처리

Glide는 이러한 작업을 효율적으로 관리하도록 설계된 이미지 로딩 라이브러리입니다.

물론 Glide를 사용한다고 해서 모든 성능 문제가 자동으로 해결되는 것은 아닙니다.

요청 크기와 캐시 정책, 이미지 원본의 크기, RecyclerView 구성 방식은 개발자가 적절히 설계해야 합니다.

4. Glide 의존성 추가하기

앱 모듈의 build.gradle.kts에 Glide를 추가합니다.

dependencies {
    implementation("com.github.bumptech.glide:glide:4.16.0")
}

위 버전은 Glide 4 계열의 예시입니다. 프로젝트에 적용할 때는 공식 저장소에서 유지보수 상태와 호환성을 확인하는 것이 좋습니다.

Glide의 기본적인 이미지 로딩에는 별도의 어노테이션 프로세서가 필수는 아닙니다.

사용자 정의 AppGlideModule 및 생성 API가 필요한 경우에만 관련 설정을 추가하면 됩니다.


5. 가장 기본적인 Glide 이미지 로딩

이미지 URL을 ImageView에 표시하는 기본 코드입니다.

Glide.with(imageView)
    .load(imageUrl)
    .placeholder(R.drawable.ic_image_placeholder)
    .error(R.drawable.ic_image_error)
    .into(imageView)

각 메서드의 역할은 다음과 같습니다.

메서드역할
load()이미지 데이터 지정
placeholder()로딩 중 표시할 이미지
error()로딩 실패 시 표시할 이미지
into()결과를 표시할 ImageView

Glide.with(imageView)처럼 View를 전달하면 Glide가 적절한 생명주기와 요청 관리를 수행하는 데 도움이 됩니다.

다만 ImageView가 재사용되거나 화면에서 분리되는 상황에서는 요청 생명주기를 함께 고려해야 합니다.

6. 이미지가 없을 때 기본 이미지 표시하기

도감 앱에서는 모든 항목에 이미지가 존재하지 않을 수 있습니다.

이때는 이미지가 없는 경우를 별도로 처리하는 것이 좋습니다.

fun loadBirdImage(
    imageView: ImageView,
    imageUrl: String?
) {
    if (imageUrl.isNullOrBlank()) {
        Glide.with(imageView).clear(imageView)
        imageView.setImageResource(
            R.drawable.ic_image_placeholder
        )
        return
    }

    Glide.with(imageView)
        .load(imageUrl)
        .placeholder(R.drawable.ic_image_placeholder)
        .error(R.drawable.ic_image_error)
        .into(imageView)
}

중요한 부분은 기존 요청을 정리하는 것입니다.

RecyclerView에서는 같은 ImageView가 다른 데이터 항목에 재사용되므로, 이미지가 없는 항목에서도 이전 요청이 남지 않도록 해야 합니다.


7. RecyclerView 이미지 로딩 최적화

RecyclerView는 화면에 보이는 항목의 ViewHolder를 재사용합니다.

예를 들어 다음과 같은 상황입니다.

ViewHolder A → 참새 이미지 표시
       ↓
스크롤
       ↓
ViewHolder A → 까치 이미지 표시

이 과정에서 이전 이미지 요청이 제대로 관리되지 않으면 잘못된 이미지가 표시되는 것처럼 보일 수 있습니다.

Glide는 동일한 ImageView에 새로운 요청을 연결할 때 이전 요청을 관리하지만, 개발자가 직접 Bitmap을 보관하거나 별도의 비동기 로딩 로직을 섞어 사용하는 경우에는 주의해야 합니다.

8. ViewHolder에서 Glide 사용하기

다음은 RecyclerView ViewHolder 예제입니다.

class BirdViewHolder(
    private val binding: ItemBirdBinding
) : RecyclerView.ViewHolder(binding.root) {

    fun bind(bird: Bird) {

        binding.textName.text = bird.nameKo

        Glide.with(binding.imageBird)
            .load(bird.imageUrl)
            .placeholder(
                R.drawable.ic_image_placeholder
            )
            .error(
                R.drawable.ic_image_error
            )
            .centerCrop()
            .into(binding.imageBird)
    }
}

이 예제는 ViewBinding을 사용합니다.

RecyclerView의 ViewHolder에서는 Glide.with(binding.imageBird)처럼 실제 ImageView를 기준으로 요청을 생성할 수 있습니다.

9. onViewRecycled에서 이미지 요청 정리하기

필요한 경우 ViewHolder가 재활용될 때 이미지 요청을 정리할 수 있습니다.

override fun onViewRecycled(
    holder: BirdViewHolder
) {
    super.onViewRecycled(holder)

    holder.clearImage()
}

ViewHolder에는 다음과 같은 함수를 추가합니다.

fun clearImage() {
    Glide.with(binding.imageBird)
        .clear(binding.imageBird)
}

Glide는 기본적으로 View 재사용 상황을 고려해 요청을 관리하므로 모든 프로젝트에서 이 처리가 반드시 필요한 것은 아닙니다.

하지만 사용자 정의 이미지 로딩 로직이나 복잡한 ViewHolder 구조를 사용하는 경우에는 명시적인 정리가 도움이 될 수 있습니다.


10. 썸네일 크기를 제한해야 하는 이유

RecyclerView에서 100×100 정도의 이미지가 필요한데 서버의 4000×3000 원본을 그대로 사용하면 비효율적입니다.

Glide는 ImageView의 크기에 맞춰 디코딩 크기를 조정할 수 있습니다.

명확한 크기를 지정하고 싶다면 override()를 사용할 수 있습니다.

Glide.with(imageView)
    .load(imageUrl)
    .override(240, 240)
    .centerCrop()
    .into(imageView)

여기서 240은 픽셀 단위입니다.

실제 UI에서 필요한 크기는 기기의 화면 밀도와 ImageView 크기를 고려해야 합니다.

따라서 무조건 240×240을 지정하기보다 화면에 표시되는 크기에 맞춰 설정하는 것이 좋습니다.

또한 override()는 디코딩 크기 조정에 도움을 주지만 서버에서 다운로드하는 원본 파일 크기를 반드시 줄여 주는 것은 아닙니다.

네트워크 사용량까지 줄이려면 서버에서 별도의 썸네일 이미지를 제공하는 것이 효과적입니다.

11. 서버에서 썸네일과 원본 이미지 분리하기

이미지가 많은 앱이라면 서버에서 다음과 같이 이미지 URL을 구분할 수 있습니다.

{
  "id": 1001,
  "name": "참새",
  "thumbnail_url": "https://example.com/images/1001_thumb.webp",
  "original_url": "https://example.com/images/1001_original.jpg"
}

RecyclerView에서는 썸네일을 사용합니다.

Glide.with(imageView)
    .load(bird.thumbnailUrl)
    .centerCrop()
    .into(imageView)

상세 화면에서는 원본 이미지 또는 화면에 적합한 중간 해상도 이미지를 사용합니다.

Glide.with(imageView)
    .load(bird.originalUrl)
    .fitCenter()
    .into(imageView)

이렇게 하면 목록 화면에서 불필요하게 큰 이미지를 다운로드하는 문제를 줄일 수 있습니다.


12. Glide 메모리 캐시 이해하기

Glide는 이미지를 효율적으로 재사용하기 위해 여러 캐시 계층을 활용합니다.

대표적으로 다음과 같은 캐시가 있습니다.

  • 활성 리소스

  • 메모리 캐시

  • 디스크 리소스 캐시

  • 디스크 데이터 캐시

이미지가 이미 캐시에 존재하면 매번 네트워크 요청과 디코딩을 반복하지 않아도 됩니다.

다만 실제 캐시 적중 여부는 요청 크기, 변환 옵션, 이미지 식별자, 캐시 정책 등에 따라 달라집니다.

13. Glide DiskCacheStrategy 비교

Glide에서는 DiskCacheStrategy로 디스크 캐시 정책을 지정할 수 있습니다.

정책설명
AUTOMATICGlide가 데이터 유형에 따라 자동 결정
ALL원본 데이터와 변환 리소스를 가능한 범위에서 캐시
DATA원본 데이터 중심 캐시
RESOURCE변환된 리소스 중심 캐시
NONE디스크 캐시 사용 안 함

일반적인 이미지 로딩에서는 기본값인 AUTOMATIC부터 사용하는 것이 좋습니다.

특별한 이유 없이 모든 이미지에 ALL을 적용하면 디스크 사용량이 증가할 수 있습니다.

14. 디스크 캐시 정책 적용하기

예를 들어 다음과 같이 설정할 수 있습니다.

Glide.with(imageView)
    .load(imageUrl)
    .diskCacheStrategy(
        DiskCacheStrategy.AUTOMATIC
    )
    .into(imageView)

이미지를 오프라인에서 재사용하려는 경우에는 디스크 캐시가 도움이 될 수 있습니다.

하지만 Glide 캐시는 영구 저장소가 아닙니다.

캐시가 제거될 수 있으므로 반드시 오프라인에서 제공해야 하는 이미지는 별도의 앱 관리 저장소에 보관해야 합니다.


15. skipMemoryCache를 남용하지 않기

Glide에는 메모리 캐시를 건너뛰는 옵션이 있습니다.

Glide.with(imageView)
    .load(imageUrl)
    .skipMemoryCache(true)
    .into(imageView)

이 옵션을 사용하면 메모리 캐시 재사용 기회가 줄어들 수 있습니다.

따라서 일반적인 RecyclerView 이미지에 일괄 적용하는 것은 권장하지 않습니다.

메모리 부족 문제가 발생했다고 해서 무조건 메모리 캐시를 비활성화하면 오히려 이미지 디코딩이 반복되어 성능이 나빠질 수 있습니다.

먼저 이미지 크기, 요청 수, Bitmap 보관 방식 등을 확인해야 합니다.

16. 이미지 캐시가 갱신되지 않는 문제

서버에서 이미지 파일을 변경했는데 앱에는 이전 이미지가 계속 표시되는 경우가 있습니다.

예를 들어 URL이 동일한 상태에서 이미지 내용만 변경되면 캐시된 이미지가 재사용될 수 있습니다.

이 문제를 해결하려면 이미지 버전 정보를 관리하는 방법을 고려할 수 있습니다.

Glide.with(imageView)
    .load(imageUrl)
    .signature(
        ObjectKey(imageVersion)
    )
    .into(imageView)

imageVersion은 서버에서 제공하는 이미지 버전이나 수정 시각 등을 사용할 수 있습니다.

예를 들어 다음과 같습니다.

{
  "image_url": "https://example.com/images/bird_1001.webp",
  "image_version": "20261030_01"
}

이미지 내용이 변경될 때 버전도 변경하면 Glide가 이전 캐시와 구분할 수 있습니다.


17. ViewPager2 이미지 슬라이드 최적화

상세 화면에서 여러 이미지를 표시할 때 ViewPager2를 사용할 수 있습니다.

예를 들어 다음과 같은 구조입니다.

BirdDetailFragment
        │
        ▼
     ViewPager2
        │
        ▼
  ImagePagerAdapter
        │
        ▼
      Glide

이미지가 여러 장 있다고 해서 모든 이미지를 한 번에 Bitmap으로 디코딩할 필요는 없습니다.

ViewPager2는 필요한 페이지의 ViewHolder를 생성하고 재사용하도록 구성할 수 있습니다.

18. ViewPager2 이미지 어댑터 구현하기

다음은 RecyclerView.Adapter를 사용하는 간단한 예제입니다.

class ImagePagerAdapter(
    private val images: List<String>
) : RecyclerView.Adapter<ImagePagerAdapter.ImageViewHolder>() {

    class ImageViewHolder(
        val imageView: ImageView
    ) : RecyclerView.ViewHolder(imageView)

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): ImageViewHolder {

        val imageView = ImageView(parent.context).apply {
            layoutParams = RecyclerView.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
            )

            scaleType = ImageView.ScaleType.FIT_CENTER
        }

        return ImageViewHolder(imageView)
    }

    override fun onBindViewHolder(
        holder: ImageViewHolder,
        position: Int
    ) {

        Glide.with(holder.imageView)
            .load(images[position])
            .placeholder(
                R.drawable.ic_image_placeholder
            )
            .fitCenter()
            .into(holder.imageView)
    }

    override fun getItemCount(): Int {
        return images.size
    }
}

이 어댑터를 ViewPager2에 연결하면 이미지 슬라이드 화면을 구성할 수 있습니다.

binding.viewPager.adapter =
    ImagePagerAdapter(imageUrls)

실제 서비스에서는 이미지 확대, 로딩 상태, 오류 처리, 페이지 표시 등의 기능을 추가할 수 있습니다.


19. ViewPager2 offscreenPageLimit 주의하기

ViewPager2에는 offscreenPageLimit 옵션이 있습니다.

viewPager.offscreenPageLimit = 1

이 값은 현재 페이지 주변에 유지할 페이지 수에 영향을 줍니다.

값을 지나치게 크게 설정하면 여러 페이지의 View와 이미지 리소스가 동시에 유지되어 메모리 사용량이 증가할 수 있습니다.

따라서 이미지 갤러리에서는 기본 동작을 우선 사용하고, 실제 페이지 전환 성능 문제가 확인될 때 조정하는 것이 좋습니다.

offscreenPageLimit를 늘린다고 해서 모든 이미지 로딩 문제가 해결되는 것은 아닙니다.


20. Glide preload 활용하기

사용자가 다음 이미지를 열기 전에 미리 로딩하면 페이지 전환 시 대기 시간을 줄일 수 있습니다.

Glide에서는 preload()를 사용할 수 있습니다.

Glide.with(context)
    .load(nextImageUrl)
    .preload()

다만 미리 로딩하는 이미지가 많으면 네트워크와 캐시 사용량이 증가할 수 있습니다.

따라서 다음 한두 장 정도만 미리 로딩하는 전략을 고려할 수 있습니다.

또한 preload()는 ImageView에 표시하는 요청과 크기나 변환 옵션이 다르면 기대한 캐시 재사용 효과가 줄어들 수 있습니다.

21. RecyclerView에서 이미지 프리로딩하기

RecyclerView에서도 스크롤 방향을 기준으로 다음 항목의 이미지를 미리 로딩할 수 있습니다.

하지만 이미지가 수천 장인 도감 앱에서 모든 이미지를 한 번에 미리 다운로드하는 방식은 피해야 합니다.

예를 들어 다음과 같은 정책을 사용할 수 있습니다.

  • 현재 화면에 보이는 이미지 우선 로딩

  • 다음에 표시될 일부 썸네일만 프리로딩

  • 고해상도 원본은 상세 화면에서 요청

  • 모바일 데이터 사용량 고려

  • 네트워크 상태에 따라 프리로딩 조정

실제 구현에서는 Glide의 RecyclerView 프리로딩 기능도 검토할 수 있습니다.


22. WebP 이미지 형식의 장점

WebP는 웹과 앱에서 널리 사용하는 이미지 형식입니다.

JPEG나 PNG와 비교해 파일 크기를 줄일 수 있는 경우가 있습니다.

특히 사진형 이미지에서는 손실 압축 WebP를 사용하면 화질과 파일 크기 사이의 균형을 조정할 수 있습니다.

다만 모든 이미지에서 WebP가 JPEG나 PNG보다 반드시 작아지는 것은 아닙니다.

이미지의 특성과 압축 품질에 따라 결과가 달라집니다.

23. JPEG, PNG, WebP, AVIF 비교

형식특징추천 용도
JPEG사진에 널리 사용원본 사진, 일반 이미지
PNG무손실 압축, 투명도 지원아이콘, 그래픽
WebP손실·무손실 압축 지원썸네일, 앱 이미지
AVIF높은 압축 효율을 기대할 수 있음최신 환경의 사진 이미지

AVIF는 높은 압축 효율을 제공할 수 있지만 Android 버전과 이미지 디코더 지원 여부를 확인해야 합니다.

특히 Android 12(API 31)부터 AVIF 이미지 디코딩 지원이 도입되었으므로, 오래된 Android 버전까지 지원하는 앱에서는 WebP나 JPEG 대체 이미지를 준비하는 것이 안전합니다.

24. 이미지 형식 선택 기준

이미지 형식을 선택할 때는 다음 요소를 고려해야 합니다.

  1. 대상 Android 버전

  2. 이미지의 종류

  3. 투명도 필요 여부

  4. 파일 크기

  5. 화질

  6. 디코딩 성능

  7. 서버 이미지 변환 비용

예를 들어 조류 사진 썸네일은 WebP를 우선 검토할 수 있습니다.

반면 고해상도 원본 사진은 JPEG 또는 지원 환경에 맞는 다른 형식을 유지할 수 있습니다.


25. 앱 assets 이미지 최적화

Android 앱에 이미지 파일을 직접 포함하는 경우에는 앱 크기도 고려해야 합니다.

예를 들어 다음과 같은 구조가 있다고 가정하겠습니다.

app/src/main/assets/
    images/
        bird_1001.jpg
        bird_1002.jpg
        bird_1003.jpg

수천 장의 이미지를 assets에 포함하면 AAB 크기가 커질 수 있습니다.

이때는 다음 방법을 고려할 수 있습니다.

  • 원본 대신 적절한 크기의 썸네일 포함

  • 이미지 압축 품질 조정

  • WebP 변환

  • 필요한 이미지와 선택적 다운로드 이미지 구분

  • 중복 이미지 제거

다만 assets에 포함된 파일을 무조건 WebP로 변환하면 되는 것은 아닙니다.

이미지 품질과 Android 버전별 디코딩 호환성을 함께 확인해야 합니다.

26. Product Flavor별 이미지 분리

Product Flavor를 사용하는 프로젝트에서는 이미지 리소스를 Flavor별로 분리할 수 있습니다.

예를 들어 다음과 같은 구조입니다.

app/src/
    main/
        res/
            drawable/

    bird/
        assets/
            images/

    fish/
        assets/
            images/

이렇게 하면 조류 앱에는 조류 이미지만, 어류 앱에는 어류 이미지만 포함하도록 구성할 수 있습니다.

단, 실제 AAB에 어떤 파일이 포함되는지는 빌드 결과를 확인해야 합니다.

Flavor별 assets 경로와 데이터베이스 파일 이름도 일관되게 관리하는 것이 좋습니다.


27. 이미지 로딩 중 OOM이 발생하는 이유

OutOfMemoryError는 앱이 사용할 수 있는 메모리를 초과했을 때 발생할 수 있습니다.

이미지 관련 OOM의 대표적인 원인은 다음과 같습니다.

  • 원본 해상도의 Bitmap을 여러 장 생성

  • Bitmap을 전역 변수에 장기간 보관

  • Activity나 Fragment가 해제되지 않음

  • RecyclerView에서 이미지 요청이 과도하게 발생

  • 대형 이미지를 동시에 디코딩

  • 이미지 캐시를 잘못 구성

  • 이미지 처리 과정에서 불필요한 복사본 생성

OOM은 단순히 메모리 캐시 크기만 줄인다고 해결되는 문제가 아닙니다.

먼저 어떤 객체가 메모리를 많이 사용하고 있는지 확인해야 합니다.

28. Bitmap을 직접 디코딩할 때 주의하기

직접 Bitmap을 디코딩하는 경우에는 이미지 크기를 확인하고 필요한 크기로 축소하는 것이 좋습니다.

예를 들어 BitmapFactory.Options를 사용할 수 있습니다.

val options = BitmapFactory.Options().apply {
    inJustDecodeBounds = true
}

BitmapFactory.decodeFile(
    imagePath,
    options
)

val width = options.outWidth
val height = options.outHeight

이렇게 하면 실제 픽셀 데이터를 모두 디코딩하지 않고 이미지 크기 정보를 확인할 수 있습니다.

이후 필요한 크기에 맞춰 inSampleSize를 설정해 디코딩할 수 있습니다.

다만 Glide를 사용한다면 대부분의 일반적인 이미지 표시 작업에서 직접 Bitmap 디코딩 로직을 작성할 필요는 없습니다.


29. Activity와 Fragment 메모리 누수 방지

이미지 처리 문제는 Activity나 Fragment의 메모리 누수와 연결될 수 있습니다.

예를 들어 Fragment의 View가 파괴되었는데 ImageView나 Binding을 계속 참조하면 불필요한 객체가 메모리에 남을 수 있습니다.

ViewBinding을 사용하는 Fragment에서는 다음과 같이 정리합니다.

private var _binding: FragmentBirdDetailBinding? = null

private val binding
    get() = _binding!!

override fun onDestroyView() {

    binding.viewPager.adapter = null

    _binding = null

    super.onDestroyView()
}

이 예제는 Fragment의 View 생명주기에 맞춰 Binding과 Adapter 참조를 정리하는 방법을 보여줍니다.

다만 실제 프로젝트에서는 다른 View 참조나 리스너도 함께 정리해야 할 수 있습니다.

Glide 자체가 생명주기를 관리하더라도 개발자가 보관한 Activity, Fragment, View 참조까지 모두 자동으로 해결해 주는 것은 아닙니다.

30. LeakCanary로 메모리 누수 확인하기

메모리 누수가 의심된다면 LeakCanary를 활용할 수 있습니다.

예를 들어 다음과 같은 상황을 테스트합니다.

  1. 이미지 상세 화면 열기

  2. 여러 이미지 넘기기

  3. 이전 화면으로 돌아가기

  4. 같은 화면 반복 진입

  5. Fragment가 정상적으로 해제되는지 확인

LeakCanary에서 Fragment, View, Adapter 등이 계속 유지되는지 확인할 수 있습니다.

단, 메모리 사용량이 높다는 사실만으로 메모리 누수라고 판단해서는 안 됩니다.

이미지 캐시가 정상적으로 메모리를 활용하는 상황일 수도 있기 때문입니다.


31. Android Studio Memory Profiler 활용하기

Android Studio의 Memory Profiler를 사용하면 앱의 메모리 사용량을 분석할 수 있습니다.

특히 다음 상황을 비교하는 것이 좋습니다.

  • 앱 시작 직후

  • RecyclerView 목록 표시

  • 빠른 스크롤 이후

  • 이미지 상세 화면 진입

  • ViewPager2 페이지 이동

  • 상세 화면 종료 이후

이때 Java/Kotlin Heap뿐 아니라 Native 메모리와 Graphics 메모리도 함께 확인해야 합니다.

Bitmap의 실제 픽셀 메모리가 항상 Java Heap에만 표시되는 것은 아니기 때문입니다.

32. 이미지 로딩 시간 측정하기

이미지 로딩 시간을 확인하려면 Glide의 RequestListener를 사용할 수 있습니다.

다음은 로딩 완료 시간을 기록하는 예제입니다.

val startTime =
    SystemClock.elapsedRealtime()

Glide.with(imageView)
    .load(imageUrl)
    .listener(
        object : RequestListener<Drawable> {

            override fun onLoadFailed(
                e: GlideException?,
                model: Any?,
                target: Target<Drawable>,
                isFirstResource: Boolean
            ): Boolean {

                Log.w(
                    "ImageLoading",
                    "이미지 로딩 실패",
                    e
                )

                return false
            }

            override fun onResourceReady(
                resource: Drawable,
                model: Any,
                target: Target<Drawable>?,
                dataSource: DataSource,
                isFirstResource: Boolean
            ): Boolean {

                val elapsed =
                    SystemClock.elapsedRealtime() -
                    startTime

                Log.d(
                    "ImageLoading",
                    "로딩 시간: ${elapsed}ms, " +
                        "source=$dataSource"
                )

                return false
            }
        }
    )
    .into(imageView)

dataSource를 확인하면 메모리 캐시, 디스크 캐시, 원격 데이터 등 이미지가 어떤 경로에서 제공되었는지 분석하는 데 도움이 됩니다.

다만 위 코드는 설명용 예제이며, 사용하는 Glide 버전에 맞춰 리스너의 메서드 시그니처를 확인해야 합니다.

또한 실제 운영 앱에서는 이미지 URL이나 사용자 관련 정보가 로그에 노출되지 않도록 주의해야 합니다.


33. 이미지 캐시를 무조건 삭제하지 않기

앱에서 이미지가 갱신되지 않는 문제가 발생하면 캐시를 전부 삭제하고 싶을 수 있습니다.

Glide에서는 캐시 삭제 기능을 제공합니다.

Glide.get(context).clearMemory()

디스크 캐시는 다음과 같이 삭제할 수 있습니다.

withContext(Dispatchers.IO) {
    Glide.get(context).clearDiskCache()
}

다만 메모리 캐시 삭제는 메인 스레드에서 호출해야 하고, 디스크 캐시 삭제는 백그라운드 스레드에서 실행해야 합니다.

또한 전체 캐시 삭제는 이후 이미지 재다운로드와 재디코딩을 유발할 수 있습니다.

따라서 특정 이미지가 변경된 경우에는 전체 캐시 삭제보다 버전 기반 캐시 무효화를 우선 고려하는 것이 좋습니다.

34. 이미지 URL과 캐시 정책 설계

서버에서 이미지를 관리한다면 URL 구조도 중요합니다.

예를 들어 다음과 같은 방식입니다.

/images/birds/1001/v1.webp
/images/birds/1001/v2.webp

이미지가 변경될 때 URL을 변경하면 캐시를 구분하기 쉬워집니다.

또는 동일한 URL을 유지하면서 이미지 버전 정보를 별도로 제공할 수도 있습니다.

어떤 방식을 선택하든 서버의 HTTP 캐시 헤더와 Glide의 캐시 정책을 함께 고려해야 합니다.


35. 이미지 로딩 실패 처리하기

이미지 로딩은 여러 이유로 실패할 수 있습니다.

  • 네트워크 연결 실패

  • 서버 오류

  • 잘못된 URL

  • 파일 삭제

  • 지원하지 않는 이미지 형식

  • 이미지 파일 손상

  • 저장 공간 부족

  • 메모리 부족

이때 단순히 빈 화면을 표시하는 것보다 오류 이미지를 제공하는 것이 좋습니다.

Glide.with(imageView)
    .load(imageUrl)
    .placeholder(
        R.drawable.ic_image_placeholder
    )
    .error(
        R.drawable.ic_image_error
    )
    .into(imageView)

오류 이미지는 사용자에게 이미지가 표시되지 않았다는 사실을 알려줍니다.

필요한 경우 재시도 버튼을 제공할 수도 있습니다.

다만 RecyclerView의 모든 항목에서 자동으로 반복 재시도하면 네트워크 요청이 과도하게 발생할 수 있으므로 주의해야 합니다.


36. 오프라인 환경에서 이미지 처리하기

오프라인 우선 앱에서는 이미지 처리도 별도로 설계해야 합니다.

SQLite에 이미지 URL만 저장되어 있다면 인터넷이 끊긴 상태에서 이미지를 표시하지 못할 수 있습니다.

따라서 다음과 같은 전략을 고려할 수 있습니다.

이미지 유형권장 전략
기본 아이콘앱 리소스 포함
필수 썸네일앱 내부 파일 또는 assets
일반 목록 이미지Glide 캐시 활용
고해상도 원본필요할 때 다운로드
사용자 저장 이미지앱 관리 저장소에 보관

중요한 점은 Glide 캐시와 영구 저장소를 구분하는 것입니다.

Glide 캐시는 성능 최적화를 위한 것이며, 사용자가 반드시 보관해야 하는 이미지를 저장하는 용도로 설계된 것은 아닙니다.

37. 이미지 다운로드와 저장 공간 관리

사용자가 고해상도 이미지를 오프라인으로 저장할 수 있는 앱이라면 저장 공간 관리 기능이 필요합니다.

예를 들어 다음과 같은 기능을 제공할 수 있습니다.

  • 다운로드한 이미지 용량 표시

  • 오프라인 이미지 삭제

  • Wi-Fi 연결 시에만 원본 다운로드

  • 썸네일과 원본 구분

  • 다운로드 실패 시 재시도

  • 저장 공간 부족 안내

이러한 기능은 Glide 캐시와 별도의 파일 관리 계층에서 구현하는 것이 좋습니다.


38. 이미지 최적화 전후 성능 비교하기

이미지 최적화 효과를 확인하려면 같은 조건에서 비교해야 합니다.

예를 들어 다음과 같은 테스트를 구성할 수 있습니다.

테스트최적화 전최적화 후
목록 이미지원본 이미지 사용썸네일 사용
캐시 정책기본 설정버전 기반 캐시 관리
이미지 크기화면보다 큰 이미지필요한 크기로 제한
ViewPager2과도한 페이지 유지필요한 페이지 중심
이미지 형식JPEG/PNGWebP 검토
오류 처리빈 화면Placeholder/Error 이미지

성능 측정에서는 다음 지표를 확인하는 것이 좋습니다.

  • 목록 첫 표시 시간

  • 이미지 로딩 시간

  • 메모리 사용량

  • 스크롤 프레임 지연

  • 네트워크 다운로드 크기

  • 디스크 캐시 용량

  • OOM 발생 여부

단순히 파일 크기가 줄었다는 이유만으로 전체 앱 성능이 개선되었다고 판단해서는 안 됩니다.

이미지 디코딩 비용이나 캐시 효율도 함께 확인해야 합니다.


39. 개인 개발자에게 추천하는 이미지 최적화 순서

이미지가 많은 Android 앱이라면 다음 순서로 개선하는 것이 좋습니다.

1단계: Glide 요청 정리

모든 이미지 로딩을 Glide 중심으로 통일하고 Placeholder와 Error 이미지를 설정합니다.

2단계: 썸네일 분리

RecyclerView에서는 작은 이미지를 사용하고 상세 화면에서 고해상도 이미지를 로딩합니다.

3단계: 캐시 정책 확인

기본 캐시 동작을 확인하고 이미지 버전 변경 시 캐시가 갱신되도록 구성합니다.

4단계: RecyclerView 최적화

ViewHolder 재사용과 이미지 요청 생명주기를 확인합니다.

5단계: ViewPager2 최적화

불필요하게 많은 페이지와 이미지를 동시에 유지하지 않도록 구성합니다.

6단계: 이미지 형식 개선

WebP와 AVIF의 파일 크기, 화질, Android 버전 호환성을 비교합니다.

7단계: 메모리 분석

Memory Profiler와 LeakCanary로 OOM과 메모리 누수를 분석합니다.

8단계: 실제 기기 성능 측정

저사양 기기와 오래된 Android 버전에서도 테스트합니다.


40. Android 이미지 처리 최적화 체크리스트

  • Glide 의존성 및 버전 확인

  • Placeholder 이미지 설정

  • Error 이미지 설정

  • 이미지가 없는 항목 처리

  • RecyclerView 이미지 요청 관리

  • ViewHolder 재사용 시 이미지 불일치 확인

  • 목록 썸네일과 상세 원본 분리

  • 필요한 이미지 크기로 디코딩

  • Glide 메모리 캐시 동작 확인

  • Glide 디스크 캐시 정책 확인

  • 불필요한 skipMemoryCache(true) 제거

  • 이미지 변경 시 캐시 버전 관리

  • ViewPager2 페이지 유지 개수 확인

  • 필요한 이미지에만 preload 적용

  • WebP 변환 효과 비교

  • AVIF Android 버전 호환성 확인

  • assets 이미지 용량 최적화

  • Product Flavor별 이미지 분리

  • Bitmap 직접 보관 여부 확인

  • Fragment ViewBinding 참조 정리

  • LeakCanary 메모리 누수 확인

  • Memory Profiler 분석

  • 이미지 로딩 시간 측정

  • 오프라인 이미지 저장 정책 수립

  • 저사양 기기에서 스크롤 테스트

  • 고해상도 이미지 연속 로딩 테스트

  • OOM 발생 여부 확인

41. 마무리

Android 앱에서 이미지 최적화는 단순히 이미지 파일을 압축하는 작업이 아닙니다.

이미지를 언제 다운로드하고, 어떤 크기로 디코딩하며, 어디에 캐시하고, 언제 메모리에서 해제할지를 함께 설계해야 합니다.

특히 Kotlin + XML + RecyclerView + ViewPager2 + Glide를 사용하는 앱에서는 다음 원칙이 중요합니다.

  1. 목록에서는 원본 대신 썸네일을 사용합니다.

  2. 화면에 필요한 크기로 이미지를 디코딩합니다.

  3. Glide의 기본 캐시 기능을 우선 활용합니다.

  4. 이미지 변경 시 버전 기반 캐시 무효화를 고려합니다.

  5. RecyclerView의 View 재사용 구조를 이해합니다.

  6. ViewPager2에서 불필요한 이미지 로딩을 줄입니다.

  7. WebP·AVIF는 파일 크기뿐 아니라 호환성도 확인합니다.

  8. OOM 문제는 Memory Profiler로 원인을 분석합니다.

  9. Glide 캐시와 영구 이미지 저장소를 구분합니다.

  10. 최적화 결과는 실제 기기에서 측정합니다.

도감이나 사진 중심 앱에서는 썸네일과 원본 이미지를 분리하는 것만으로도 네트워크 사용량과 이미지 로딩 부담을 줄일 수 있습니다.

이후 캐시 정책과 ViewPager2 설정을 개선하고, 메모리 사용량을 측정하면서 단계적으로 최적화하는 것이 좋습니다.

이미지 최적화의 핵심은 이미지를 무조건 작게 만드는 것이 아니라, 사용자에게 필요한 화질을 유지하면서 불필요한 네트워크·메모리·디코딩 비용을 줄이는 것입니다.


참고 자료

태그: Android, Kotlin, Glide, RecyclerView, ViewPager2, Bitmap, WebP, AVIF, 이미지 최적화, 이미지 캐시, OOM, Memory Profiler, LeakCanary, Android 성능 최적화