# 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의 사용 이유에 대해 고민이 되었습니다.
처음 적용하는 개념이다보니 제대로 한건지 걱정이 많았습니다. 우선 앱이 돌아가도록 구현하는데 의의를 두었습니다...! 저희가 잡고간 방향이 올바른 방향일까요?