시배's Android

Compose | Pager OOM(Out of Memory) issue 본문

Android/Compose

Compose | Pager OOM(Out of Memory) issue

si8ae 2024. 7. 19. 19:56

최근 진행 중인 홈노크타운 프로젝트에서 Hi-Jack-Mocker 라이브러리를 연동하는 과정에서 OOM 문제를 겪었습니다. 이 문제를 해결하기 위해 여러 시도를 해보았고, 그 과정을 공유하고자 합니다.

 

 

GitHub - koreatlwls/Hi-Jack-Mocker: Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify net

Hi-Jack-Mocker is a project that leverages OkHttp3's interceptor to intercept and modify network requests and responses, allowing you to verify the UI easily. - koreatlwls/Hi-Jack-Mocker

github.com

Hi Jack Mocker는 비개발자도 UI 엣지 케이스를 쉽게 테스트할 수 있도록 돕는 라이브러리입니다. 이 프로젝트는 OkHttp3 인터셉터를 활용하여 네트워크 요청과 응답을 가로채고 수정할 수 있게 합니다. 이를 통해 개발자뿐만 아니라 다양한 사용자들이 다양한 시나리오를 테스트할 수 있게 합니다.

 

문제의 시작 : 무한 스크롤 배너

  • 홈노크타운 프로젝트 상단 배너: 프로젝트 상단에 사용자가 무한 스크롤할 수 있는 롤링 배너가 존재합니다.
  • PagerState 설정: 이 배너는 무한 스크롤 기능을 위해 PagerState의 pageCount를 Int.MAX_VALUE로 설정했습니다.

프로젝트 구성과 버전 차이

  • Compose 버전 차이: 홈노크타운 프로젝트는 비교적 낮은 버전의 Compose를 사용하고 있었습니다. 하지만 HiJackMocker는 최신 Compose 버전을 사용하고 있었습니다.
  • 의존성 추가: 개발 도중 HiJackMocker를 devImplementation에 추가한 후, OOM 문제가 발생하기 시작했습니다.

문제 분석

  • LeakCanary 사용: 메모리 누수를 의심해 LeakCanary를 추가하여 살펴보았지만, 문제점을 발견하지 못했습니다.
  • 원인 추적: 우연히 배너가 스와이핑되지 않아 배너를 제거하고 확인해보니 OOM이 발생하지 않았습니다. 이를 통해 배너와 관련된 문제가 있다는 것을 알게 되었습니다.

해결 과정 

  • Issue Tracker 확인: Compose의 Issue Tracker를 확인해보니 유사한 문제가 보고되어 있었습니다.
  • 수정 사항 확인: 해당 문제의 수정 사항을 보니, maxScrollOffset을 Int.MAX_VALUE에서 Float.MAX_VALUE로 변경한 것이었습니다.
 

Google Issue Tracker

 

issuetracker.google.com

PagerState의 문제점

1. maxScrollOffset 계산 

internal fun PagerLayoutInfo.calculateNewMaxScrollOffset(pageCount: Int): Long {
    val pageSizeWithSpacing = pageSpacing + pageSize
    val maxScrollPossible =
        (pageCount.toLong()) * pageSizeWithSpacing + beforeContentPadding + afterContentPadding
    // ... 나머지 코드
}

이 부분에서 pageCount가 매우 크면 maxScrollPossible이 Int.MAX_VALUE를 쉽게 초과할 수 있습니다.

2. 스크롤 계산

private var maxScrollOffset: Int = Int.MAX_VALUE
	private set

private fun performScroll(delta: Float): Float {
    val currentScrollPosition = currentAbsoluteScrollOffset()
    // ... 중간 코드 생략
    val updatedScrollPosition = (currentScrollPosition + decimalAccumulationInt)
    val coercedScrollPosition = updatedScrollPosition.coerceIn(minScrollOffset, maxScrollOffset)
    // ... 나머지 코드
}

여기서 updatedScrollPosition과 coercedScrollPosition이 Int.MAX_VALUE를 초과하는 경우, 이전에는 오버플로우나 부정확한 계산이 발생했을 수 있습니다.

  • Pager는 일반적으로 가상화 기법을 사용하여 화면에 보이는 항목만 메모리에 유지합니다.
  • scrollPosition이 잘못 계산되면, 시스템이 실제로 필요한 것보다 훨씬 많은 페이지를 메모리에 로드하려 할 수 있습니다.

3. 리소스 누수

internal fun applyMeasureResult(
    result: PagerMeasureResult,
    visibleItemsStayedTheSame: Boolean = false
) {
    // ... 코드 생략
    pagerLayoutInfoState.value = result
    // ... 나머지 코드
}
  • 잘못된 scrollPosition으로 인해 applyMeasureResult가 부정확한 결과를 적용할 수 있습니다.
  • 이로 인해 더 이상 필요하지 않은 페이지 객체들이 메모리에서 해제되지 않을 수 있습니다.

4. 무한 루프 가능성

private fun notifyPrefetch(delta: Float, info: PagerLayoutInfo) {
    // ... 코드 생략
    val indexToPrefetch = // 계산 로직
    if (indexToPrefetch in 0 until pageCount) {
        // 프리페치 로직
    }
}

indexToPrefetch 계산 시 매우 큰 값이 사용될 수 있으며, 이는 과도한 메모리 사용으로 이어질 수 있습니다.

이러한 부분들에서 Int.MAX_VALUE를 초과하는 값들이 사용될 때 오버플로우, 부정확한 계산, 또는 예상치 못한 동작이 발생할 수 있었고, 이는 결과적으로 과도한 메모리 사용이나 OOM을 유발할 수 있었습니다.

  • scrollPosition 계산 오류로 인해 notifyPrefetch가 계속해서 새로운 페이지를 프리페치하려 할 수 있습니다.
  • 이는 지속적인 메모리 할당으로 이어질 수 있습니다.

이러한 부분들에서 Int.MAX_VALUE를 초과하는 값들이 사용될 때 오버플로우, 부정확한 계산, 또는 예상치 못한 동작이 발생할 수 있었고, 이는 결과적으로 과도한 메모리 사용이나 OOM을 유발할 수 있었습니다. Float.MAX_VALUE로 변경함으로써 OOM이 발생하지 않게 되었습니다. 

향후 계획

안정화 버전 기다리기: 현재 이 수정 사항은 1.7.0-alpha01 버전에만 적용되어 있습니다. 아직 stable 버전으로 올라오지 않았기 때문에, 안정화 버전을 기다려야 할 것 같습니다.

결론

이번 경험을 통해 최신 라이브러리를 사용하는 프로젝트와의 의존성 추가 시 발생할 수 있는 문제를 해결하는 과정을 배웠습니다. 특히, 메모리 관련 문제는 작은 설정 하나로도 큰 영향을 미칠 수 있다는 점을 다시 한번 깨닫게 되었습니다. 앞으로도 꾸준한 모니터링과 최신 정보 확인을 통해 더 안정적인 서비스를 제공할 수 있도록 노력해야겠습니다.