본문 바로가기

iOS

Realm에 Repository Pattern을 곁들인

728x90

기깔난 제목을 쓰고 싶은데 생각이 안남. ㅈㅅ. 나도 그만 곁들이고 싶음.

 

여러 ViewController에서 Realm의 테이블에 접근하여 CRUD를 할 수 있다. (아마 대부분의 앱이 그렇게 동작이 될텐데) 이 때마다 새롭게 변수를 선언해서, CRUD 코드를 작성하는 것은 효율적이지 못하다. 그러므로 Repository Pattern을 적용해서 코드를 개선할 수 있다. 

 

Repository Pattern

패턴을 적용하기 전에, 무슨 패턴인지/어떤 패턴인지 먼저 알아보도록 하자.

이 패턴은 주로 MVP, MVVM 구조에서 많이 사용한다.

 

프로그램의 핵심인 비즈니스 로직이 얼마나 잘 짜여졌는지에 따라서 결과가 다르게 나온다. 이 비즈니스 로직은 보통 외부 데이터베이스나 앱 내 데이터베이스에 접근하게 되는데 이 과정에서 여러 문제가 발생할 수 있다. (중복 코드, 오류 코드 등 ..)

이러한 문제를 해결하기 위한 방법으로, 아래 두 가지가 있다.

1. 비즈니스 로직과 데이터 레이어를 분리하는 것

2. 중앙 집중 처리 방식을 통해 일관된 데이터와 로직을 제공하는 것 

 

Repository Pattern이란?

이 두가지 해결 방법을 토대로 나온 것이 바로 Repository Pattern이다.

저장소 라는 뜻을 가진 레포지토리는 데이터 소스 레이어와 비즈니스 레이어 사이를 중재한다.

데이터 소스에 쿼리를 날리거나 다른 도메인에서 사용할 수 있도록 새롭게 매핑할 수 있다.

 

 

데이터가 있는 저장소인 RemoteDataSource와 LocalDataSource를 추상화하여 중앙 집중 처리 방식을 구현한다.

 

🟢 데이터를 사용하는 도메인에서 비즈니스 로직에만 집중할 수 있다. ex) ViewModel에서는 데이터가 Local DB에서 오는지, 서버 API를 통해 오는지 알 필요가 없다. Repository를 참조해 제공하는 데이터를 이용하기만 하면 된다.

 

🟢 Repository가 추상화되어 있으므로, 항상 같은 Interface로 데이터를 요청할 수 있다.

 

Repository Pattern의 장점

해당 패턴을 사용했을 때 장점은 아래와 같다.

  • 데이터 로직과 비즈니스 로직을 분리할 수 있다. → 관심사의 분리
  • 도메인에서는 일관된 인터페이스를 통해 데이터를 요청할 수 있다.
  • 데이터 저장소의 데이터를 캡슐화할 수 있어 객체지향적인 프로그래밍에 더 적합하다.
  • 단위 테스트를 통한 검증이 가능하며, 객체 간 결합도가 감소한다.

 


Repository Pattern 적용 

쇼핑리스트 앱에 레포지토리 패턴을 적용해서 코드를 개선해보자 !!

 

가장 먼저 Repository 파일을 만든다.

 

해당 프로젝트의 경우 Realm의 테이블 구조가 Product 하나였기 때문에 ProductRepository 이름의 파일로 만들었다.

import Foundation

import RealmSwift

class ProductRepository {
    
}

 

레포지토리 패턴의 장점은 항상 같은 Interface로 데이터를 요청할 수 있다.는 것이다.

그러므로 같은 인터페이스를 사용하기 위한 protocol을 하나 만들어준다. 

 

protocol UserDiaryRepositoryType {
    // Create
    func addItem(item: Product)
    
    // Read
    func fetch() -> Results<Product>
    func fetchSort(_ sort: String) -> Results<Product>
    func fetchFilter(_ filter: String) -> Results<Product>
    
    // Update
    func updateCheck(item: Product)
    
    // Delete
    func deleteItem(item: Product)
}

class ProductRepository: UserDiaryRepositoryType {
    
}

프로토콜을 만들고 이를 채택하면 ProductRepository 내부에서 해당 함수들을 구현해야 한다.

 

구현하지 않으면, 오류 메시지가 뜨게 되는데 fix 버튼을 누르게 되면 아래와 같이 구현해야 하는 함수들이 자동 완성된다.

 

그리고 각 함수에 그동안 ViewController에 개별적으로 작성한 코드를 작성하면 된다.

 

Create (= add)

쇼핑 리스트 추가하는 코드를 레포지토리 패턴으로 개선해보자.

 

먼저 개선하기 전에 작성한 곳으로 이동한다.

// AddViewController

    @objc func touchUpDoneButton() {
        if let text = textField.text {
            let task = Product(name: text, check: false, date: Date())
            
            try! localRealm.write {
                localRealm.add(task)
            }
            
            dismiss(animated: true)
        }
    }

위와 같이 렘에 새롭게 추가하는 코드가 있다.

 

여기서 렘에 추가하는 코드를 가지고 와서 붙여넣어주면 된다. ⬇️

class ProductRepository: UserDiaryRepositoryType {
    
    // local Realm 생성
    let localRealm = try! Realm()
    
    // 추가
    func addItem(item: Product) {
        do {
            try localRealm.write {
                localRealm.add(item)
            }
        } catch let error {
            print(error)
        }
    }
}

*do-try-catch문을 통해 error handling을 하였다.

 

그리고 다시 AddViewController에서는 레포지토리 상수를 선언해서 해당 함수를 호출하면 된다.

private let repository = ProductRepository() ✅

...

@objc func touchUpDoneButton() {
    if let text = textField.text {
        let task = Product(name: text, check: false, date: Date())
        
        repository.addItem(item: task) ✅
        
        dismiss(animated: true)
    }
}

 

같은 방식으로 다른 기능(= CRUD)도 구현하면 된다.

class ProductRepository: UserDiaryRepositoryType {
    
    // local Realm 생성
    let localRealm = try! Realm()
    
    // 데이터 추가
    func addItem(item: Product) {
        do {
            try localRealm.write {
                localRealm.add(item)
            }
        } catch let error {
            print(error)
        }
    }
    
    // 데이터 읽기
    func fetch() -> Results<Product> {
        return localRealm.objects(Product.self).sorted(byKeyPath: "date", ascending: false)
    }
    
    // 키워드 정렬
    func fetchSort(_ sort: String) -> Results<Product> {
        return localRealm.objects(Product.self).sorted(byKeyPath: "\(sort)", ascending: false)
    }
    
    // 체크 유무 필터 
    func fetchFilter() -> Results<Product> {
        return localRealm.objects(Product.self).filter("check == true")
    }
    
    // 체크 업데이트
    func updateCheck(item: Product) {
        do {
            try localRealm.write {
                item.check.toggle()
            }
        } catch let error {
            print(error)
        }
    }
    
    // 삭제
    func deleteItem(item: Product) {
        do {
            try localRealm.write {
                removeImageFromDocument(fileName: "\(item.objectId).jpg")
                localRealm.delete(item)
            }
        } catch let error {
            print(error)
        }
    }
}