안드로이드 앱은 하나의 프로세스로 각각의 앱이 실행되는 것은 하나의 프로세스가 실행됨으로 표현할 수 있다. 즉, 하나의 앱이 실행되는 것은 OS 차원에서 하나의 프로세스가 생성되어 동작하는 것과 같다.

여기서 핵심적으로 보아야 할 점은 AOS(Android OS)가 이 프로세스들을 어떻게 관리하는지, 그리고 앱 내부의 메모리와 캐시가 어떤 메커니즘으로 동작하는지이다.
우선, OS는 우리가 보는 화면 단위가 아니라 PCB(Process Control Block)라는 구조체를 통해 프로세스를 식별하고 관리한다. 안드로이드 프로세스는 실행 시 시스템 자원을 효율적으로 쓰기 위해 공통 라이브러리 메모리를 공유하며 진행되는데, 이때 각 앱 프로세스에 할당된 메모리는 기기의 물리적 한계가 제약이 존재한다.
이렇게 제한된 메모리 환경에서 특정 앱이 과도하게 자원을 점유하는 것을 막기 위해, 안드로이드는 각 앱을 독립된 샌드박스(Sandbox)로 격리하여 운영한다. 또한, 프로세스 내부에서는 ART(Android Runtime)의 GC(Garbage Collector)가 동작하며 불필요한 객체를 정리하고, 메모리를 유동적으로 관리하며 시스템 전체의 안정성을 유지한다.
이러한 프로세스 단위의 격리와 관리 체계 위에서, 앱은 한정된 메모리 자원을 극대화하기 위해 운영체제의 설계 철학을 애플리케이션 레벨로 끌어올린다. 그 대표적인 사례이자, 실제 아키텍쳐를 구현하면서 느껴본 계층적 캐시 전략(Tiered Caching Strategy)이다.
흔히 운영체제는 물리 메모리(RAM)의 공간적 한계를 극복하기 위해 디스크의 일부를 가상 메모리 공간으로 활용하는 스왑(Swap) 메커니즘을 사용한다. 안드로이드 앱 역시 이와 본질적으로 동일한 논리 구조를 Memory 캐시와 Disk 캐시의 2-Tier 구조를 통해 재현하며, 이를 이미지 로더(Image Loader)의 동작 과정에서 명확히 확인할 수 있다.
1. Memory Cache: OS의 물리 RAM 역할
앱 프로세스 내부의 메모리 캐시(주로 LruCache 등)는 OS의 물리적 RAM과 대응된다. CPU(혹은 GPU)가 즉각적으로 참조할 수 있는 가장 빠른 영역이지만, 샌드박스에 할당된 힙 메모리 임계치 내에서만 생존할 수 있다는 제약이 있다. GC의 주요 관리 대상이기도 한 이 영역은 '현재 가장 자주 쓰이는 데이터'를 최우선으로 유지하며 빠른 사용자 경험을 보장한다.
2. Disk Cache: OS의 가상 메모리(Swap) 역할
메모리 캐시에서 밀려났거나, 앱이 종료되어도 유지되어야 하는 데이터는 디스크 캐시로 이동한다. 이는 OS 차원에서 RAM의 부족한 공간을 보완하기 위해 디스크를 가상 메모리(Swap) 영역으로 활용하는 것과 일맥상통한다. 속도는 메모리에 비해 현저히 느리지만, 프로세스의 생명주기와 무관하게 데이터를 영속적으로 보관할 수 있는 광활한 저장 공간을 제공한다.
3. 앱 레벨의 Swap-in: 이미지 로더의 데이터 로딩
이미지 로더가 특정 이미지를 요청받았을 때, 메모리 캐시에서 데이터를 찾지 못하는 'Cache Miss'가 발생하면 비로소 디스크 캐시를 탐색한다. 이때 디스크의 바이너리 데이터를 읽어와 비트맵으로 디코딩한 뒤 다시 메모리 캐시에 올리는 과정은, OS가 Page Fault 발생 시 디스크의 데이터를 RAM으로 로드하는 'Swap-in' 동작의 앱 레벨 구현체라고 볼 수 있다.

[구현하면서 느낀 트러블 슈팅 사례: DB가 아닌 Disk Cache를 선택한 이유]
실제 다계층 캐시를 구축하며 직면했던 고민 중 하나는 이미지 데이터의 영속성을 어떻게 보장할 것인가였다. 초기에 단순히 데이터의 유지(Persistence)만을 생각했을 때는 Room 라이브러리를 통한 DB 관리를 고려했다. 하지만 이미지와 같은 대용량 바이너리 데이터를 DB에 직접 적재하는 것은 입출력 오버헤드와 성능 저하를 야기할 수 있다는 점이 걸림돌이었다.
여기서 깊이 파고든 지점은 DiskLruCache의 Journal 메커니즘이었다. 디스크 캐시는 단순히 파일을 저장하는 것에 그치지 않고, journal 파일을 통해 각 캐시 데이터의 상태(읽기, 쓰기, 삭제 등)를 기록하며 관리한다. 이 구조 덕분에 앱이 완전히 종료된 콜드 스타트(Cold Start) 환경에서도 캐시의 일관성을 유지하며 데이터를 즉각적으로 복구할 수 있음을 이해하게 되었다.
결국, DB라는 무거운 레이어를 추가하는 대신, 파일 시스템 기반의 저널링 기법을 활용한 디스크 캐시를 채택함으로써 데이터 영속성과 입출력 성능이라는 두 마리 토끼를 잡을 수 있었다. 이 과정에서 "얕게 알고 있는 지식보다 도구들의 차이점을 명확히 알고 제대로 쓸 수 있는 것이 진짜 설계"라는 점을 다시 한번 깨달았다.
Android를 다룸에 있어서, 단순하게 UI/UX적인 측면에서 접근하려 하지 않고 실제 메모리 적재와 앱 성능의 향상에 대해 고민을 하면서 deepdive를 진행해보니 CS를 다시 공부했던게 많은 도움이 됨을 절실히 느끼고 있다. 솔직히 이제 더 재미있고 흥미가 느껴지는 거 같다.
이번에 다계층 구조화 캐시를 직접 구현해봤으니, 다음에는 코루틴을 실행하는 것이 보일러플레이트 코드를 어떻게 줄이는지에 대해서 더 공부해보고 이해해보려고 한다.
공부 또 공부
'짬짬히 기술 블로그✍️ > Android' 카테고리의 다른 글
| 코코..코루틴.. (1) | 2026.03.27 |
|---|---|
| hilt의 dependency injection 간단 정리 (0) | 2026.03.06 |
| Fragment랑 Compose 비교하면서 공부! (0) | 2026.03.05 |
| android의 선언형 UI compose란? (0) | 2026.03.04 |