본문 바로가기
안드로이드

Android 17부터 앱을 강제 종료시키는 메모리 제한, 지금 대비하는 법

by 안드뽀개기 2026. 6. 9.
반응형

Android 17(API 36)을 타깃으로 하는 앱, 또는 minSdk 24 이상의 중대형 앱에서 곧바로 적용 가능한 내용입니다. Kotlin 1.9 이상이면 되고 Compose와 View 기반 모두 상관없습니다. 별도 라이브러리 의존성 없이 Android SDK API만으로 시작할 수 있으며, 이미지 로딩 섹션에서는 Coil 2.x 또는 Glide 4.x가 필요합니다.


Android 17이 앱을 끊는 방식이 바뀌었다

기존에는 메모리를 과도하게 사용하는 앱도 Low Memory Killer(LMK)가 다른 캐시 앱을 먼저 정리하는 방식으로 살아남는 경우가 많았습니다. Android 17부터는 기기 전체 RAM을 기준으로 앱별 메모리 상한선을 설정하고, 이를 초과하면 스택 트레이스 없이 프로세스를 즉시 종료합니다.

문제는 이 종료가 ANR이나 Crash와 달리 Play Console에서 쉽게 포착되지 않는다는 점입니다. 앱이 갑자기 꺼졌다는 사용자 리뷰만 쌓이고 원인을 찾지 못하는 상황이 전형적인 패턴입니다.


앱이 메모리 한계로 죽었는지 감지하기

ApplicationExitInfo를 활용하면 메모리 제한 초과 여부를 코드로 확인할 수 있습니다.

// Application.onCreate() 또는 앱 시작 시점에 호출
fun checkMemoryLimitKill(context: Context) {
    val am = context.getSystemService(ActivityManager::class.java)
    val exitReasons = am.getHistoricalProcessExitReasons(
        context.packageName, 0, 5
    )

    for (reason in exitReasons) {
        if (reason.reason == ApplicationExitInfo.REASON_OTHER) {
            val desc = reason.description ?: continue
            if (desc.contains("MemoryLimiter:AnonSwap")) {
                // 메모리 한계 초과로 종료됨 → 서버 리포트 또는 로컬 로그
                Log.e("MemoryCheck", "앱이 메모리 제한으로 종료됨: $desc")
            }
        }
    }
}

REASON_OTHER이면서 description에 "MemoryLimiter:AnonSwap" 문자열이 있으면 메모리 한계 종료입니다. 이 로직을 앱 초기화 단계에 넣어두면 실제 기기에서 문제가 얼마나 발생하는지 파악할 수 있습니다.


이미지 로딩에서 가장 많이 낭비되는 메모리

RecyclerView나 LazyColumn에서 이미지 크기를 지정하지 않고 로드하면, 3000×2000 원본 이미지가 그대로 메모리에 올라옵니다. 이것이 실제로 가장 자주 발생하는 메모리 낭비 원인입니다. Coil 2.x를 기준으로 개선 전후를 비교하면 다음과 같습니다.

// Before: 원본 해상도 그대로 로드 → 힙 과점유 위험
AsyncImage(
    model = imageUrl,
    contentDescription = null
)

// After: 표시 영역 크기에 맞게 디코딩 요청
AsyncImage(
    model = ImageRequest.Builder(LocalContext.current)
        .data(imageUrl)
        .size(400, 400)                  // 표시 영역 px 기준으로 지정
        .precision(Precision.INEXACT)    // 정확도보다 메모리 절약 우선
        .build(),
    contentDescription = null
)

size() 하나만 추가해도 대형 이미지 목록에서 힙 사용량이 눈에 띄게 줄어듭니다. Glide를 쓴다면 override(width, height)가 동일한 역할을 합니다.


앱이 백그라운드로 가면 메모리를 돌려줘라

onTrimMemory 콜백은 시스템이 "지금 메모리 좀 줄여달라"고 요청하는 신호입니다. 이를 무시하면 LMK 대상이 될 가능성이 높아집니다.

class MyApplication : Application() {

    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
        when (level) {
            TRIM_MEMORY_UI_HIDDEN -> {
                // 앱이 백그라운드 전환 직후 → UI 캐시 우선 해제
                imageLoader.memoryCache?.trimToSize(0)
            }
            TRIM_MEMORY_RUNNING_CRITICAL,
            TRIM_MEMORY_COMPLETE -> {
                // 시스템 메모리 심각 → 모든 인메모리 캐시 비우기
                clearAllCaches()
            }
        }
    }

    private fun clearAllCaches() {
        // 앱 전체 인메모리 캐시 정리 로직
    }
}

TRIM_MEMORY_UI_HIDDEN은 앱이 화면에서 완전히 사라진 직후에 발생합니다. 이 시점에 이미지 캐시나 불필요한 리소스를 해제하면 백그라운드에서 강제 종료될 확률이 크게 낮아집니다.


R8으로 힙 크기 자체를 줄이기

앱 코드와 라이브러리의 미사용 클래스를 제거하면 메모리에 올라오는 코드 자체가 줄어듭니다. build.gradle.kts에서 릴리스 빌드에 R8 최적화를 활성화하세요.

// build.gradle.kts (app 모듈)
android {
    buildTypes {
        release {
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

proguard-android-optimize.txt는 기본 규칙보다 더 공격적인 최적화를 적용합니다. 처음 적용 시 난독화로 인한 크래시가 발생할 수 있으므로, 반드시 릴리스 빌드로 기능 테스트를 거친 뒤 배포하세요.


주의사항

메모리 한계 종료는 targetSdk 36 이상일 때 더 엄격하게 적용될 예정이지만, 하반기 중 일부 정책은 소급 적용될 수 있어 지금부터 대응하는 것이 안전합니다.

이미지 크기를 size()로 제한할 때 dp와 px를 혼용하면 의도치 않은 화질 저하가 생길 수 있습니다. Density를 고려해 실제 픽셀 값으로 계산해서 넘기는 것이 정확합니다.

onTrimMemory는 Activity와 Service에도 오버라이드 가능하지만 Application 레벨에서 한 곳에서 관리하는 편이 누락을 줄여줍니다. Activity별로 분산하면 일부 화면에서만 처리되는 실수가 자주 발생합니다.


지금 바로 시작하는 최소 단계

Application.onCreate()checkMemoryLimitKill() 함수 하나만 넣어보세요. 이미 배포된 앱에서 메모리 한계 종료가 얼마나 일어나고 있는지 로그로 확인하는 것, 그것이 첫 번째 행동입니다. 원인 파악 없이 최적화를 시작하면 엉뚱한 곳에 시간을 쏟게 됩니다.

기존 코드를 건드리지 않아도 되고 의존성 추가도 없습니다. 다음 패치 배포에 이 함수 하나만 추가해서 실제 상황을 먼저 측정해보세요.

※ 본 글은 정보 제공 목적이며 특정 제품·서비스의 추천이 아닙니다.

반응형