Trinap 앱을 만들며 우리 팀이 생각한 도전들 중 하나는 라이브러리 만들어서 써보기였습니다.
그중 Kingfisher를 Queenfisher(ㅋㅋ)라는 이름으로 만들어서 사용했는데요, 이 Queenfisher를 개선한 경험을 남겨보도록 하겠습니다.
이전에 적용된 이미지 캐시 로직과 정책
- memoryCache와 diskCache 중 하나를 선택해서 사용을 할 수 있도록 해두었습니다.
- 캐싱 정책에 대해서 고민하지 않고 무제한으로 캐싱하도록 했습니다.
문제는?
- memoryCache와 diskCache 중 하나만 사용을 하면서 캐싱의 효율을 극대화할 수 없었습니다.
- memoryCache의 리밋이 없어서 imageData들을 모두 NSCache에 넣게 되었고, 이미지가 많이 로드되고 나서 지워지지 않고 계속 메모리에 남아있게 되었습니다.
개선기
첫 번째 접근
캐싱 로직 변경
가장 간단한 접근으로 MemoryCache와 DiskCache를 동시에 사용하는 로직을 구상했습니다.
이미지 요청이 오면
- 메모리 캐시를 확인하고 hit 될 경우 해당 이미지를 반환합니다.
- 메모리 캐시에서 hit 되지 않으면 디스크 캐시를 확인하고 hit 될 경우 해당 이미지를 반환합니다.
- 디스크 캐시에서도 hit 되지 않는다면 그때, 네트워크에서 이미지를 로드해 옵니다.
- 로드해온 이미지를 메모리 캐시와 디스크 캐시에 저장합니다.
하지만 위의 로직만 변경할 경우 모든 이미지가 메모리 캐시와 디스크 캐시에 들어가게 되고, 아래와 같은 문제가 생기게 되었습니다.
- 메모리 캐시에 계속해서 쌓이게 되어서 메모리 관리가 효율적으로 되지 않습니다.
- 한 번 로드된 데이터의 경우 메모리 캐시에 전부 쌓여있어서 디스크 캐시의 의미가 퇴색되었습니다.
캐싱 Configuration 도입
상황에 따라서 캐시 스토리지의 용량과 최대 저장 개수를 다르게 설정할 수 있도록 ConfigType이라는 enum으로 lower, normal, high라는 case에 따라서 MemoryCache의 설정값과 DiskCache의 설정값을 한 번에 설정할 수 있도록 하였습니다.
MemoryCache의 경우 캐시의 총 용량과 저장 개수를 설정해 주었고, DiskCache의 경우 총 저장 개수를 설정해 주었습니다.
이를 통해 Queenfisher를 외부에서 사용하는 setImage메소드에서 performance라는 파라미터로 캐시 스토리지의 설정을 바꿀 수 있도록 하였습니다.
위와 같은 설정으로 메모리 캐시의 limit 때문에 메모리를 더 효율적으로 관리할 수 있습니다.
하지만 디스크 캐시에 들어가게 된 정보가 같은 url 값이 이미지 자체만 변경된 경우 변경된 이미지를 가져오지 못하고, 기존에 캐시에 저장된 이미지만 들고 오게 되었습니다.
두 번째 접근
위의 개선 사항에서 같은 url인데도 불구하고 데이터가 변경되었을 경우를 신경 써야 했습니다.
그래서 ETag를 사용해서 변경 여부를 확인하는 접근을 가져갔습니다.
ETag(EntityTag)는 웹 캐시 유효성 검증에 사용되는데요, 해당 url의 데이터가 바뀌기 전까지는 ETag 또한 동일하고, 데이터가 바뀌는 경우 ETag도 변경됩니다.
그래서 우리는 ETag와 이미지 데이터를 함께 캐싱해 두고, ETag를 헤더에 담아서 보내서 캐싱되어있는 데이터가 유효한 데이터인지 확인할 수 있는 것이죠.
데이터 유효성 검증
유효성 검증을 할 수 있는 방법은 여러 가지인데요,
- If-Modified-Since
- 변경이 되었을 경우 200번으로 데이터가 내려오고, 변경이 되지 않았을 경우 304번(not modified)이 내려오게 됩니다.
- 날짜가 다르지만 같은 데이터로 수정해서 결과가 똑같아도 200번으로 데이터를 내려받는 경우가 있습니다.
- Last-Modified를 사용
- If-None-Match
- 변경이 되었을 경우 200번으로 데이터가 내려오고, 변경이 되지 않았을 경우 304번(not modified) 가 내려오게 됩니다.
- ETag를 사용
위와 같은 방법이 있습니다.
여기서 우리는 ETag를 사용한 If-None-Match 헤더를 사용해서 유효성을 검증하였습니다.
ETag 도입한 로직
위의 그림을 글로 풀어보면
- 메모리 캐시 hit -> 헤더에 ETag 심어서 조건부 요청해서 검증
- 디스크 캐시 hit -> 헤더에 ETag 심어서 조건부 요청해서 검증
- 캐시에 없으면 -> 헤더에 ETag 심지 않고 요청
위 로직으로 변경 후 캐싱된 url의 이미지가 변경이 되어도 변경된 이미지가 잘 나오게 되었습니다.
굳이?
조건부 요청으로 인해서 캐싱된 값이 유효한 경우 304를 내려받고 데이터를 다시 내려받지 않아서 네트워크 오버헤드를 줄일 수 있었는데요, 하지만 여기서 의문이 생기기 시작했습니다.
- 메모리에 캐싱되어 있는 데이터는 비교적 최근에 캐싱된 데이터들인데 가져올 때마다 유효성 검증을 위해 네트워크 요청을 보내야 할까?
- Kingfisher를 뜯어본 결과 Kingfisher는 ETag를 사용하지 않는다,,,!
(참고: https://github.com/onevcat/Kingfisher/issues/275) - 우리 서비스에서 이미지가 항상 최신이어야 할 정도의 민감도를 가지고 있는가?
- 304로 response를 받는다고 해도 조건부 요청 또한 네트워크 요청이고, 하나의 이미지를 사용할 때마다 요청을 하게 되면 그것 또한 비용이지 않을까?
세 번째 접근
벌써 세번째 접근인데요....
위의 의문들로 인해서 다른 접근을 하기로 했습니다.
먼저 메모리 캐시에 저장된 값들의 경우 비교적 최근의 값들이고, 앱이 꺼지게 되면 사라지게 되는 아이들이므로 앱이 꺼졌다가 켜질 때 최소한 1번 새로 업로드가 됩니다.
이러한 메모리 캐시에 저장된 데이터까지 조건부 요청을 통해 바뀐 값들을 새로 보여줄 필요가 없다고 생각했고, 메모리 캐시에서 hit 될 경우는 조건부 요청을 보내지 않고 캐싱된 데이터를 바로 전달해주도록 했습니다.
하지만 디스크 캐시의 경우 메모리 캐시보다 더 장기적으로 있게 되고, 따라서 변경 사항에 대해서 유효성 검증이 필요할 것이라고 생각했습니다.
그래서 디스크 캐시에서 hit 되는 경우만 유효성 검증을 해주도록 변경했습니다.
아쉬운 점
많은 성능 개선의 노력을 했으나 아쉬운 점 또한 있었습니다.
같은 URL의 요청이 동시에 여러 번 들어왔을 경우, 같은 URL의 데이터가 이미 캐싱이 되어 있는 경우 다시 요청이 들어오면 캐싱된 데이터를 사용할 수 있으나, 캐싱되어있지 않은 URL로 동시에 여러번 호출이 불릴 경우 한 번의 요청으로 데이터를 공유하도록 되어 있지 않습니다.
이를 개선하기 위해 이미지 다운로드를 하는 클래스를 따로 만들었지만,
해당 클래스에 Dictionary를 두고 같은 URL을 묶어서 한 번의 요청만 보내도록 개선하려 하였으나 시간적인 제약 때문에 구현하지 못했습니다.
결과
ETag의 부분적인 도입과 Configuration, 로직 변경을 통해 성능 개선을 경험할 수 있었습니다.
ETag를 도입함으로써 10Mb의 데이터를 받아온다고 가정했을 때 304(Not Modified)를 내려받음으로써 157Byte로 유효성 검증을 할 수 있었고, 이를 통해 약 7,000배 정도의 네트워크 비용을 절감할 수 있었습니다.
또한 저희 서비스에 알맞게 설정을 함으로써 서비스의 상황에 알맞은 로직을 구성할 수 있었습니다.
'🍎 iOS > 📱 iOS' 카테고리의 다른 글
BDD로 테스트하기(feat. Quick) (2) | 2023.10.29 |
---|---|
Custom Network Module에서 토큰 관리하기(RequestInterceptor) (0) | 2023.02.01 |
MVVM-C + Clean Architecture 도입기 (0) | 2022.12.14 |