프로젝트 매니저 RemoteDB, Local DB구현

Groot·2022년 10월 10일
0

TIL

목록 보기
76/153
post-thumbnail
post-custom-banner

TIL

🌱 난 오늘 무엇을 공부했을까?

📌 프로젝트 매니저 RemoteDB, Local DB구현

📍 RemoteDB

🔗 FirebaseManager

  • Firebase DB에 접근해서 init 시에 Model을 정해 encoding, decoing 하는 방식으로 데이터를 CRUD 할 수 있도록 구성해봤다.

//
//  FirebaseRepository.swift
//  ProjectManager
//
//  Created by Groot on 2022/09/27.
//

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를 변경 할 수 있도록 내부에서 처리

//
//  FirebaseRepository.swift
//  ProjectManager
//
//  Created by Groot on 2022/09/27.
//

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 구현

//
//  CoreDataManager.swift
//  ProjectManager
//
//  Created by Groot on 2022/09/28.
//

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변경에 용이
//
//  CoreDataRepository.swift
//  ProjectManager
//
//  Created by Groot on 2022/09/28.
//

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를 쓰는구나 라고 생각하게 되서 굉장히 기뻤다.
profile
I Am Groot
post-custom-banner

0개의 댓글