앱 개발에서의 OutOfMemoryError: 개념, 원인 및 예방 방법

저자: IT Sectr 게시일: 2026-03-29 읽는 시간: 9 분

OutOfMemoryError는 JVM(Java Virtual Machine) 또는 Android Runtime(ART)이 힙(Heap) 공간 부족으로 새 객체에 메모리를 할당할 수 없을 때 발생하는 치명적인 예외입니다. Square Engineering에 따르면, 모바일 애플리케이션에서 OutOfMemoryError의 70%는 실제 한도 초과가 아닌 메모리 누수로 인해 발생합니다. OOM의 원인을 이해하는 것이 애플리케이션 안정성의 핵심입니다.

주요 내용

  • OutOfMemoryError — 새 객체를 생성하기에 Heap이 부족할 때의 예외
  • Heap — 모든 Java/Kotlin 객체가 존재하는 메모리 영역
  • Bitmap — Android에서 Heap의 주요 소비자, 전형적인 OOM 원인
  • Heap Dump — 누가 얼마나 많은 메모리를 점유하는지 분석하기 위한 Heap 스냅샷
  • OOM 치료는 누수 수정과 메모리 소비 최적화가 필요

OutOfMemoryError란

OutOfMemoryError(OOM)는 Java/Kotlin의 VirtualMachineError 계열에 속하는 예외로, 새 객체에 메모리를 할당할 수 없음을 나타냅니다. 확인된 예외(checked exception)와 달리 OOM은 Error이므로 catch를 통한 처리가 필요하지 않지만, 기술적으로는 잡을 수 있습니다. OOM이 발생한 후에는 애플리케이션이 일반적으로 불안정한 상태에 있으므로 종료하는 것이 좋습니다.

Android에서는 각 애플리케이션에 기기 제조업체가 설정한 Heap 제한이 있습니다. 6+ GB RAM의 최신 스마트폰의 경우 제한은 256~512MB, 보급형 기기의 경우 128~192MB입니다. 모든 활성 객체의 총량이 이 제한을 초과하면 ART가 OutOfMemoryError를 던집니다.

중요한 것은 OOM이 항상 기기의 물리적 메모리가 부족하다는 의미는 아니라는 점입니다. 이는 애플리케이션이 시스템에서 설정한 Heap 제한을 소진했음을 의미합니다. 다른 애플리케이션에는 여유 메모리가 있을 수 있지만, Android의 프로세스 격리로 인해 사용자의 애플리케이션은 이를 사용할 수 없습니다.

OutOfMemoryError의 주요 원인

다섯 가지 시나리오가 모바일 애플리케이션에서 정기적으로 OOM을 발생시킵니다. 각 시나리오는 특정 데이터 유형 또는 작업과 관련됩니다.

스케일링 없는 Bitmap

Bitmap은 Android 애플리케이션의 주요 메모리 소비자입니다. FullHD 이미지(1920×1080)를 원래 크기로 로드하면 ARGB_8888 형식으로 8.3MB를 차지합니다. RecyclerView에 이러한 이미지가 50개 있으면 415MB로, 어떤 기기의 Heap도 초과합니다. inSampleSize 없이 이미지를 로드하면 저사양 기기에서 확실히 OOM이 발생합니다.

자동 스케일링을 위해 Glide 또는 Coil을 사용하세요. 이러한 라이브러리는 원본 해상도가 아닌 View에 맞는 크기로 이미지를 로드합니다. BitmapFactory.Options를 직접 사용하는 경우 inSampleSize를 적용하세요. 2의 거듭제곱으로 계산하여 최종 크기가 2048×2048픽셀을 초과하지 않도록 합니다. 추가로, 투명도가 없는 이미지에는 ARGB_8888 대신 RGB_565를 사용하면 메모리 소비가 절반으로 줄어듭니다.

kotlin
fun loadScaledBitmap(path: String, reqWidth: Int): Bitmap? {
    val opts = BitmapFactory.Options().apply {
        inJustDecodeBounds = true
    }
    BitmapFactory.decodeFile(path, opts)
    opts.inSampleSize = calculateSampleSize(opts.outWidth, reqWidth)
    opts.inJustDecodeBounds = false
    return BitmapFactory.decodeFile(path, opts)
}

메모리 누수(축적)

단일 수 KB의 누수는 OOM을 유발하지 않습니다. 그러나 각 화면에서 수십 개의 누수가 축적됩니다. 화면 전환마다 누수가 추가되고 GC가 객체를 해제할 수 없으며 Heap이 가득 찹니다. 일반적인 패턴: 사용자가 프로필 화면을 20번 열고 닫음 → Heap이 200MB 증가 → 애플리케이션이 OOM으로 크래시.

자동 누수 감지를 위해 프로젝트에 LeakCanary를 설치하세요. 정확한 스택 추적과 함께 누수된 각 객체를 보여줍니다. 모든 누수를 수정한 후 Heap 소비가 안정화됩니다. 화면을 닫은 후 메모리가 기본 수준으로 돌아옵니다.

메모리 내 대용량 파일

전체 파일을 byte[]로 로드하는 것은 OOM으로 가는 직접적인 경로입니다. 50MB JSON 파일은 구문 분석 시 동일한 크기의 문자열과 DOM 모델을 생성합니다. 메모리에 로드된 비디오 파일, 오디오 버퍼, 대규모 protobuf 데이터셋 모두 단일 작업으로 Heap 제한을 초과할 수 있습니다.

대용량 데이터는 스트림으로 처리하세요. 4~8KB 버퍼의 InputStream, 스트리밍 JSON 파서(Jackson 또는 Gson + JsonReader), 비디오용 MediaCodec. 사용 가능한 Heap의 10%를 초과하는 파일에서 File.readBytes()를 호출하지 마세요.

루프에서 많은 객체 생성

중간 GC 없이 루프에서 집중적으로 객체를 생성하면, 특히 Heap이 작은 기기에서 OOM이 발생할 수 있습니다. 예: GC가 수집하기 전에 Heap에 들어가지 않는 100,000개의 객체를 for-loop에서 생성. 이는 게임 및 그래픽 편집기에서 더 흔합니다.

대량으로 생성 및 소멸되는 객체에는 Object Pool을 사용하세요. 숫자 데이터에는 기본형을 사용합니다(List<Float> 대신 FloatArray). ViewHolder Pool이 있는 RecyclerView는 UI 구성 요소에서 이 문제를 해결합니다.

Heap 단편화

단편화는 전체적으로 충분한 여유 메모리가 있지만 새 객체를 위한 연속 블록이 없는 상태입니다. ART는 GC 중에 Heap을 압축하지만 항상 성공하지는 않습니다. 큰 배열(Bitmap, byte[])이 단편화에 가장 취약합니다.

Android 8+의 ART는 세대별 GC를 사용하여 젊은 객체와 오래된 객체를 분리함으로써 단편화를 줄입니다. 그럼에도 동일한 풀에서 다른 크기의 조각을 할당하지 말고 미리 할당된 고정 크기 버퍼를 사용하세요.

Android의 Heap 제한

Android의 Heap 제한은 상수가 아닙니다. 제조업체, 기기 모델 및 OS 버전에 따라 다릅니다. Google은 CDD(Compatibility Definition Document)를 통해 최소 요구 사항을 설정하지만, 실제 값은 제조업체가 결정합니다.

기기 카테고리일반 HeaplargeHeap
보급형(1~2 GB RAM)128~192 MB256~384 MB
중급형(3~4 GB RAM)256~384 MB512 MB
플래그십(6+ GB RAM)384~512 MB768 MB~1 GB
태블릿(4+ GB RAM)256~512 MB768 MB
Wear OS32~64 MB없음

매니페스트에서 android:largeHeap="true"를 통해 증가된 제한을 요청할 수 있습니다. 주의해서 사용하세요. Heap을 늘려도 누수 문제가 해결되지 않으며, 시스템이 사용자의 애플리케이션을 위해 메모리를 확보하기 위해 다른 애플리케이션을 강제 종료해야 하는 경우 사용자 경험이 악화될 수 있습니다. Wear OS의 경우 Heap 제한이 최소 32~64MB에 불과하며, largeHeap을 사용할 수 없고 메모리 절약이 두 배로 중요합니다.

OutOfMemoryError 진단

OOM 진단에는 Heap Dump 분석과 어떤 객체가 메모리를 소비하는지 이해하는 것이 필요합니다. Android Studio는 필요한 모든 도구를 제공합니다.

1단계: OOM 순간을 포착합니다. Android Memory Profiler에서 Record memory allocations를 클릭하고 크래시를 유발하는 시나리오를 실행합니다. Profiler는 OOM 전에 할당 급증을 보여줍니다. OOM이 재현되지 않는 경우 디버그 빌드에서 android:smallHeap을 통해 Heap을 줄이거나 수동 GC 호출로 DDMS를 사용합니다.

2단계: 최대 부하 시(OOM 전) Heap Dump를 캡처합니다. Android Studio에서 Dump를 엽니다. Classes 탭은 Retained Size로 정렬됩니다. 가장 큰 객체는 Bitmap, byte[], String입니다. 각 Bitmap의 크기(너비×높이×4바이트)와 Stack Trace를 통한 로드 경로를 확인합니다.

3단계: 중복 객체 수를 분석합니다. 200개의 동일한 Fragment 또는 Activity가 보이면 누수입니다. 동일한 크기의 500개 Bitmap이 있으면 이미지 캐싱 문제입니다. MAT(Memory Analyzer Tool)는 Dominator Tree를 통해 어떤 객체가 Heap의 80%를 보유하고 있는지 보여주는 심층 분석을 제공합니다.

text
// adb를 통한 Heap Dump 명령
adb shell am dumpheap com.example.app /data/local/tmp/dump.hprof
adb pull /data/local/tmp/dump.hprof.

OOM 예방 전략

포괄적인 OOM 예방 전략은 아키텍처 결정부터 프로덕션 모니터링까지 5가지 보호 수준을 포함합니다.

아키텍처 결정

ViewModel + Repository 패턴은 데이터를 UI에서 분리하고 화면 회전 시 View 유지를 방지합니다. ViewModel은 Activity보다 오래 지속되며 데이터가 손실되지 않고 View가 메모리의 데이터를 복제하지 않고 다시 생성될 수 있습니다. 명시적 상태 관리를 위해 LiveData 대신 StateFlow를 사용하세요.

Bitmap 및 이미지 관리

Glide는 이미지 작업을 위한 필수 라이브러리입니다. 자동으로 Bitmap을 스케일링, 캐싱(디스크+메모리) 및 재활용합니다. 대규모 목록의 경우 diskCacheStrategy와 skipMemoryCache를 구성하세요. 애니메이션 이미지의 경우 GIF/WebP와 함께 Glide를 사용하세요. Bitmap 시퀀스보다 메모리를 적게 사용합니다.

프로덕션 모니터링

Firebase Performance Monitoring은 실시간으로 메모리 소비를 추적합니다. Heap 사용량이 제한의 80%를 초과하면 알림을 설정하세요. 이는 조사 신호입니다. Crashlytics는 OOM을 예외로 수집하고 크래시 전 마지막으로 알려진 Heap 상태를 표시합니다. Android 11+의 경우 ApplicationExitInfo를 사용하여 OOM 종료를 감지합니다.

저사양 기기에서 테스트

반드시 최소 Heap(128~192MB) 기기에서 애플리케이션을 테스트하세요. 작은 화면과 작은 Heap의 에뮬레이터는 보급형 기기를 에뮬레이트합니다. 이러한 기기에서 애플리케이션이 작동하면 플래그십에서 OOM 문제가 발생하지 않습니다. 다양한 가격대의 실제 기기로 Firebase Test Lab을 사용하세요.

kotlin
// 무거운 작업 전 사용 가능한 Heap 확인
fun canAllocate(requiredBytes: Long): Boolean {
    val runtime = Runtime.getRuntime()
    val free = runtime.freeMemory()
    return free > requiredBytes * 2 // 50% 버퍼
}

자주 묻는 질문

OutOfMemoryError를 try-catch로 잡을 수 있나요?

기술적으로는 가능하지만 권장되지 않습니다. OOM 후 애플리케이션은 불안정한 상태에 있으며 새 할당이 실패할 수 있고 일부 객체가 부분적으로 생성될 수 있습니다. catch에서 합리적인 유일한 조치는 로깅 및 Activity 재시작입니다.

왜 OOM이 모든 기기에서 발생하지 않나요?

Heap 제한은 기기마다 다릅니다. 300MB가 필요한 작업은 192MB 제한 기기에서 크래시되지만 512MB 플래그십에서는 성공합니다. OOM 시나리오를 감지하려면 최소 사양의 기기에서 테스트하세요.

largeHeap이 성능에 어떤 영향을 미치나요?

largeHeap은 제한을 늘리지만 애플리케이션을 빠르게 하지 않습니다. 큰 Heap을 수집하는 데 더 많은 시간이 걸리므로 GC 일시 중지가 길어집니다. 시스템이 메모리를 확보하기 위해 백그라운드 애플리케이션을 종료할 수 있습니다. largeHeap은 객관적으로 많은 메모리가 필요한 애플리케이션(카메라, 편집기)에만 사용하세요.

OOM과 시스템 종료의 차이점은 무엇인가요?

OOM은 Heap이 부족할 때 애플리케이션 내부의 예외입니다. 시스템 종료(Low Memory Killer)는 다른 애플리케이션을 위해 메모리를 확보하기 위해 프로세스를 종료하는 Linux 커널의 결정입니다. 시스템 종료 시 애플리케이션은 예외를 받지 않으며 프로세스가 그냥 종료됩니다.

Bitmap은 실제로 얼마나 많은 메모리를 소비하나요?

공식: 너비 × 높이 × bytesPerPixel. ARGB_8888 = 4B/픽셀, RGB_565 = 2B/픽셀. ARGB_8888의 FullHD Bitmap(1920×1080) = 8.3MB. 4K Bitmap(3840×2160) = 33MB. 이미지는 항상 화면 표시에 필요한 크기로 스케일링하세요.

요약

  • OutOfMemoryError — 애플리케이션의 Heap 제한이 소진되었을 때의 치명적인 예외
  • 스케일링 없는 Bitmap — 모바일 애플리케이션에서 OOM의 주요 원인
  • 메모리 누수는 각 전환 시 객체 축적을 통해 OOM의 70%를 유발
  • Heap 제한은 보급형 기기에서 128MB부터 플래그십에서 512MB까지
  • Heap Dump와 Retained Size 분석이 OOM 진단의 주요 도구
  • Glide 또는 Coil은 모든 크기의 이미지 작업에 필수
  • 최소 Heap 기기에서의 테스트는 모든 프로젝트에 필수

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기