TIL
🌱 난 오늘 무엇을 공부했을까?
📌 프로젝트 매니저 RemoteDB, Local DB구현
📍 RemoteDB
🔗 FirebaseManager
- Firebase DB에 접근해서 init 시에 Model을 정해 encoding, decoing 하는 방식으로 데이터를 CRUD 할 수 있도록 구성해봤다.
import Firebase
import FirebaseDatabase
class FirebaseManager<T: Codable> {
typealias Entity = T
private let reference: DatabaseReference
private let rootChildID: String
init(rootChildID: String) {
self.rootChildID = rootChildID
reference = Database.database().reference()
}
func setValue(childId: String,
model: Entity) throws {
guard let encodedData = try? JSONEncoder().encode(model)
else { throw FirebaseError.encoding }
guard let jsonData = try? JSONSerialization.jsonObject(with: encodedData)
else { throw FirebaseError.JSONSerialization}
reference.child(rootChildID).child(childId).setValue(jsonData)
}
func readAllValue(completionHandler: @escaping((Result<[Entity], FirebaseError>) -> Void)) {
reference.child(rootChildID).observeSingleEvent(of: .value, with: { [weak self] snapshot in
guard let self = self
else { return }
let dictionary = self.convertDataSnapshot(snapshot)
let jsondata = self.convertJSONSerialization(with: dictionary)
let model = self.decode(with: jsondata)
completionHandler(model)
})
}
func deleteValue(childId: String) {
reference.child(rootChildID).child(childId).removeValue()
}
private func decode(with data: Result<Data, FirebaseError>) -> Result<[Entity], FirebaseError> {
switch data {
case .success(let jsonData):
guard let decodedData = try? JSONDecoder().decode([Entity].self, from: jsonData)
else { return .failure(.decoding) }
return .success(decodedData)
case.failure(let error):
return .failure(error)
}
}
private func convertDataSnapshot(_ snapshot: DataSnapshot) -> Result<[String: Any], FirebaseError> {
guard let dictionary = snapshot.value as? [String: Any]
else { return .failure(.dataSnapshot) }
return .success(dictionary)
}
private func convertJSONSerialization(with dictionary: Result<[String: Any], FirebaseError>)
-> Result<Data, FirebaseError> {
switch dictionary {
case .success(let data):
let values = data.map {
$0.value
}
guard let jsonData = try? JSONSerialization.data(withJSONObject: values)
else { return .failure(.JSONSerialization) }
return .success(jsonData)
case .failure(let error):
return .failure(error)
}
}
}
enum FirebaseError: Error {
case encoding
case decoding
case JSONSerialization
case dataSnapshot
}
🔗 FirebaseRepository
- FirebaseManager 객체를 사용해서 RemoteDB에 접근
- 기존에 임시 DB에서 채택한 ProjectRepository 프로토콜을 채택하는 방식으로 useCase를 수정하지 않고 DB를 변경 할 수 있도록 내부에서 처리
import Foundation
class FirebaseRepository: ProjectRepository {
private var firebase: FirebaseManager<ProjectModel>
init() {
firebase = FirebaseManager<ProjectModel>(rootChildID: "ProjectModel")
}
func create(data: ProjectModel) {
do {
try firebase.setValue(childId: data.id, model: data)
} catch {
print(error)
}
}
func read(completionHandler: @escaping ([ProjectModel]) -> Void) {
firebase.readAllValue { database in
switch database {
case .success(let data):
completionHandler(data)
return
case .failure(let error):
print(error)
completionHandler([])
}
}
}
func update(id: String, data: ProjectModel) {
do {
try firebase.setValue(childId: id, model: data)
} catch {
print(error)
}
}
func delete(id: String) {
firebase.deleteValue(childId: id)
}
}
📍 LocalDB
🔗 CoreDataManager
- CoreData의 모델과 entity를 init시에 설정해주는 방식으로 CRUD가 가능한 Manager 구현
import CoreData
class CoreDataManager<T> {
typealias Entity = T
private let modelName: String
private let entityName: String
init(modelName: String,
entityName: String) {
self.modelName = modelName
self.entityName = entityName
}
private lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: modelName)
container.loadPersistentStores(completionHandler: { (_, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
private lazy var context: NSManagedObjectContext = persistentContainer.viewContext
func createObject(entityKeyValue: [String: Any]) {
guard let entity = NSEntityDescription.entity(forEntityName: entityName,
in: context)
else { return }
let managerObject = NSManagedObject(entity: entity,
insertInto: context)
entityKeyValue.forEach {
managerObject.setValue($0.value,
forKey: $0.key)
}
saveContext()
}
func readObject<Entity>(request: NSFetchRequest<Entity>) -> Result<[Entity], CoreDataError> {
guard let fetchObject = try? context.fetch(request) as [Entity]
else { return .failure(.fetch) }
return .success(fetchObject)
}
func updateObject(object: NSManagedObject,
entityKeyValue: [String: Any]) {
entityKeyValue.forEach { object.setValue($0.value,
forKey: $0.key) }
saveContext()
}
func deleteObject(object: NSManagedObject) {
context.delete(object)
saveContext()
}
private func saveContext() {
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
enum CoreDataError: Error {
case fetch
}
🔗 CoreDataRepository
- RemoteDB와 마찬가지로 ProjectRepository를 채택해서 useCase에서 DB변경에 용이
import Foundation
import CoreData
class CoreDataRepository: ProjectRepository {
private var coreDataManager: CoreDataManager<ProjectModel>
init() {
self.coreDataManager = CoreDataManager<ProjectModel>(modelName: "ProjectCoreDataModel",
entityName: "Project")
}
func create(data: ProjectModel) {
let dictionnary = [
"id": data.id,
"title": data.title,
"body": data.body,
"date": data.date,
"workState": data.workState.rawValue
] as [String: Any]
coreDataManager.createObject(entityKeyValue: dictionnary)
}
func read(completionHandler: @escaping ([ProjectModel]) -> Void) {
switch coreDataManager.readObject(request: Project.fetchRequest()) {
case .success(let fetchList):
let list = fetchList.map {
ProjectModel(id: $0.id ?? "",
title: $0.title ?? "",
body: $0.body ?? "",
date: $0.date ?? Date(),
workState: ProjectState(rawValue: $0.workState ?? "todo") ?? .todo)
}
completionHandler(list)
case .failure(let error):
print(error)
completionHandler([])
}
}
func update(id: String, data: ProjectModel) {
switch coreDataManager.readObject(request: Project.fetchRequest()) {
case .success(let fetchList):
guard let filteredList = fetchList.filter({ $0.id == id }).first
else { return }
let dictionnary = [
"id": data.id,
"title": data.title,
"body": data.body,
"date": data.date,
"workState": data.workState.rawValue
] as [String: Any]
coreDataManager.updateObject(object: filteredList, entityKeyValue: dictionnary)
case .failure(let error):
print(error)
}
}
func delete(id: String) {
switch coreDataManager.readObject(request: Project.fetchRequest()) {
case .success(let fetchList):
guard let object = fetchList.filter({ $0.id == id }).first
else { return }
coreDataManager.deleteObject(object: object)
case .failure(let error):
print(error)
}
}
}
📍 의존성 주입
- 각각의 역할을 분리하고 프로토콜로 묶어주게 되면서 DB의 변경이 자유로웠다.
- firebase, coredata가 아닌 다른 DB도 만들어서 넣어보면 재밌을 것 같다.
useCase = UseCase(repository: TemporaryRepository(projectModels: [ProjectModel]()))
useCase = UseCase(repository: CoreDataRepository())
useCase = UseCase(repository: FirebaseRepository())
📍 느낀점
- Clean Architecture를 적용해 레이어들의 역할을 나눠줘서 수정이 굉장히 간단했다.
- 아쉬운 점은 처음에 ProjectRepository를 구현할 때 왜 CompletionHandler 방식을 생각하지 못 했을까 라는 생각이 들었다.
- 임시 라이브러리는 단순하게 배열을 사용해서 데이터를 가지고 있으니, 동기처리가 가능하지만, 네트워크를 사용해야 한다는 걸 알았는데 비동기로 처리하지 않고 동기로 처리해서 CompletionHandler 활용에 대한 코드 수정이 많이 있었다.
- 이래서 Clean Architecture를 쓰는구나 라고 생각하게 되서 굉장히 기뻤다.