관리 메뉴

오늘도 더 나은 코드를 작성하였습니까?

안드로이드 개발에서 사용하는 캐싱 전략1 본문

카테고리 없음

안드로이드 개발에서 사용하는 캐싱 전략1

hik14 2026. 5. 19. 16:22

Cache란?

캐시(Cache)는 자주 사용되는 데이터를 빠르게 접근할 수 있는 임시 저장 공간에 보관하는 기술입니다. 원본 데이터 소스(디스크, 네트워크, DB 등)보다 접근 속도가 빠른 곳(메모리 등)에 데이터를 두어 반복적인 요청의 응답 시간을 단축시킵니다. Android에서는 LruCache(메모리 캐시), OkHttp Cache(네트워크 응답 캐시), Room 등을 활용해 성능과 사용자 경험을 개선합니다.

 

in-memory cache

In-memory cache는 앱 프로세스의 RAM에 데이터를 보관하는 방식으로, 접근 속도가 매우 빠르지만 앱 종료나 OS의 메모리 회수 시 사라집니다. 용도에 따라 적절한 도구를 선택하는 것이 핵심입니다.

 

1. 이미지 캐싱 (Coil / Glide)

- 이미지는 크기가 크고 디코딩 비용이 비싸기 때문에 전용 라이브러리를 쓰는 것이 표준입니다. 두 라이브러리 모두 메모리 캐시 + 디스크 캐시 2단계를 자동으로 관리합니다.

 

Coil

// 기본 사용 - 캐시 자동 처리
AsyncImage(model = url, contentDescription = null)

// 전역 설정
val imageLoader = ImageLoader.Builder(context)
    .memoryCache {
        MemoryCache.Builder(context)
            .maxSizePercent(0.25)  // 가용 메모리의 25%
            .build()
    }
    .build()

// 캐시 정책 개별 제어
imageView.load(url) {
    memoryCachePolicy(CachePolicy.ENABLED)
    diskCachePolicy(CachePolicy.ENABLED)
}

 

Glide

Glide.with(context)
    .load(url)
    .diskCacheStrategy(DiskCacheStrategy.ALL)
    .skipMemoryCache(false)
    .into(imageView)

 

2. Coroutine / Flow 기반 패턴

라이브러리 없이 직접 캐시 레이어를 만들 때 자주 쓰는 패턴. API 호출시에 사용한다. 

class UserRepository {
    private val _users = MutableStateFlow<Map<String, User>>(emptyMap())
    val users: StateFlow<Map<String, User>> = _users.asStateFlow()

    suspend fun getUser(id: String): User {
        return _users.value[id] ?: api.fetchUser(id).also { user ->
            _users.update { it + (id to user) }
        }
    }
}

 

3. Paging3 - cachedIn

cachedIn(scope)의 scope가 캐시의 생명주기를 결정한다. 일반적으로 viewModelScope 캐싱한다.

 

Configuration Change 시 처음부터 다시 로드

사용자가 100번째 아이템까지 스크롤한 상태에서 화면을 회전할때 cachedIn 없다면

  1. Activity가 재생성됨
  2. Fragment/Activity/Compose가 다시 collectLatest로 구독
  3. cold flow이므로 PagingSource가 처음부터 다시 실행
  4. 1페이지부터 다시 로드, 사용자는 맨 위로 돌아감 

cachedIn(viewModelScope)로 사용

  1. Activity가 재생성됨
  2. ViewModel은 살아있고, 캐시된 PagingData도 메모리에 있음
  3. 새 구독자는 이미 로드된 100개 데이터를 그대로 받음 
class ProductViewModel(repo: ProductRepository) : ViewModel() {
    val products: Flow<PagingData<Product>> = Pager(
        config = PagingConfig(pageSize = 20),
        pagingSourceFactory = { repo.getProductPagingSource() }
    ).flow
        .cachedIn(viewModelScope)  // ← 핵심
}

 

여러 구독자가 있으면 크래시

같은 Flow<PagingData>를 두 곳에서 구독하면 Paging3는 런타임 예외를 던집니다.

cachedIn은 내부적으로 SharedFlow 형태로 변환해서 여러 구독자가 같은 데이터를 공유할 수 있게 만들어줍니다.

// ⚠️ cachedIn 없이
viewModel.products.collectLatest { adapter1.submitData(it) }
viewModel.products.collectLatest { adapter2.submitData(it) }  // 💥 크래시
 

viewModelScope에 캐싱했기 때문에 ViewModel이 살아있는 동안 PagingData가 메모리에 유지됩니다. ViewModel이 onCleared()로 사라지면 캐시도 함께 정리되어 메모리 누수가 없습니다.

 

DataStore(Sharedpreferences) cache 

DataStore는 SharedPreferences의 후속으로, 작은 키-값 데이터를 비동기로 안전하게 저장하기 위한 라이브러리입니다.

캐시라고 부르긴 하지만 실제로는 영속 저장소이며, 그 특성을 이해해야 어떤 데이터를 둘지 결정할 수 있습니다.

특성 내용
저장 방식 파일 기반 (Preferences는 XML 유사, Proto는 바이너리)
접근 Flow 기반 비동기
트랜잭션 O (원자적 업데이트 보장)
크기 한계 전체 파일을 메모리에 로드 — 작아야 함
쿼리 불가 (전체 읽기만)
동시성 Coroutine 

 

사용자 환경설정

val Context.settingsDataStore by preferencesDataStore("settings")

object SettingsKeys {
    val DARK_MODE = booleanPreferencesKey("dark_mode")
    val LANGUAGE = stringPreferencesKey("language")
    val FONT_SCALE = floatPreferencesKey("font_scale")
    val NOTIFICATION_ENABLED = booleanPreferencesKey("notification_enabled")
}

 

  • 다크모드, 언어, 폰트 크기, 알림 on/off
  • 화면별 UI 옵션상태 (탭 선택, 정렬 순서, 필터 옵션)

인증/세션 메타데이터

object AuthKeys {
    val ACCESS_TOKEN = stringPreferencesKey("access_token")
    val REFRESH_TOKEN = stringPreferencesKey("refresh_token")
    val TOKEN_EXPIRES_AT = longPreferencesKey("token_expires_at")
    val USER_ID = stringPreferencesKey("user_id")
}

 

  • 토큰, 토큰 만료 시간, 로그인 상태
  • 토큰은 EncryptedSharedPreferences 또는 Android Keystore로 암호화 저장 권장.다.

 

앱 상태 플래그

  • 온보딩 완료 여부 (최초 앱실행)
  • 튜토리얼 표시 여부 
  • "이 메시지 다시 보지 않기" 플래그
  • 마지막 앱 버전 (마이그레이션 트리거용)
  • 첫 실행 여부