Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- 애니메이션
- 리펙토링
- RxCocoa
- Human interface guide
- swiftUI
- HIG
- map
- UICollectionView
- combine
- SWIFT
- tableView
- Observable
- 리펙터링
- Protocol
- clean architecture
- UITextView
- Clean Code
- rxswift
- MVVM
- swift documentation
- Refactoring
- 클린 코드
- ribs
- Xcode
- 리팩토링
- uitableview
- collectionview
- 스위프트
- ios
- uiscrollview
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[Clean Architecture] 8. 코드로 알아보는 SOLID - 캐싱 Disk Cache (UserDefeaults, CoreData) 본문
Clean Architecture/Clean Architecture 코드
[Clean Architecture] 8. 코드로 알아보는 SOLID - 캐싱 Disk Cache (UserDefeaults, CoreData)
jake-kim 2021. 9. 26. 23:430. 코드로 알아보는 SOLID - 클래스 다이어그램 필수 표현
1. 코드로 알아보는 SOLID - SRP(Single Responsibility Principle) 단일 책임 원칙
2. 코드로 알아보는 SOLID - OCP(Open Close Principle) 개방 폐쇄 원칙
3. 코드로 알아보는 SOLID - LSP(Liskov Substitution Principle) 리스코프 치환 원칙
4. 코드로 알아보는 SOLID - ISP(Interface Segregation Principle) 인터페이스 분리 원칙
5. 코드로 알아보는 SOLID - DIP(Dependency Inversion Principle, testable) 의존성 역전 원칙
6. 코드로 알아보는 SOLID - Coordinator 패턴 화면전환
7. 코드로 알아보는 SOLID - Network, REST (URLSession, URLRequest, URLSessionDataTask)
8. 코드로 알아보는 SOLID - 캐싱 Disk Cache (UserDefeaults, CoreData)
Cache
- memory cache는 따로 개념 참고
- Network 호출을 통해서 얻은 데이터를 disk cache하여 로컬에 저장
- Network호출 전에 디스크 캐싱을 통해 로컬에서 데이터를 불러오고, 그 다음 network호출한 결과가 오면 dataSource에 append하는 방식
- UserDefaults, Core Data 모두 사용 가능
예제 앱
- disk cache와 network 호출을 사용하여 tableView에 데이터를 뿌리는 앱
설계
- useCase의 repository모듈에서 fetchCities(request) 호출하면, repository 구현체에서 disk cache, 네트워크 하는 코드 존재
- Repository는 Interface이므로 내부에서의 데이터 저장소는 쉽게 변경 가능 (DIP)
- 내부 저장소를 바꾸어도 사용하는 쪽인 UseCase에 영향을 주지 않는 관점
주요 코드
- 도메인에서 사용하는 모델인 City와 UserDefaults 캐시 저장소에서 사용하는 모델을 구분하기 위해서 UDS(UserDefaultsStorage)관련 모델 정의
// UDS: UserDefaultsStorage
struct CityListUDS: Codable {
var list: [CityUDS]
}
struct CityUDS: Codable {
let name: String
let subName: String
init(city: City) {
name = city.name
subName = city.subName
}
}
extension CityUDS {
func toDomain() -> City {
return .init(id: name, name: name, subName: subName)
}
}
- UserDefaults 저장소 구현: CityResponseStorage 인터페이스를 준수하여 UseCase에서 사용할 수 있도록 구현
final class UserDefaultsCityStorage {
private let maxStorageLimit: Int
private let cityStorageKey = "cityStorage"
private var userDefaults: UserDefaults
init(maxStorageLimit: Int, userDefaults: UserDefaults = UserDefaults.standard) {
self.maxStorageLimit = maxStorageLimit
self.userDefaults = userDefaults
}
private func fetchCities() -> [City] {
if let citiesData = userDefaults.object(forKey: cityStorageKey) as? Data {
if let cities = try? JSONDecoder().decode(CityListUDS.self, from: citiesData) {
return cities.list.map { $0.toDomain() }
}
}
return []
}
private func persist(cities: [City]) {
let encoder = JSONEncoder()
let cityListUDS = cities.map(CityUDS.init)
if let encoded = try? encoder.encode(CityListUDS(list: cityListUDS)) {
userDefaults.set(encoded, forKey: cityStorageKey)
}
}
}
extension UserDefaultsCityStorage: CityResponseStorage {
func getResponse(maxCount: Int, completion: @escaping (Result<[City], Error>) -> Void) {
DispatchQueue.main.async { [weak self] in
guard var cities = self?.fetchCities() else { return }
let maxStorageLimit = self?.maxStorageLimit ?? 0
cities = cities.count < maxStorageLimit ? cities : Array(cities[0..<maxCount])
completion(.success(cities))
}
}
func save(cities: [City]) {
persist(cities: cities)
}
}
- CityRepository에서 UserDefaults 캐시 저장소를 사용하기위해 CitySceneDIContainer에서 주입
// CitySceneDIContainer
private let cityResponseCache = UserDefaultsCityStorage(maxStorageLimit: 10)
private func makeCityRepository() -> CityRepository {
return CityRepositoryImpl(dataTransferService: dependencies.cityDataTransferService, cache: cityResponseCache)
}
- 캐시 사용 시 useCase에 cached파라미터 추가: ViewModel에서 dataSource에 새로 얻은 데이터를 append하는 메소드를 주입
- 보통 네트워크 response 받을 경우 completion의 success를 보고 직접 호출하지만, 캐시를 통해 append가 호출되게끔 하는 일은 repository에 위임하여 각자 기능 분리에 적합하여 이처럼 구현
// CityViewModel.swift
func viewDidLoad() {
let _ = cityUseCase.execute(requestValue: .init(order: .revenue, sortOrder: .desc),
cached: appendCities(_:)) { [weak self] result in
switch result {
case .success(let cities):
self?.appendCities(cities)
case .failure(let error):
self?.handle(error: error)
}
}
}
// Private
private func appendCities(_ cities: [City]) {
let newCities = cities.filter { !self.items.value.contains($0) }
items.accept(newCities)
}
- CityUseCase: ViewModel이 의존하고 있는 모듈이고, 이곳에서 repository 객체를 통해 데이터 획득
protocol CityUseCase {
@discardableResult
func execute(requestValue: CityUseCaseRequestValue,
cached: @escaping (([City]) -> Void),
completion: @escaping (Result<[City], Error>) -> Void) -> Cancellable?
}
final class CityUseCaseImpl: CityUseCase {
/// DIP: CityRepository에서 network가 바뀌든, CityRepository는 protocol이므로 json이 xml로 되든간에 영향을 받지 않는 코드
private let cityRepository: CityRepository
init(cityRepository: CityRepository) {
self.cityRepository = cityRepository
}
func execute(requestValue: CityUseCaseRequestValue,
cached: @escaping (([City]) -> Void),
completion: @escaping (Result<[City], Error>) -> Void) -> Cancellable? {
return cityRepository.fetchCities(order: requestValue.order,
sortOrder: requestValue.sortOrder,
cached: cached) { result in
if case .success = result {
// TODO: cache 저장
}
switch result {
case .success(let response):
return completion(.success(response))
case .failure(let error):
return completion(.failure(error))
}
}
}
}
struct CityUseCaseRequestValue {
let order: Order
let sortOrder: SortOrder
}
- Repository의 구현체에서 cache를 시도하고 network호출까지 진행
final class CityRepositoryImpl {
private let dataTransferService: DataTransferService
private let cache: CityResponseStorage
init(dataTransferService: DataTransferService, cache: CityResponseStorage) {
self.dataTransferService = dataTransferService
self.cache = cache
}
}
extension CityRepositoryImpl: CityRepository {
func fetchCities(order: Order,
sortOrder: SortOrder,
cached: @escaping (([City]) -> Void),
completion: @escaping (Result<[City], Error>) -> Void) -> Cancellable? {
let endpoint = APIEndpoints.getCities(with: CityRequestDTO(order: order, sortOrder: sortOrder))
let task = RepositoryTask()
cache.getResponse(maxCount: 10) { result in
if case let .success(response) = result {
cached(response)
}
/// ViewModel에서 네트워크 호출 cancel요청을 받은 경우 (ex - 사용자가 검색 중 취소 버튼 탭)
guard !task.isCancelled else { return}
task.networkTask = self.dataTransferService.request(with: endpoint, completion: { result in
switch result {
case .success(let response):
self.cache.save(cities: response.toDomain())
completion(.success(response.toDomain()))
case .failure(let error):
completion(.failure(error))
}
})
}
return task
}
}
* 전체 소스 코드: https://github.com/JK0369/CleanArchitectureCache
'Clean Architecture > Clean Architecture 코드' 카테고리의 다른 글
Comments