ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 캐시 전략 도입, 화면 로딩 속도 개선
    Android 2025. 9. 13. 11:12

    화면 데이터 수집의 문제점

    회사에서 개발하고 있는 앱은 여러 IoT 기기의 상태를 확인하거나 손쉽게 제어하기 위해 화면에 다양한 데이터를 보여준다.

    화면 데이터는 계층 구조로, 사용자 계정부터 계정에 연결된 장소, 장소에 연결된 기기, 기기에 연결된 자동화 등으로 구성된다.

     

    데이터 수집 과정

     

     

    데이터를 가져오기 위해선 데이터 별 API를 개별적으로 호출해 데이터를 수집해야 한다. 플랫폼 정책상, 모든 데이터를 단일 호출로 가져올 수 없다. 문제는 화면 데이터는 계층 구조로 되어 있으므로 순차적으로 호출할 수밖에 없는 구조라는 것이다. 사용자 계정에 설정된 현재 장소의 최소 정보를 이용해 장소 정보를 조회하고 결과를 받으면 하위 데이터를 순서대로 호출하는 것이다.

     

    물론 모든 데이터가 해당하는 것은 아니므로 가능한 병렬 호출로 시간을 단축할 수 있지만, n번의 api 호출 대기 동안 유저는 로딩 화면을 기다려야 한다. 과정이 여러 번인만큼, 네트워크 환경이 불안정하다면 UX에 크게 영향 갈 수 있으며 이는 사용자 이탈로 이어질 수 있다.

     

    MacroBenchmark를 이용해 로딩 과정을 분석한 결과, 모든 화면 구성요소가 그려지기까지 2.55초가 걸렸다.

    그중에서 API 호출 시간은 1.56초 로딩 시간의 61.2%를 차지한다. 

     

     

    문제 될만한 Jank가 없는 상태이므로 API 호출 과정을 기다리는 시간을 개선한다면 큰 폭으로 로딩 시간을 개선할 수 있을 것이라 생각했다. 이를 위해 로컬 캐시를 적용하였다. 그 과정을 기록한다.

     

    Local Storage 도입

    캐싱을 위해 로컬 데이터를 저장하는 방법은 다양하다. 나는 그중에서 Room과 Preference DataStore를 이용했다.

    ProtoBuf 기반의 빠른 속도를 제공하는 DataStore Proto도 고민했지만 다음 이유들로 Room을 도입했다.

    • 저장할 데이터가 4~5 Depth의 복잡도 있는 인스턴스로 구성되어 있다.
    • 사용자, 기기 이벤트로 인한 업데이트가 잦으며 특정 필드에 대한 데이터 업데이트가 필요하다.
    • 법인 사용자들이 사용하는 데이터(장소, 기기 등)의 개수가 100개를 넘기는 경우가 있으며 이를 고려해야 한다.

    IoT 기기는 기기가 가진 데이터 값이 수시로 변한다는 점에서 쿼리를 통해 변경 이벤트를 받은 필드만 변경할 수 있다. 그리고 화면은 한 가지의 api가 아닌 여러 api를 조합해 가져온 데이터의 결과물로 시점에 따라 전체 데이터를 업데이트해야 할 수도 있고 부분 데이터만 업데이트하는 경우가 있다. 따라서 Room을 도입해 기본적인 캐싱 처리를 하고 Primitive 값에 대해선 Preference DataStore를 통해 저장하는 방식을 채택했다.

     

    UDF 기반, 캐시 전략 도입 

    캐시를 적용한 이후 화면 데이터를 가져오기 위해 뷰모델은 로컬 스토리지 데이터를 조회해 UiState에 구성하며 UI는 업데이트된 UiState를 반영한다. 그와 동시에 서버 데이터를 조회해 실제 데이터로 값을 업데이트한다. 이를 위해 뷰모델은 서버 데이터를 담당하는 클래스에 데이터를 요청한다.

     

    이후 서버 데이터를 수신하면 그 값을 로컬 저장소에 적절한 값으로 변환해 제공하며 유저 이벤트에 따른 값 저장 처리 등의 기능을 수행했다. 뷰모델에 많은 책임이 주어졌고 그만큼 뷰모델은 비대해지고 복잡한 상태였다. 메인화면의 뷰모델로써 사용자 이벤트로 인한 데이터 변경, IoT 기기 이벤트 처리까지 수행하기 위해선 구조 변경을 통해 모듈별 책임을 분리하고 기능을 간소화시킬 필요가 있었다.

     

    구글 앱 아키텍처 레이어

     

    이 문제를 해결하기 위해 구글의 권장 앱 아키텍처를 프로젝트에 적용했다. 가장 중요한 원칙은 관심사 분리로 레이어 책임을 분리하는 것이다. 복잡도에 가장 큰 비중을 차지하던 데이터는 단일 소스 저장소로 할당하고 UDF 패턴으로 흐르도록 수정했다. 그 결과, 모든 데이터의 변경사항은 일원화되고 데이터 추적이 쉬워졌다.

     

     

     

    변경된 레이어 역할은 다음과 같다.

     

    • View: ViewModel의 UiState를 관찰한다. UiState가 업데이트되면 화면 컴포넌트를 리컴포지션 한다. 유저 이벤트가 발생한다면 뷰모델에 알리고 대기한다.

    • ViewModel: 화면 구성요소인 UiState를 Flow 기반의 데이터 홀더 클래스인 StateFlow로 랩핑하고 Repository로부터 데이터 변화가 감지되면 UiState를 업데이트한다. Repository에게 초기 화면 로딩 시 데이터 요청과 유저 이벤트를 전달한다.

    • Repository: Local DataSource로부터 전달받은 데이터를 ViewModel에 알리고 Remote DataSource에서 전달받은 데이터를 Local DataSource에 저장한다.

    • Local DataSource: 내부 저장소 데이터를 조회하거나 저장한다.

    • Remote DataSource: 서버 데이터를 조회하거나 전달받은 제어를 요청한다. 그 외 플랫폼에 접근하기 위한 초기화나 특정 기기 패널을 열기 위한 매니저 역할을 담당한다.

     

    처리하는 데이터는 모두 Flow 기반으로 처리되며 단방향이다. 캐시 도입으로 최초 화면 렌더링은 로컬 데이터가 표시되고 이후 서버 데이터로 업데이트되지만 화면을 업데이트하기 위해서 서버 데이터를 바라볼 필요가 없다. 바라보는 방향은 변하지 않는다.

     

    앱이 시작되고 메인 화면이 활성화될 때 ViewModel은 Repository에 서버 데이터 수집을 요청하고 Repository를 통해 관찰 가능한 Local DataSource의 HomeDashBoard를 관찰하고 있을 뿐이다. Remote DataSource으로부터 수신받은 최신 데이터는 Repository를 거쳐 Local DataSource로 흘러간다. 그 데이터는 다시 ViewModel과 View로 전이된다.

     

    구조변경을 통한 레이어 관심사 분리와 데이터 흐름 재정립 그리고 인터페이스 캡슐화로 복잡도를 크게 낮췄다.

    탄탄한 구조 기반으로 추후 기획이 변경되거나 기능이 추가되더라도 유연하게 대처할 수 있다는 자신감이 생겼다.

     

     

    캐시 적용 결과

     

    Cold Start 및 median 기준 20회 반복을 거친 결과, 화면이 완전히 그려지기까지 0.96초가 걸렸다.

    앱 프로세스 초기화 및 Choreographer 처리 이후 단, 0.0007초 시간으로 로컬 데이터를 가져오기 때문에 가능한 개선이었다.

    이는 기존 2.55초 대비 62.4% 개선된 결과이며, 렌더링 시간 대비 API 호출 대기 시간의 비중이 큰 만큼 유저 네트워크 환경이 불안정한 상황에선 더 큰 개선효과를 기대할 수 있다.

Designed by Tistory.