
Data? _cachedData;
Future<Data> get data async {
// Step 1: Check whether your cache already contains the desired data
if (_cachedData == null) {
// Step 2: Load the data if the cache was empty
_cachedData = await _readData();
}
// Step 3: Return the value in the cache
return _cachedData!;
}
caching하는 방법을 pseudocode로 작성하였다.
여러 방법이 있는데, 미리 데이터를 캐시에 저장하여 웜업하는 방법 등이 있다.
Cache hit
원하는 정보가 이미 저장되어 있고, 데이터 검증이 필요 없는 경우 발생
Cache miss
원하는 정보가 없고, 데이터를 호출해야 하는 경우 발생
실제 데이터가 갱신되면 stale cache 상태가 된다.
이 경우, 새롭게 데이터를 캐시에 저장해야 한다.
모든 캐싱 전략은 stale data를 다루는 부분에서 리스크를 가진다.
너무 빈번하게 캐시 데이터를 검증하면 데이터 호출에 오랜 시간이 걸린다.
즉, 대부분의 앱은 별도의 검증 없이 캐시의 데이터가 유효하다고 믿고 진행시킨다.
대부분 캐시 데이터의 유효 시간은 정해져 있다.
해당 시간이 지나면 cache miss가 발생하고, 새로운 데이터를 호출한다.
class UserRepository {
UserRepository(this.api);
final Api api;
final Map<int, User?> _userCache = {}; // in-memory cache
Future<User?> loadUser(int id) async {
if (!_userCache.containsKey(id)) {
final response = await api.get(id);
if (response.statusCode == 200) {
_userCache[id] = User.fromJson(response.body);
} else {
_userCache[id] = null;
}
}
return _userCache[id];
}
}
가장 간단하고 성능이 뛰어난 방법은 in-memory 캐싱 방법이다.
이 방법은 캐시가 시스템 메모리에 저장되기 때문에 세션이 종료되면 캐시도 사라진다.
물론 부수 효과로 자동으로 stale cache가 사라진다는 점도 있다.
위와 같은 repository pattern은 3가지 장점을 가진다.
하지만 in-memory 캐싱은 앱을 다시 설치하면 데이터를 다시 로드해야 하기 때문에 UX적으로 좋지 않다.
캐시를 유지하려면 데이터를 디바이스의 하드 드라이브에 저장해야 한다.
shared_preferences는 key-value storage로 구성된 플러터 플러그인이다.
작은 사이즈의 데이터 저장에 유용하며 대부분의 캐싱 전략에도 부합한다.
만약 low-throughput 시나리오를 넘어서는 경우, shared_preference를 사용하지 말자.
디바이스의 파일 시스템에 저장하는 방법을 고려해보자.
참고로 low-throughput 시나리오는 데이터의 읽기/쓰기 빈도가 낮고, 소량의 데이터를 저장하는 경우를 말한다.
최종 방법은 적절한 DB에 데이터를 읽고 쓰는 것이다.
관계형/비관계형 DB가 존재하고, 모두 데이터가 큰 경우에 엄청난 성능 향상을 보여준다.
이미지를 캐싱하고 싶으면 아래의 패키지를 사용해보자.
네비게이션 스택, 스크롤 위치, 입력 중이던 폼 데이터와 같이 유저 관련 데이터를 저장하고 싶을 때가 있다.
이러한 패턴을 state restoration이라 부르고, 플러터에서 기본적으로 지원한다.
플러터는 element tree에서 데이터를 가져와 플러터 엔진과 동기화한다.
그 후, 해당 데이터를 플랫폼별 저장소에 캐싱하여 다음 세션에서도 유지할 수 있도록 한다.