메모리 누수와 블로트 — 원인과 회피 방법

저자: IT Sectr 게시일: 2026-07-28 읽는 시간: 10 분

메모리 누수 — 모바일 개발에서 가장 교활한 문제 중 하나입니다. 앱의 메모리 사용량은 OS가 설정한 한계에 도달할 때까지 꾸준히 증가하며, 이후 OutOfMemoryError 또는 강제 종료가 발생합니다. Square Engineering에 따르면 약 40%의 Android 앱에 프로파일링을 통해서만 발견할 수 있는 메모리 누수가 하나 이상 있습니다. 원인과 메모리 증가 방지 방법을 살펴보겠습니다.

핵심 요점

  • GC 도달 가능성 — 루트 세트에서 활성 참조가 있으면 객체가 제거되지 않음
  • Activity 또는 Context에 대한 정적 참조 — Android에서 누수의 가장 흔한 원인
  • LeakCanary — Android에서 자동 누수 탐지를 위한 표준 도구
  • WeakReference — 가비지 컬렉션을 방해하지 말아야 할 참조를 위한 해결책
  • Lifecycle-aware 컴포넌트 — 뷰가 소멸될 때 자동으로 구독 취소

메모리 누수와 앱 블로트란?

메모리 누수 — 앱에 더 이상 필요하지 않은 객체가 GC 루트 세트의 활성 참조가 여전히 가리키고 있기 때문에 힙에 계속 유지되는 상황입니다. 가비지 컬렉터는 이러한 객체를 살아있는 것으로 간주하고 제거하지 않습니다.

메모리 블로트 — 앱이 현재 작업을 수행하는 데 필요한 것보다 더 많은 메모리를 소비하는 광범위한 문제입니다. 원인: 과도한 캐싱, 객체 중복, 비최적 데이터 구조, 힙 단편화.

Android에서는 각 앱에 제한된 힙이 할당됩니다 (일반적으로 기기 및 OS 버전에 따라 64–512MB). iOS에서는 제한이 덜 엄격하지만, 한계에 가까워지면 시스템이 메모리 경고를 보냅니다.

특성AndroidiOS
힙 제한64–512MB (기기에 따라 다름)암시적 (시스템)
가비지 컬렉션ART (동시, 컴팩트)ARC (자동 참조 카운팅)
누수 메커니즘GC 루트 참조리테인 사이클 (강한 참조 사이클)
결과OutOfMemoryError메모리 경고 → 종료

Facebook Engineering Blog에 따르면, 메모리 누수는 모바일 앱 크래시 보고서의 약 15%를 차지합니다. Android에서는 메모리 부족 시 빈번한 GC 일시 중지로 인한 ANR도 추가됩니다.

Android 및 iOS의 일반적인 메모리 누수 패턴

Activity에 대한 정적 참조 — 고전적인 Android 누수입니다. 정적 필드나 싱글톤이 Activity에 대한 참조를 보유하면, 싱글톤이 살아있는 한 finish() 후에도 GC가 수집하지 않습니다. Activity는 뷰 계층 구조, 리소스 및 Context를 포함하는 무거운 객체입니다.

kotlin
object LeakHolder {
    var activityRef: Activity ?= null // leak: static reference to Activity
}

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle ?= null) {
        super.onCreate(savedInstanceState)
        LeakHolder.activityRef = this // ❌ MainActivity will never be GC'd
    }
}

익명 클래스와 람다 — 외부 클래스에 대한 참조를 암시적으로 보유합니다. Runnable 또는 Callback이 외부 서비스에 전달되고 Activity가 소멸되어도, 익명 클래스 객체는 여전히 큐에 남아 Activity의 가비지 컬렉션을 방해합니다.

  • Handler 지연 포함 — Activity가 소멸되었지만 Handler.postDelayed가 아직 실행되지 않은 경우 Activity 누수 발생
  • Thread 및 AsyncTask — 화면 회전 시 Activity는 다시 생성되지만 이전 Thread는 이전 Activity에 대한 참조를 계속 보유
  • Retrofit/Callback — 익명 Callback이 프리젠터나 프래그먼트에 대한 참조를 보유
  • 관찰자 — onDestroy에서 구독 해지하지 않은 LiveData 또는 RxJava 구독

iOS에서는 주요 문제가 리테인 사이클입니다: 두 객체가 서로에 대한 강한 참조를 보유하고 ARC가 둘 중 어느 것의 참조 카운트도 0으로 만들 수 없습니다. 일반적인 사례: self를 강하게 캡처하는 클로저와 클로저에 대한 참조를 보유하는 self.

메모리 누수를 탐지하는 방법

LeakCanary — Android에서 자동 누수 탐지를 위한 Square의 라이브러리입니다. Activity 또는 Fragment가 소멸된 후, 객체가 GC에 의해 수집되었는지 확인합니다. 수집되지 않은 경우 힙 덤프를 생성하고 누수 추적을 표시합니다.

kotlin
// LeakCanary 2.x — auto-integration via Application
class ExampleApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        // LeakCanary auto-installs in debug build
        // via ContentProvider — zero code setup
    }
}

// Force check invocation
AppWatcher.objectWatcher.watch(watchedObject, "leak description")

Android Studio Profiler — 실시간 메모리 모니터링을 위한 내장 도구입니다. 힙 덤프 기록, 의심스러운 객체 찾기 (Retained Size > 1MB), 각 객체까지의 GC 루트 경로 추적이 가능합니다.

iOS의 경우 Xcode Memory Graph Debugger를 사용하세요. 메모리의 객체 그래프를 시각화하고, 리테인 사이클을 표시하며, 순환 참조를 즉시 탐지할 수 있습니다. 장기 모니터링을 위해 Instruments > Allocations도 사용 가능합니다.

예방 전략

WeakReference — 가비지 컬렉션을 방해하지 말아야 할 참조를 위한 기본 메커니즘입니다. GC가 객체를 수집하기로 결정하면 WeakReference는 null을 반환합니다. 콜백, 리스너 및 백그라운드 스레드에서 UI 컴포넌트에 대한 참조에 사용됩니다.

Lifecycle-aware 컴포넌트 — Android Jetpack (Lifecycle, LiveData, Flow, coroutines)에서 구현된 아키텍처 접근 방식입니다. onDestroy에서 구독이 자동으로 취소되어 주요 누수 클래스를 제거합니다.

kotlin
class MyViewModel : ViewModel() {
    private val _data = MutableLiveData<List<User>>()
    val data: LiveData<List<User>> get() = _data

    fun loadData() {
        viewModelScope.launch {
            val result = repository.fetchData()
            _data.postValue(result)
            // coroutine auto-cancels on onCleared()
        }
    }
}

viewModelScopelifecycleScope — Android에 내장된 CoroutineScope로, 해당 라이프사이클 이벤트에서 취소됩니다. 이는 현대 Android 개발에서 가장 흔한 시나리오인 코루틴을 통한 누수를 제거합니다.

  • 사용하지 마세요: Context, Activity, View 또는 Fragment에 대한 정적 참조
  • 취소하세요: onDestroy에서 disposeBag / CompositeDisposable의 모든 RxJava 구독
  • 사용하세요: iOS 클로저에서 [weak self] / [unowned self]로 리테인 사이클 방지
  • 확인하세요: Bitmap 및 큰 객체는 재활용되거나 null 처리되어야 함

메모리 프로파일링 도구

Android Studio의 Memory Profiler — 힙 모니터링을 위한 주요 도구입니다. 라이브 할당, 힙 스냅샷, 유형별 객체 수를 표시합니다. 덤프를 기록하고 MAT (Memory Analyzer Tool)에서 분석하여 의심스러운 객체를 찾을 수 있습니다.

Eclipse MAT — 데스크톱 힙 덤프 분석기입니다. Android Studio에서 HPROF 파일을 로드한 후, MAT는 지배자 트리를 구축하고 각 객체의 리테인 크기를 표시하며 Leak Suspects Report를 통해 자동 누수 분석을 제공합니다.

Xcode Memory Graph — 시각적 리테인 사이클 디버거입니다. Memory Graph Debugger 버튼을 클릭하면 Xcode가 앱을 중지하고, 메모리의 완전한 객체 그래프를 구축하며, 리테인 사이클을 빨간색으로 강조 표시합니다.

도구플랫폼특징
LeakCanaryAndroid소멸 후 누수 자동 탐지
Memory ProfilerAndroid Studio힙 덤프 + 라이브 할당
Eclipse MATAndroid지배자 트리, Leak Suspects Report
Memory GraphiOS (Xcode)리테인 사이클 시각화 도구

Google I/O 2023에 따르면, 디버그 빌드에서 LeakCanary를 사용하는 앱은 도입 후 첫 2개월 동안 메모리 관련 크래시가 30–50% 감소합니다. 프로젝트 온보딩 단계에서 LeakCanary를 추가하는 것이 좋습니다.

자주 묻는 질문

메모리 누수와 블로트의 차이는 무엇인가요?

누수 — 코드에서 접근할 수 없지만 활성 참조로 인해 GC가 수집하지 않는 객체. 블로트 — 앱이 논리적으로 필요하지만 과도한 양의 객체를 유지하는 상태 (예: 80MB 실행 앱에서 50MB 캐시). 블로트는 아키텍처적으로 해결하고, 누수는 올바른 참조 관리로 해결합니다.

LeakCanary는 어떻게 누수를 찾나요?

LeakCanary는 ObjectWatcher를 사용합니다 — Activity의 onDestroy() 후 Activity에 대한 WeakReference를 만들고 GC를 실행합니다. 5초 후 WeakReference가 지워지지 않으면 힙 덤프를 생성하고, GC Root에서 객체까지의 최단 참조 체인을 분석하여 파일과 코드 줄이 포함된 정확한 누수 스택을 표시합니다.

Bitmap이 자주 OutOfMemoryError를 일으키는 이유는?

Bitmap은 Java 힙 외부의 네이티브 메모리 (네이티브 힙)를 차지합니다. 하나의 Bitmap 크기 = 너비 × 높이 × 4바이트 (ARGB_8888). 12MP 사진 (4000×3000)은 48MB를 차지합니다. Android가 네이티브 메모리를 항상 적시에 해제할 수 있는 것은 아니므로, 여러 Bitmap이 축적되면 충분한 Java 힙이 있어도 OOM이 발생합니다.

iOS에서 리테인 사이클이란?

리테인 사이클 — ARC에서 두 객체가 서로에 대한 강한 참조를 보유하고 참조 카운트가 절대 0이 되지 않는 상황입니다. 일반적인 예: 클로저에 대한 강한 참조를 가진 ViewController와 self를 강하게 캡처하는 클로저. 해결책: 클로저에서 [weak self] 또는 [unowned self]를 사용하세요.

Android의 최대 힙 크기는 얼마인가요?

크기는 기기와 Android 버전에 따라 다릅니다. 구형 기기 (API 15–24)의 경우 64–128MB, 최신 기기 (API 25+)의 경우 256–512MB입니다. 정확한 값은 ActivityManager.getMemoryClass()를 통해 얻을 수 있습니다. 대규모 앱 (게임, 편집기)의 경우 매니페스트에서 largeHeap=true를 설정하면 최대 1GB까지 제공됩니다.

요약

  • 메모리 누수 — 루트 세트의 활성 참조로 인해 GC가 수집하지 못한 객체; 블로트 — 명시적 누수 없이 과도한 메모리 소비
  • Activity, Context, View에 대한 정적 참조 — Android 누수의 첫 번째 원인; 해결책 — WeakReference 또는 Application Context
  • 익명 클래스와 람다는 외부 클래스에 대한 참조를 암시적으로 보유; 취소되지 않은 콜백이 두 번째로 흔한 원인
  • LeakCanary — Android 누수 자동 탐지의 표준; 통합에 5분 소요, 크래시율 30–50% 감소
  • lifecycleScopeviewModelScope는 소멸 시 코루틴을 자동 취소하여 누수 클래스 전체 제거
  • iOS의 리테인 사이클은 클로저와 델리게이트에서 weak/unowned self로 해결
  • 메모리 프로파일링 — 스프린트당 최소 한 번, MAT 또는 Memory Graph로 힙 덤프를 코드 리뷰의 일부로 삼아야 함

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

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

프로젝트 논의

더 읽어보기