기어가더라도 제대로

[iOS-CoreData]Repository Pattern with CoreData 본문

Swift - 데이터베이스

[iOS-CoreData]Repository Pattern with CoreData

Damagucci-juice 2023. 2. 1. 14:44

목차

0. 선수 지식
1. Repository는 무엇인가? 
2. 왜 이런 패턴을 사용하는가?
3. Repository Pattern의 구성요소 
    3.1. Data-mapping Layer
    3.2. Domain Layer
    3.3. Collection-Like Interface
4. CoreData를 레포지토리 패턴으로 도입하기
    4.1. Generic한 레포지토리 만들기
    4.2. Domain Model과 Data Model을 분리하기
5. 글 맺음

0. 선수 지식

1. Repository는 무엇인가?

  • 요즘 앱은 거의 데이터를 사용함(API Service, CoreData, Realm, UserDefaults…)
    • 클린 아키텍처에서 말하는 Domain, Data 계층의 데이터를 말함
  • 쉽게 생각하면 Domain이 필요로 하는 Model을 가져다주는 역할을 하는 객체가 Repository 임

2. 왜 이런 패턴을 사용하는가?

  • 도메인 계층과 데이터 계층 사이에는 ‘추상 계층’을 두는 것이 좋다.
    • 예외적으로 CoreData에서 사용하는 Entity Model을 바로 Present 단에서 사용할 거면 Repository 패턴을 사용하지 않아도 됨
  • 사용하는 이유
    • 각각의 데이터 계층에 접근해서 원하는 Model을 가져오기 위해서 반복되는 보일러 플레이팅 코드가 발생
      • ex) API Service, CoreData, Realm, UserDefaults 등의 데이터 계층마다 거의 가져오는 모델만 다르지 수행하는 “행동” 자체는 비슷한 경우가 많다.
    • 그래서 프로토콜을 사용하는 제네릭한 레포지토리를 만들어주면 이 문제를 해결할 수 있다.
  • 기대 효과
    • 도메인 계층과 데이터 계층 사이에 추상 계층(protocol Repository)를 만들면 실제 접근하는 데이터가 위에 나열한 4가지 데이터 중에 어떤 데이터에 접근했는지 관여하지 않고 도메인 계층은 모델을 사용할 수 있다.

도입 전

  • 도메인 계층의 객체는 1,3,5번 메시지를 배치를 결정해야 함
  • 2,4,6 응답이 오면 각각에 맞는 데이터 타입으로 변환을 해야 함
  • 이것을 판단해서 타입 변환하는 코드를 도메인 객체에서 들고 있으면 안 된다. 그럴 필요도 없고.

도입 후

  • CoreData이든, API Service이든, User Defaults이든 도메인 계층은 관여하지 않는다.
    • 각각의 Data Layer 객체가 Repository 만 채택을 하면 사용 가능
  • 위의 그림과 다르게 판단할 필요도, 형변환 할 필요도 없이 1번 메시지와 2번 응답에 대한 코드만 마련해 놓으면 된다. 얼마나 간편한가?

3. Repository Pattern의 구성요소

3.1. Data-mapping 계층

  • 데이터 계층에서 사용하는 모델과 도메인 계층에서 사용하는 모델이 다른 경우 데이터 계층에 저장된 모델을 도메인 모델로 변환시켜 주는 계층
  • ex) CoreData Entity ↔ Domain Model

3.2. Domain 계층

  • 하나의 앱에 여러 도메인이 존재할 수 있음
    • ex) 음식 앱
      • 음식 주문 도메인
      • 결제 정보 도메인
  • 특정 도메인의 기능을 캡슐화하는 애플리케이션의 계층이다.
    • ex) UseCase …

3.3. Collection-Like Interface(Repository)

  • 일반적으로 Repository 가 하는 일은 CRUD를 크게 벗어나지 않음
  • 그 CURD 메서드를 Repository 프로토콜 안에 담아 놓은 것이라고 보면 됨
  • 여기서 중요한 것이 associatedType 을 이해하는 것임
    • protocol 안에 앞으로 Model에서 사용할 타입을 실질적으로 지정해 놓은 것이라고 보면 됨
    • 코드를 보면서 이해하는 것이 쉬우므로 일단 이 정도만 보고 넘어가자.

4. CoreData를 레포지토리 패턴으로 도입하기

이론은 위에서 대강 봤으니 실질적으로 구현해 보면서 만들어보자.

여기서는 CoreData를 어떻게 만드는지는 알려주지 않음

기초적인 CoreData와 관련한 개념과 기본적인 사용법을 안다는 가정하에 글을 씀

apple Core Data Beginner Guide ← 요고를 보고 오시면 좋습니다.

4.1. Generic 한 레포지토리 만들기

프로토콜(추상 인터페이스)

protocol Repository {
    associatedtype Entity

    func get(predicate: NSPredicate?, sortDescriptors: [NSSortDescriptor]?) -> Result<[Entity], Error>

    func create() -> Result<Entity, Error>

    func delete(entity: Entity) -> Result<Bool, Error>
}
  • get(), create(), delete() 메서드등이 있는 기초적인 프로토콜
  • 가장 위쪽에 associatedType Entity 를 주목
  • 그러고 보니 각각의 메서드들에 Entity를 파라미터로 받거나 반환 타입으로 지정한 것을 볼 수 있음
  • 이 Repository Protocol을 채택하는 객체들이 Entity 구체화하면 그 구체화된 타입을 활용하는 것임
    • 아직 이 프로토콜을 채택하는 객체가 무엇인지 나오지 않아서 이해가 안 될 수 있음, 천천히 넘어가세요
  • CoreData 용어
    • NSPredicate: 어떤 Entity를 가져올 것인지 밝주는 타입이라고 이해하면 됨
      • ex) [name 속성에 “kindness”라는 스트링이 포함된 Book Entity만 가져오고 싶다]라고 선언하는 타입임
    • [NSSortDescriptor]: 가져온 Entity들을 어느 순서로 정렬할지 기준을 담은 배열

구체 구현 객체

enum CoreDataError: Error {
    case invalidManagedObjectType
}

class CoreDataRepository<T: NSManagedObject>: Repository {
    typealias Entity = T

    private let managedObjectContext: NSManagedObjectContext

    init(managedObjectContext: NSManagedObjectContext) {
        self.managedObjectContext = managedObjectContext
    }

    func get(predicate: NSPredicate?, sortDescriptors: [NSSortDescriptor]?) -> Result<[Entity], Error> {
        let fetchRequest = Entity.fetchRequest()
        fetchRequest.predicate = predicate
        fetchRequest.sortDescriptors = sortDescriptors
        do {
            if let fetchResults = try managedObjectContext.fetch(fetchRequest) as? [Entity] {
                return .success(fetchResults)
            } else {
                return .failure(CoreDataError.invalidManagedObjectType)
            }
        } catch {
            return .failure(error)
        }
    }

    func create() -> Result<Entity, Error> {
        let className = String(describing: Entity.self)
        guard let managedObject = NSEntityDescription.insertNewObject(forEntityName: className, into: managedObjectContext) as? Entity else {
            return .failure(CoreDataError.invalidManagedObjectType)
        }
        return .success(managedObject)
    }

    func delete(entity: Entity) -> Result<Bool, Error> {
        managedObjectContext.delete(entity)
        return .success(true)
    }
}
  • Repository 프로토콜을 채택한 구체적인 CoreData 구현 객체
  • class CoreDataRepository<T: NSManagedObject>: Repository
    • NSManagedObject 타입의 제네릭 T를 선언하고 typealiasEntityT 로 지정
  • get(), create(), delete() 가 구현된 모습
    • 메서드 내부 구현은 상상력에 맡깁니다.
      • CoreData 개념이 필요한 이유입니다.

실행 모습

let bookRepository = CoreDataRepository<BookMO>(managedObjectContext: context)

let bookResult = bookRepository.create()
switch result {
case .success(let bookMO):
    bookMO.title = "The Swift Handbook"
case .failure(let error):
    fatalError("Failed to create book: \(error)")
}
context.save()
  • CoreDataRepository 객체를 초기화할 때 제네릭 타입 T를 지정함 <BookMO>
  • bookRepository.create() 이런 식으로 깔끔하게 명령을 내릴 수 있음
  • 그렇지만 여기에도 문제점은 있다.
  • 이 코드 블록이 자리 잡을 곳이 아마도 Domain Layer
    • 결과적으로 Domain Model에서 Data Model(BookMO)에
      직접 접근하고 생성을 성공인지 실패인지 switch 문을 돌려보는 게 됨
  • 도메인 계층의 코드들은 CoreData에서 어떻게 모델을 가져오고 어떻게 생성하는지 알아서는 안됨
    1. 언젠가는 BookMO을 분리할 수도 있음, PressMO, AuthorMO, 등등…
    2. CoreData에서 Realm으로 DataBase를 변경한다고 하면, Domain Layer의 코드가 변경되어야 함
  • 데이터 오브젝트(BookMO)를 대신 다룰 수 있으면서 도메인과 소통할 Repository가 하나 더 필요함
    • 위에서 만든 <protocol> Repository 에 추가적으로 하나 더 필요함

4.2. Domain Model과 Data Model을 분리하기


protocol BookRepositoryInterface {
    func getBooks(predicate: NSPredicate?) -> Result<[Book], Error>
    func create(book: Book) -> Result<Bool, Error>
}

class BookRepository {
    private let repository: CoreDataRepository<BookMO>

    init(context: NSManagedObjectContext) {
        self.repository = CoreDataRepository<BookMO>(managedObjectContext: context)
    }
}

extension BookRepository: BookRepositoryInterface {

    @discardableResult func getBooks(predicate: NSPredicate?) -> Result<[Book], Error> {
        let result = repository.get(predicate: predicate, sortDescriptors: nil)
        switch result {
        case .success(let booksMO):
            let books = booksMO.map { bookMO -> Book in
                return bookMO.toDomainModel()
            }
            return .success(books)
        case .failure(let error):
            return .failure(error)
        }
    }

    @discardableResult func create(book: Book) -> Result<Bool, Error> {
        let result = repository.create()
        switch result {
        case .success(let bookMO):
            // Update the book properties.
            bookMO.identifier = book.identifier
            bookMO.title = book.title
            bookMO.author = book.author
            return .success(true)

        case .failure(let error):
            // Return the Core Data error.
            return .failure(error)
        }
    }

}
  • 이전에 도메인 계층에서 했던 작업을 캡슐화해줄 BookRepository 가 생겼음
  • 구체적인 구현과 성공 실패에 대한 에러처리를 함
  • 여기서 중요한 게 반환 타입이 BookMO 에서 Book 으로 변화한 걸 봐야 함
    • BookMO : CoreData가 사용하는 모델, 편하게 데이터 모델이라고 부르겠음
    • Book: Domain 계층에서 사용하는 모델, 도메인 모델
  • 도메인 계층의 객체가 BookRepository와 상호작용을 하면 BookMO에 대해서 전혀 모르고도 Book 모델에 접근할 수 있는 것이 포인트임
//MAKR: - 도메인 모델
struct Book {
    let identifier: String
    let title: String
    let author: String
}

protocol DomainModel {
    associatedtype DomainModelType
    func toDomainModel() -> DomainModelType
}

extension BookMO: DomainModel {
    func toDomainModel() -> Book {
        return Book(identifier: identifier,
                    title: title,
                    author: author)
    }
}
  • 이렇게 데이터 모델에 도메인 모델로 변환하는 프로토콜을 채택시켜서 도메인 모델로 변환함
  • 그림으로 그리자면 이렇게 되었음

  • 도메인 계층의 객체의 입장에서 보면 1번과 7번만 보이지 2,3,4,5,6 은 캡슐화 되어서 보이지 않음

5. 글 맺음

요즘 앱에서 데이터 통신은 필수라고 해도 과언이 아닙니다. (API Service, Persistent Store)

이때 사용하는 것이 Respository Pattern이고 하면 여러 일들을 캡슐화할 수 있어서 유용합니다.

반복되는 코드들도 줄일 수 있고, 무엇보다 한 곳에서 관리 가능한 점이 응집도가 올라가는 결과를 냈다는 것이죠.

혹시나 지금 내용이 어렵다고 하시면,
Core Data 학습과 Clean Architecture를 조금 보고 오시는 것을 추천드립니다.

Repository Pattern 자체는 이것보다 쉽지만, Core Data를 사용해야 해서 조금 글이 어려워진 감이 있습니다.

항상 즐거운 코딩 하세요 ^^

참고

 

Repository Pattern에 대해서

본 글은 Repository Pattern in Swift을 해석 및 재해석 했습니다.

devming.medium.com

 

 

Repository Pattern in Swift

See how to use the repository pattern to make your data layer easier to maintain

blog.devgenius.io

 

 

Repository pattern in Swift

If you have any experience or exposure to the Android development world, you must have heard Google promoting the usage of the "Repository pattern" in conjunction with MVVM. But the Repository pattern is much more than just a Google recommendation. ...

pavanpowani.hashnode.dev

 

 

Repository pattern using Core Data and Swift - UserDesk

Core Data is a framework that is included in the iOS SDK that helps developers persist their model objects into an object graph. While it may prove tryicky to master at first, it helps developers with built-in features such as: Change tracking Relationship

www.userdesk.io

 

 

Unit Testing Core Data in iOS

Testing code is a crucial part of app development, and Core Data is not exempt from this. This tutorial will teach you how you can test Core Data.

www.kodeco.com

 

Comments