# STEP3 @July911 줄라이 안녕하세요! 코낄이, 리지입니다. 박스오피스 II STEP3 PR입니다. CoreData를 처음 적용하여 오래걸렸습니다 🥲 이번 프로젝트도 잘 부탁드립니다. 🙇‍♂️ # ✅ 이번 스텝에서 구현한 기능 - 네트워크로 수신하는 데이터에 로컬 캐시 구현 # ✅ 고민했던 점 ## 1️⃣ 캐싱 방법 프로젝트를 시작하기 전 어떤 방식을 채택하면 좋을지 크게 CoreData와 URLCache, NSCache에 대해 고민하였습니다. 먼저 각각의 특징을 살펴보았습니다. - **CoreData** - 많은 양의 정보를 저장하고 각각의 정보가 객체 형태로 저장하고 관리하며 관계를 설정할 수 있음 - On-disk 방식으로 저장 - **URLCache** - NSURLRequest -> CachedURLRequest 객체에 매핑하여 URL로드 요청에 대한 응답을 캐싱 - In-memory, On-disk 방식 중 선택하여 저장할 수 있음 - On-disk로 저장하면 애플리케이션이 종료되도 사라지지 않음 - **NSCache** - In-memory 방식으로 저장 - 애플리케이션이 종료되면 메모리에서 해제되어 사라짐 이를 토대로 영화리스트와 상세정보에 대한 데이터는 변하지 않는 데이터라고 생각하여 On-disk 방식으로 저장하여 앱이 종료되어도 사라지지 않도록 구현하고자 했습니다. 따라서 선택지를 CoreData와 URLCache로 좁혔고, 그 안에서 **CoreData**를 선택하였습니다. 그 이유는 크게 3가지가 있습니다. 1. URLCache의 경우 꺼내오는 데이터 타입이 URLRequest 타입으로 꺼내올 수 있기 때문에 원하는 타입으로 한번 더 변환해주어야 하는 과정이 필요한데, 이 과정이 크진 않지만 불필요하지 않을까? 생각하였습니다. 2. CoreData의 특징 중 하나가 데이터들의 관계를 설정할 수 있다는 것인데, 처음 생각했을때 영화리스트 데이터와 상세정보 데이터간의 관계를 설정하여 관리할 수 있지 않을까? 생각하였습니다. 3. URLCache는 저희가 사용해본적이 있는데 CoreData는 한번도 적용해본적이 없어 직접 구현해보고 싶었습니다! 상세정보 화면에서 띄우는 포스터이미지의 경우, 검색한 첫 번째 이미지를 불러오기 때문에 검색하는 시점에 따라 계속해서 포스터 이미지가 변경되었습니다. 따라서 In-memory 방식으로 저장하여 앱이 종료되면 삭제되도록 **NSCache**를 사용하여 구현하였습니다. ## 2️⃣ iOS File System 위치 - **CoreData** 파일 위치를 찍어보니 `Library/ApplicationSupport`에 저장되는 것을 확인할 수 있었습니다. `Library/ApplicationSupport`에는 주로 앱이 실행되는데 사용되지만 사용자에게 숨겨야 하는 파일을 저장하는 것으로 알고있습니다. `Library` 하위 폴더에는 `ApplicationSupport` 말고`Caches`도 존재하는데`Library/Caches`에는 일시적인 데이터보다는 오래 유지되어야 하지만, 지원하는 파일만큼 유지될 필요 없는 캐시 데이터가 저장됩니다. 따라서저희는 캐싱한 데이터를 저장하는 것이 목적이기 때문에 `Library/Caches`에 저장하도록 fileManager를 활용하여 파일경로를 변경하였습니다. ```swift class AppDelegate: UIResponder, UIApplicationDelegate { lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "BoxOfficeCoreData") let fileManager = FileManager.default let cacheDirectoryURL = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0] let persistentStoreURL = cacheDirectoryURL.appendingPathComponent("BoxOfficeCoreData.sqlite") let description = container.persistentStoreDescriptions.first description?.url = persistentStoreURL container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error { fatalError("Failed to load store: \(error)") } }) return container }() ... } ``` ## 3️⃣ 캐시 매니저 추상화 구현한 캐시 매니저가 공통으로 채택하도록 추상화된 `DataManager` 프로토콜을 구현하였습니다. 프로토콜에는 CRUD가 명세되어 있습니다. ``` swift protocol DataManager { func create(key: String, value: [Any]) func read(key: String) -> Any? func update(key: String, value: [Any]) func delete() } final class MovieInformationCoreDataManager: DataManager { ... } final class DailyBoxOfficeCoreDataManager: DataManager { ... } final class ImageCacheManager: DataManager { ... } ``` ## 4️⃣ 프로젝트의 모델 구조 CoreData 캐시를 구현하기 위해 CoreData에 다음과 같이 Entity를 추가하였습니다. <br> <img src="https://i.imgur.com/67YT6Pi.png" width=500> <img src="https://i.imgur.com/uBfBznt.png" width=500> Entity가 추가되어 모델을 추가 구현하였는데, 그 결과 프로젝트 내 모델이 많아졌고, 모델 간 관계를 파악하기가 복잡해진 것 같습니다. 프로젝트 내 모델 구조는 다음과 같습니다. ### 데일리 박스 오피스 조회 화면에 사용되는 모델 <img src="https://i.imgur.com/NGMB24h.png" width=600> - `DailyBoxOffice` : JSON 파싱을 위한 모델 - `DailyBoxOfficeData` : Core Data 캐시를 위한 모델 - `DailyBoxOfficeItem` : VC에서 컬렉션뷰의 DataSource에 사용하기 위한 Hashable 모델 ### 영화 상세 정보 화면에 사용되는 모델 <img src="https://i.imgur.com/UiOFR9l.png" width=600> - `MovieInformation` : JSON 파싱을 위한 모델 - `MovieInformationData` : Core Data 캐시를 위한 모델 - `MovieInformationItem` : VC에서 UI요소들에 데이터를 적용하기 위한 모델 `MovieInformationData` 모델의 프로퍼티는 `MovieInformation`의 프로퍼티와 다른 형태로 구현하였습니다. **MovieInformation** ``` swift struct MovieInformation: Decodable { struct MovieInformationResult: Decodable { let movie: Movie ... struct Movie: Decodable { ... let nations: [Nation] ... struct Nation: Decodable { let name: String ... } } } } ``` **MovieInformationData** ``` swift public final class MovieInformationData: NSManagedObject { ... @NSManaged var details: Details? } final class Details: NSObject { ... var nationsName: [String]? ... } ``` JSON 원본에서는 `Nation`이라는 중첩 타입으로 구현되어있던 프로퍼티를 `MovieInformationData` 내에서는 `String`으로 풀어서 저장하였습니다. 사용자 정의 타입을 CoreData에 캐시하기 위해선 타입이 `NSSecureCoding`을 준수하고 해당 타입을 위한 `NSSecureUnarchiveFromDataTransformer` 모델을 추가적으로 구현해야 하는데, 모든 중첩 타입에 대해 위 요구사항을 구현하는 것이 번거롭게 느껴졌기 때문입니다. 하지만 원본 타입의 형태를 변경했다는 점에서 이러한 구현이 괜찮은지 의문이 들기도 합니다. :thinking: ## 5️⃣ 캐시정책 저희는 특정 시간동안 저장되고 사라지도록 제거정책을 설정하였습니다. 그 시간은 앱을 실행시킨 시점을 기준으로 24시간동안으로 지정하였고 24시간이 지나면 캐시된 데이터가 삭제되도록 구현하였습니다. - CoreData, Entity에 `createdAt` Attribute 추가 - NSPredicate로 원하는 기간 설정 ```swift func deleteByTimeInterval() { guard let context = self.context else { return } let request: NSFetchRequest<NSFetchRequestResult> = MovieInformationData.fetchRequest() let olderThanDate = Date().addingTimeInterval(-1 * 24 * 60 * 60) request.predicate = NSPredicate(format: "createdAt < %@", argumentArray: [olderThanDate]) let delete = NSBatchDeleteRequest(fetchRequest: request) do { try context.execute(delete) } catch { print(error.localizedDescription) } } ``` - AppDelegate에서 호출 ```swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { ... DailyBoxOfficeCoreDataManager.shared.deleteByTimeInterval() MovieInformationCoreDataManager.shared.deleteByTimeInterval() ... } ``` # ✅ 조언을 구하고 싶은 점 프로젝트에 들어가기 전, 저희가 나름 고민했던 이유로 CoreData를 선택하고 적용하였습니다. 새로 접하는 내용이라 생소하기도 하고 기능을 어떻게 효율적으로 사용하면 좋을지 고민하는 것도 너무 어려웠습니다 😅 막상 3가지 이유로 CoreData를 선택하였지만 적용방법이 어려웠고, JSON 파싱한 데이터 중 배열타입으로 구현된 애들을 하나씩 세팅해주는 과정이 말도안되게 번거롭고 복잡하다고 느꼈습니다. 또 저희가 가장 큰 특징이라고 생각한 Relation에 대해서는 적용해보지 못하여..더더욱 CoreData의 사용 이유에 대해 고민이 되었습니다. 처음 적용하는 개념이다보니 제대로 한건지 걱정이 많았습니다. 우선 앱이 돌아가도록 구현하는데 의의를 두었습니다...! 저희가 잡고간 방향이 올바른 방향일까요?