Repository pattern in swift

hyob·2020년 10월 22일
1

들어가며..

Mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects. - Patterns of Enterprise Application Architecture

어플리케이션 로직과 데이터베이스 사이의 레이어를 두는 패턴이다.
애플리케이션의 나머지 부분에서 데이터 저장 로직을 분리한다.

장점

  • 비즈니스 로직 레이어를 유지하기 쉽다.
  • 테스트 코드를 분리할 수 있다.
  • 디비 프레임웍을 바꾸기 쉽다.

구현

제 앱에 Realm을 도입하면서 적용해 봤습니다.
최종본은 아닌데 이 글도 초안 적어둔지 좀 되서.. 매끄럽지 못합니다.

프로토콜(인터페이스)

Pepository

protocol Repository {
    associatedtype EntityObject: Entity
    
    func getAll(where predicate: NSPredicate?) -> [EntityObject]
    func insert(item: EntityObject) throws
    func update(item: EntityObject) throws
    func delete(item: EntityObject) throws
}

extension Repository {
    func getAll() -> [EntityObject] {
        return getAll(where: nil)
    }
}

디비 쿼리를 하는 동한 수행하는 CRUD 기능을 포함하는 레포지토리 프로토콜.

Entity

public protocol Entity {
    associatedtype StoreType: Storable
    
    func toStorable() -> StoreType
}

public protocol Storable {
    associatedtype EntityObject: Entity
    
    var model: EntityObject { get }
    var id: Int { get }
}

디비에 저장해야하는 모든 모델은 Entity프로토콜을 구현해야함.

실제 데이터를 디비에 저장하기 위해 Storable 프로토콜을 사용해야 함.

이 프로토콜은 모델의 데이터베이스 객체를 나타내는 클래스에 의해 구현됨. Storable 프로토콜에는 두 가지 속성이 있음.

  • model 저장된 객체를 실제 모델로 변환하는 역할.
  • id – unique identifier

클래스

Entity

class Game {
    var cost: Int
    var targetCount: Int
    var playerCount: Int
    
    init(cost: Int, targetCount: Int, playerCount: Int) {
        self.cost = cost
        self.targetCount = targetCount
        self.playerCount = playerCount
    }
}

extension Game: Entity {
    private var storableGame: StorableGame {
        let realmGame = StorableGame()
        realmGame.id = realmGame.autoIncrementKey()
        realmGame.cost = cost
        realmGame.targetCount = targetCount

        return realmGame
    }
    
    func toStorable() -> StorableGame {
        return storableGame
    }
}

class StorableGame: Object, Storable {
    @objc dynamic var id = 0
    @objc dynamic var cost: Int = 0
    @objc dynamic var targetCount: Int = 0
    @objc dynamic var playerCount: Int = 0
    
    override static func primaryKey() -> String? {
        return "id"
    }
    
    func autoIncrementKey() -> Int {
        let realm = try! Realm()
        return (realm.objects(StorableGame.self).max(ofProperty: "id") as Int? ?? 0) + 1
    }
    
    var model: GameVO {
        get {
            return Game(cost: cost, targetCount: targetCount, playerCount: playerCount)
        }
    }
}

모델 객체인 Game 은 Entity를 채택하고 구현만 하면 됨.

Repository

class AnyRepository<RepositoryObject>: Repository
        where RepositoryObject: Entity,
        RepositoryObject.StoreType: Object {
    
    typealias RealmObject = RepositoryObject.StoreType
    
    private let realm: Realm

    init() {
        realm = try! Realm()
    }

    func getAll(where predicate: NSPredicate?) -> [RepositoryObject] {
        var objects = realm.objects(RealmObject.self)

        if let predicate = predicate {
            objects = objects.filter(predicate)
        }
        print(objects.compactMap { $0 })
        return objects.compactMap{ ($0).model as? RepositoryObject }
    }

    func insert(item: RepositoryObject) throws {
        try realm.write {
            realm.add(item.toStorable())
        }
    }

    func update(item: RepositoryObject) throws {
        try delete(item: item)
        try insert(item: item)
    }

    func delete(item: RepositoryObject) throws {
        try realm.write {
            let predicate = NSPredicate(format: "id == %@", item.toStorable().id)
            if let productToDelete = realm.objects(RealmObject.self)
                .filter(predicate).first {
                realm.delete(productToDelete)
            }
        }
    }
}

제네릭한 레포지토리 구현체.
새로운 모델 Entity가 생길때 편함.

repository : DAO 역할

UI - viewmodel(현재 Game) - model - storable(DTO) - repository(DAO) - DB(현재 Realm)

이런식으로 레이어가 나뉨.

예전엔 레이어가 많으면 불편하다 생각했는데 생각이 좀 바뀐것 같음.

물론 보일러 플레이트 같이 느껴질 때도 있지만.. 그건 다른 방법으로 해결 할 수 있어 보임.

정리

1달 전에 반쯤 정리한 글을 지금 포스팅 하는데.. 참 그지같다.
글을 완결하는 습관은 들여야 할듯.
레일즈, 루비도 쓰던게 있는데..

id대신 uuid 를 사용 하면 될 것 같다.
피부에 와닿았던 디자인 패턴 중 한가지 인듯.

카카오페이에서 랜덤 사다리 정산 서비스가 이번에 나와서
랜덤빵 접어야할듯..
새 앱 들어가야지.

profile
앵커리어에서 자소설닷컴을 개발하고 있습니다.

0개의 댓글