GS리테일 DX 블로그

Digital Transformation으로 고객 생활 가치의 이노베이션을 꿈꾸는 IT 사람들의 이야기

APP

안드로이드 포그라운드 서비스를 활용한 메모리부족으로 앱 종료되는 현상 개선

빅박용사 2023. 4. 17. 13:02

배경

우리동네GS 앱은 기존 ‘나만의 냉장고’ ‘GS THE FRESH’ ‘우리동네딜리버리’ 3개의 앱(GS25, GS The Fresh, 퀵커머스, 와인25플러스 4개의 비즈니스)을 하나로 통합하였다.
웹뷰기반의 하이브리드앱인 기존 서비스들을 Flutter를 사용하여 완전히 새롭게 개발하였고, 기존의 비즈니스와 기능을 단순 통합하지 않고, 새로운 비즈니스(픽업등)와 기존 앱의 가장 불안정한 랜더링 및 서비스 성능을 높이는 것이 주요 프로젝트의 목표였다.
새로운 기획&디자인을 만족하고 더 나은 성능의 새로운 앱을 만들기 위해, Front- end / Back-end에 다양한 솔루션을 도입했고, Front-end 는 메뉴 진입 속도 개선등을 위한 솔루션 사용으로 메모리 사용량 이슈(특히 구형 Android 기기에서)가 생길 가능성이 있었다.

 

메모리 사용량

Hello world 앱 비교시 각 OS Native 앱 대비 Flutter 앱이 약 30MB ~ 60MB 더 많이 사용하는 것으로 확인되어 큰 차이를 보이지 않았다.

 

그러나 Flutter는 기본적으로 자체적으로 메모리를 관리하며, 다양한 기능을 제공하기 때문에 Flutter 앱의 메모리 사용량은 Native 앱 대비 높아질 수 있다.  특히, 대부분 크로스 플랫폼이 그렇듯 다양한 플랫폼에서 동작할 수 있다는 이점은 앱의 크기와 메모리 사용량 증가에도 영향을 줄 수 밖에 없다.
따라서, Hello world 앱에서는 차이가 크게 나타나지 않더라도, 실제 앱에서는 Flutter 앱이 Native 앱 대비 메모리 사용량이 높아질 수 있다.

 

개발 초기에 메모리 사용량이 얼마나 높을지 예측이 힘들기 때문에, 선 오픈한 비슷한 규모 앱을 벤치마킹할 수 밖에 없다. 다행히 Flutter 기반으로 제작되어 안정적으로 서비스중인 GS SHOP 앱이 있었고, 메모리 사용량으로 인한 서비스 장애에 대한 리포트가 없었기에 어느정도 안심할 수 있었다. 

 

오픈 이후 장애접수

앱 출시 이후 안드로이드 이용 고객들을 통해 앱이 종료되는 현상이 다수 접수되었다. 

  • ISP 결제시 앱이 종료됨
  • GS pay 계좌등록을 위해 전화 본인인증후 복귀 시 앱이 종료됨

현상이 발생한 고객들 대부분은 RAM 4G, Android 9 기반의 오래된 저사양 단말(갤럭시 S8)을 사용하고 있었다. 

로그 및 리포트된 내용을 종합해본 결과, 외부앱 호출 후 복귀 시 현상이 발생하는 것으로 확인되었고, 이런 현상은 대부분 앱이 백그라운드로 전환된 이후 안드로이드 시스템에 의해 앱이 강제 종료되거나, 앱이 더 이상 메모리를 확보하지 못하게 되어 발생하게 되는 케이스들이다.

비슷한 단말을 사용하여 동일 현상 재현테스트를 진행하였고, 이 과정에서 flutter engine 내부에서 null pointer 참조 오류(segmentation fault)가 발생하는 것이 확인되었다.

앱 종료 로그
F/libc    ( 1770): Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x40 in tid 2076 (RenderThread), pid 1770 (com.gsr.gs25)
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'samsung/dreamlteks/dreamlteks:9/PPR1.180610.011/G950NKSU5DVG2:user/release-keys'
Revision: '11'
ABI: 'arm64'
pid: 1770, tid: 2076, name: RenderThread  >>> com.gsr.gs25 <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x40
Cause: null pointer dereference
    .
    .
    .
    중간생략
    .
    .
    .
    #14 pc 0000000000084df0  /system/lib64/libc.so (__pthread_start(void*)+208)
    #15 pc 0000000000023a8c  /system/lib64/libc.so (__start_thread+68)

 

안드로이드 9 버전

안드로이드 9.0부터는 "앱 스탠바이 모드"라는 기능이 추가되었다.
이 기능은 사용자가 일정 시간 동안 앱을 사용하지 않으면 해당 앱의 백그라운드 작업을 제한하여 배터리 수명을 연장한다.
이 때, 메모리를 많이 사용하는 앱은 앱 스탠바이 모드의 대상이 되고, 또한, 최근에 사용된 앱이라 하더라도 메모리 사용량이 높으면 종료될 가능성이 있다는 것을 확인되었다.

 

결국, 안드로이드 9 버전부터는 직전에 사용된 앱일지라도 메모리 사용량이 높을 경우 종료된다는 이야기이다.

하지만 테스트시 안드로이드 10 이상 버전에서는 이러한 현상이 거의 발생하지 않는 것으로 보아, 메모리 관리 방식이 개선되어 더욱 효율적으로 메모리를 관리할 수 있게 되었기 때문으로 보인다.

 

메모리 최적화

개발자로서 메모리 사용량 최적화는 중요한 작업이다. 그러나 하드웨어 스펙과 플랫폼의 개선으로 인해 이러한 노력을 예전만큼하지 않고 점점 잊혀져 간 것 같다. 저사양 디바이스에서 앱이 백그라운드에서 종료되는 현상이 확인된 후 잊고 있었던 메모리 최적화의 중요성을 다시 느끼게 되었다. 

 

Flutter 환경에서의 메모리 사용량을 줄이기 위해, 메모리 캐시 제거, 백그라운드 화면 제거, 프리로딩된 캐시 제거 등 다양한 작업을 수행하였다. 하지만 현상은 동일하게 발생하였다. 

나름 최적화 작업을 통해 메모리 사용량은 줄였지만, 백그라운드에 함께 존재하는 앱들중 다른 앱이 더 적은 메모리를 사용하고 있거나, 새롭게 실행되는 앱이 너무 많은 메모리를 필요하게 될 경우, 여전히 시스템은 메모리 확보를 위해 상대적으로 사용량이 높은 우리앱을 종료시킨으로 보인다. 
결국 안드로이드 시스템상 메모리 사용량은 앱 간 상대적인 것으로, 아무리 메모리 사용량을 줄여도 다른앱들로 인해 앱이 종료 될 수 있다는 것이다.


결과적으로 메모리 최적화 작업으로 얻은 것은 앱이 조금 더 오래 실행될 수 있는 시간을 조금 벌어준 것 뿐 근본적인 이슈가 해결된 것은 아니다.

 

생각의 전환

메모리 최적화 작업은 노력대비 효과가 미비하여 더 이상 진행하지 않기로 하였다.
대신 백그라운드 앱 종료시 메모리 최적화 대상에서 제외되는 방법을 찾기로 했고, 이를 위해 포그라운드 서비스를 실행시켜 보기로 하였다. 
포그라운드 서비스를 실행시키게 되면 앱이 활성화된 앱과 우선순위가 같아져 백그라운드에 있더라도 종료될 가능성이 적어지게 된다.

아래는 어떤 블로거가 작성한 내용이니 참조하기 바란다.

  • Foreground Service는 활성화된 액티비티와 동급의 우선순위를 가집니다. 그래서 시스템에 메모리가 부족하더라도 Android System 에의해 종료될 확률이 적습니다. Foreground Service는 상태 바(Status bar)에 알림을 표시해야합니다.

 

포그라운드 서비스 셋팅

포그라운드 서비스를 실행시켜도 실제 서비스에서 별도 작업은 하지 않고, 실행만 시켜놓을꺼라 적용이 쉽고 간단한 라이브러리로 선택하였다.

  • 라이브러리: flutter_foreground_task
  • AndroidManifest.xml 
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

    <service android:name="com.pravera.flutter_foreground_task.service.ForegroundService" android:stopWithTask="true" />
  • 포그라운드 서비스 초기화 코드
    void initForegroundTask() {
      // 안드로이드인 경우만 실행
      if (!Platform.isAndroid) {
        return;
      }

      // 버전이 9인 경우만 실행
      if (DeviceInfoUtility.osVersion != 9) {
        return;
      }

      FlutterForegroundTask.init(
        androidNotificationOptions: AndroidNotificationOptions(
          channelId: 'notification_channel_id',
          channelName: 'Foreground Notification',
          channelDescription: 'This notification appears when the foreground service is running.',
          channelImportance: NotificationChannelImportance.LOW,
          priority: NotificationPriority.LOW,
          iconData: const NotificationIconData(
            resType: ResourceType.mipmap,
            resPrefix: ResourcePrefix.ic,
            name: 'launcher',
          ),
        ),
        iosNotificationOptions: const IOSNotificationOptions(
          showNotification: false,
          playSound: false,
        ),
        foregroundTaskOptions: const ForegroundTaskOptions(
          interval: 5000,
          isOnceEvent: false,
          autoRunOnBoot: false,
          allowWakeLock: false,
          allowWifiLock: false,
        ),
      );
    }
  • 서비스 실행/종료 코드
    startForegroundTask() async {
      // 안드로이드인 경우만 실행
      if (!Platform.isAndroid) {
        return;
      }

      // 버전이 9인 경우만 실행
      if (DeviceInfoUtility.osVersion != 9) {
        return;
      }

      if (!GsStorage.useAndroidForegroundService) {
        return;
      }

      if (await FlutterForegroundTask.isRunningService) {
        return FlutterForegroundTask.restartService();
      } else {
        return FlutterForegroundTask.startService(
          notificationTitle: '우리동네GS 최적화 모드 실행중입니다.',
          callback: startCallback,
        );
      }
    }

    stopForegroundTask() {
      // 안드로이드인 경우만 실행
      if (!Platform.isAndroid) {
        return;
      }

      // 버전이 9인 경우만 실행
      if (DeviceInfoUtility.osVersion != 9) {
        return;
      }

      return FlutterForegroundTask.stopService();
    }

    // The callback function should always be a top-level function.
    @pragma('vm:entry-point')
    void startCallback() {
      // The setTaskHandler function must be called to handle the task in the background.
      FlutterForegroundTask.setTaskHandler(FirstTaskHandler());
    }

    class FirstTaskHandler extends TaskHandler {
      @override
      Future<void> onStart(DateTime timestamp, SendPort? sendPort) async {}

      @override
      Future<void> onEvent(DateTime timestamp, SendPort? sendPort) async {}

      @override
      Future<void> onDestroy(DateTime timestamp, SendPort? sendPort) async {}

      @override
      void onButtonPressed(String id) {}

      @override
      void onNotificationPressed() {
        FlutterForegroundTask.launchApp("/resume-route");
      }
    }
  • 안드로이드 9 버전에서만 보이는 설정 메뉴 추가 (우리동네GS 최적화 모드)

  • 실행된 모습

 

결론

포그라운드 서비스 실행후 앱 종료되는 현상은 더 이상 발생하지 않았다. 

역시 예상한 것과 같이 활성화된 앱과 동일하게 취급되어 효과가 있었던 것으로 보인다.
그래서 좀 더 열악한 환경에서 테스트 해보기로 하였고, 내가 생각해도 이건 심하다 싶을 정도의 테스트 환경을 조성하여 테스트를 진행하였다.
역시 100% 막지는 못하는것 같다.

 

앞서 언급했듯, 안드로이드 9 버전에서는 이 문제가 100% 발생하였으나, 동일한 4GB RAM을 사용하는 안드로이드 10 이상 버전에서는 거의 발생하지 않았다. 정확한 원인은 알 수 없지만 메모리 관리 방식이 개선되어 더욱 효율적으로 메모리를 관리하기 때문으로 보인다. 


초기에는 이 문제를 당연히 메모리 이슈로 생각했고, 앱에서 사용하는 메모리 사용량을 줄여 최대한 앱을 가볍게 만들려고만 생각했었었다.

이 과정에서 많은 시간과 노력을 소비하였지만, 다른 앱에 의해 우리 앱이 종료될 수 있다는 것을 깨닫게 되었고, 그로인해 다른 방식을 고민하게 되었고, 포그라운드 서비스를 활용하여 개선까지 생각할 수 있었던 것 같다.

 

현재 적용된 포그라운드 서비스 방식이 현 이슈에 큰 효과가 있는건 사실이지만, 전혀 다른 목적으로 사용한거라 맞는지는 아직도 의문이다.
그러나 현재 상황에서는 이 방식이 최선이라고 생각한다.

효과 및 확대

포그라운드 서비스를 이용한 최적화모드는 우리동네GS 앱에 기본기능으로 탑재되었고, 특히 안드로이드9 이하 버전인 경우 디폴드로 활성화하여 앱종료되는 현상을 미연에 방지하고 있다.
우리동네GS 앱 오픈이후 2022년말부터 ~ 2023년초 까지 꾸준히 인입된 이슈들은 더이상 리포트되지 않고 있다.
이 기능은 앞으로 더 많은 서비스에 확대 적용할 예정이다.

 


박성화 | 디지털서비스본부 > 모바일FO팀
우리동네GS / GS 프레시몰 앱 개발을 담당
프론트엔드 새로운 기술에 대한 관심이 많습니다. 

 


참고 자료

[안드로이드] 서비스(Service)에 대해 알아보자 https://jizard.tistory.com/216