라이브 오브젝트와 정밀한 알림: Realm 업데이트 기능

Realm Database series header

이 글은 Realm 모바일 데이터베이스에 대한 시리즈 글입니다. 정밀한 알림을 기반으로 부분적인 UI 업데이트를 제공할 수 있게 하는 Realm SDK 기능을 설명합니다. 혹시 시리즈 시작 글을 놓치셨다면 첫 번째 글과 성능 향상과 메모리 사용량 최소화에 최적화된 Realm API를 소개하는 두 번째 글, 플랫폼 간 코드 공유를 다룬 세 번째 글, Realm처럼 유연하고 견고한 SDK가 어떻게 좋은 개발을 돕는지에 대한 네 번째 글을 먼저 보시길 추천합니다. 마지막 시리즈에 대한 한글 번역은 아직 진행 중으로 이같은 기능이 모여 어떻게 최신 모바일 애플리케이션을 만드는지에 대한 여섯 번째 글을 참고해 주세요.


“전통적인” 지속성 레이어를 사용하면 데이터를 저장한 후에 해당 데이터를 검색할 수 있습니다. 그다음 일반적으로 애플리케이션을 구축하기 위해 데이터 엔티티를 만들고 스토리지에 넣은 후 필요한 시점에 저장소에서 받아오거나 검색하는 개념으로 개발하게 됩니다.

하지만 앱이 항상 같은 클래스에서 데이터를 읽고 쓰지는 않으므로 클래스 간에 통신할 수 있는 좋은 메커니즘을 생각해야 합니다. 변경 사항이 생기면 어떤 내용이 작성됐는지, 그리고 이전에 가져온 데이터를 새로 고쳐야 하는지 서로 알려야 하죠.

복잡한 앱에서는 종종 개발자들이 관련 없는 클래스끼리 통신하는 여러 개의 다른 메시지 전달 메커니즘을 사용해서 데이터가 추가되거나 업데이트되었는지 알도록 합니다. 노티피케이션 센터, 콜백 블럭, 세터, 클래스 주입, KVO, 다양한 델리게이트 프로토콜 등등 다양한 방식을 사용하고 있습니다.

Complex notifications setup

이런 문제의 원인은 데이터가 정적이라는 것입니다. 디스크에서 검색했던 그 순간에 정확히 고정되므로 데이터를 가져오자마자 최신 상태가 아니게 됩니다. 즉, 다른 클래스가 1초 만에 수정해버릴 수 있으므로, 항상 “구버전”으로 간주할 수밖에 없습니다.

Data is stale as soon as it's read

문제의 근본 원인은 클래스가 정적인 상태로 지속성 레이어에서 되돌아오기 때문입니다. 즉, 데이터가 특정 시점에 고정되는 것이죠. 데이터를 “최신” 상태로 유지하려면 쓸 때마다 다시 가져오는 수밖에 없습니다.

라이브 오브젝트를 사용하는 데이터베이스

이 시리즈 첫번째 글에서는 Realm의 커스텀 데이터베이스 엔진에 대해 다루면서 Realm이 기존 기술을 답습하지 않고 새로운 엔진을 만들었다고 설명했습니다.

이런 개발 뉴스를 더 만나보세요

Realm은 SQL을 사용하지 않으며 저장소에 넣을 수 있는 형식과 코드에서 사용하는 형식을 서로 변환할 필요가 없습니다. 또한, Realm SDK는 실제로 사용되는 순간에서야 비로소 메모리에 데이터를 복사합니다.

이러한 특성때문에 Realm은 라이브 오브젝트 데이터베이스 입니다. 개발자가 접근하는 데이터는 항상 최신 상태입니다. 디스크에서 데이터를 “가져온다”는 개념이 없으므로 계속 리로드할 필요가 없습니다. 데이터 모델을 디스크에서 메모리로 중복해서 복사할 필요도 없죠.

덕분에 개발자는 컨트롤러 클래스를 완전히 다른 방식으로 작성할 수 있습니다. 컨트롤러의 생명을 UI 업데이트, 읽기 및 쓰기, 데이터 더미 푸시와 풀 등의 행동 중심으로 삼을 필요가 없습니다.

실제로 읽기와 쓰기는 프로젝트의 다른 지점, 다른 스레드, 다른 프로세스 어디에서나 가능해집니다. Realm 모바일 플랫폼을 쓴다면 나아가 세계 어디에서나 가능하죠. Realm 객체는 항상 최신이므로 앱의 각 클래스에서 오래된 혹은 캐시된 데이터 개념을 고민할 필요 없이 비즈니스 로직에만 집중할 수 있습니다.

물론 Realm 데이터베이스의 단일 객체만이 항상 최신인 것은 아닙니다. 결과 세트와 리스트와 같은 컬렉션도 항상 최신을 유지합니다. 정말 멋지죠? 👏

예를 들어 한 번 Results 객체를 만들면 디스크의 최근 결과를 항상 반영합니다. 다음 코드를 보시죠.

let people = realm.objects(Person.self)
print(people.count) // outputs: "0"

try! realm.write {
  let marin = Person()
  realm.add(marin)
}

print(people.count) // outputs: "1"

reloadrefresh 메서드를 호출하지 않아도 결과 세트는 항상 Realm 모바일 데이터베이스의 최신 데이터를 제공합니다.

정밀한 알림

Realm의 ResultsList 클래스가 항상 최신 데이터를 제공하긴 하지만, UI는 내부의 변화를 알지 못합니다.

편리하게도 Realm 모바일 데이터베이스가 이 역시 처리해 줍니다. ResultsList 인스턴스에 변경 알림을 “구독”하기만 하면 됩니다. Realm은 변경이 발생했을 뿐만 아니라 삽입, 업데이트, 삭제된 객체의 정확한 인덱스도 알려 주죠.

이 방식으로 항상 최신 데이터를 사용할 수 있을 뿐만 아니라 정확히 언제, 어떤 객체가 변화했는지도 알 수 있습니다.

Complex Notificaitons Setup

또한, 컬렉션 객체의 변화된 인덱스들을 알면 앱의 UI에서 쉽게 업데이트할 수 있습니다.

물론 간단하게 reloadData()를 호출해서 테이블의 행이나 컬렉션의 셀을 새로 고쳐도 되지만, 변경된 행에만 사용자 정의 애니메이션을 넣는 것도 어렵지 않습니다.

본 시리즈에서 사용하는 예제 프로젝트를 참고하면 iOS 테이블 뷰를 알림으로 쉽게 업데이트하는 전체 코드를 확인할 수 있습니다. MasterViewController.swift의 세부 내용을 살짝 보여드리겠습니다.

switch changes {
  case .initial:
    self.tableView.reloadData()

  case .update(_, let deletions, let insertions, let updates):
      self.tableView.beginUpdates()
      self.tableView.insertRows(at: insertions.map {IndexPath(row: $0, section: 0)}, with: .automatic)
      self.tableView.reloadRows(at: updates.map {IndexPath(row: $0, section: 0)}, with: .automatic)
      self.tableView.deleteRows(at: deletions.map {IndexPath(row: $0, section: 0)}, with: .automatic)
      self.tableView.endUpdates()

  default: break
}

알림 변경은 다음처럼 세 가지 종류로 제공됩니다.

  • .initial(let results) - 결과 세트나 리스트가 관찰되기 시작한 시점. 코드에 따라 뷰 컨트롤러가 스크린에 보이는 시점일 경우가 많습니다. 대부분 이 시점에서는 애니메이션이 필요하지 않고 간단히 reloadData()만으로 충분할 수 있습니다.
  • .update(let results, let deletions, let insertions, let updates) - 관찰하던 컬렉션에 데이터가 변경된 경우. 정확한 컬렉션 인덱스와 변경 유형을 알 수 있습니다.
  • .error(let error) - 컬렉션 관찰 도중 에러가 발생한 경우. 😵

예제 프로젝트의 뷰 컨트롤러 클래스에서 단 몇 줄만으로 테이블이 변경된 경우 애니메이션하는 코드를 볼 수 있습니다. (tvOS, macOS 등을 위한 MasterViewController도 확인하세요.) 다른 뷰 컨트롤러에서 쉽게 재사용할 수 있도록 코드를 UITableView 익스텐션으로 추출할 수도 있습니다.

extension UITableView {
  func applyChanges<T>(changes: RealmCollectionChange<T>) {
    switch changes {
      case .initial: reloadData()
      case .update(_, let deletions, let insertions, let updates):
        let fromRow = {(row: Int) in 
          return IndexPath(row: row, section: 0)}

        beginUpdates()
        deleteRows(at: deletions.map(fromRow), with: .automatic)
        insertRows(at: insertions.map(fromRow), with: .automatic)
        reloadRows(at: updates.map(fromRow), with: .none)
        endUpdates()
      default: break
    }
  }
}

이 메서드를 사용하면 테이블 뷰를 Realm 컬렉션으로 “연결”합니다.

let token = myResults.addNotificationToken(tableView.applyChanges)

UITableViewUICollectionView API가 Realm SDK와 완벽하게 일치하는 것을 확인하셨나요? Realm SDK는 모바일 애플리케이션을 개발하는데 필요한 개발자의 요구를 충족시키기 위해 특별히 설계되었으므로 Realm은 범용, 다용도 데이터베이스가 아닙니다.

높은 메모리 효율로 변경 사항 업데이트

Realm은 마지막 컬렉션과 최신 컬렉션 사이에 diff를 생성할 필요 없이 변경 세트를 되돌릴 수 있습니다. 한편 같은 기능을 하는 서드 파티 라이브러리들은 메모리에 “기존” 데이터와 현재 데이터를 보관하고 이를 객체 단위로 비교해서 변경 세트를 만들죠.

이렇게 하면 데이터를 두 배로 저장하므로 애플리케이션의 메모리를 많이 사용하게 됩니다. 메모리를 더 사용할 뿐만 아니라 매우 느리기까지 합니다.

Realm SDK는 그럴 필요가 없으므로 데이터를 메모리에 복제하고 변경 사항 diff를 만들고 모든 작업을 수행하기 위해 스레드를 다시 전환하는 등의 성능 저하 동작을 피할 수 있습니다. 변경된 인덱스를 바로 가져와서 평소처럼 새 데이터를 읽으면 되죠!

인터페이스 기반 쓰기

마지막으로 Realm은 알림 정보(notification info)라는 편리한 기능을 하나 더 제공합니다. 이를 통해 사용자가 UI와 상호 작용해서 시작하는 쓰기 작업과 그렇지 않은 작업을 구분할 수 있습니다.

앱 로직 관점에서 앞서 말한 두 가지 경우에는 종종 각기 다른 접근 방식을 사용해야 합니다.

1. 인터페이스 기반 - 사용자가 테이블 셀을 왼쪽으로 스와이프 하고 “삭제”를 탭 합니다. 이 경우 테이블 뷰에서 행이 삭제되고 관련 테이블 델리게이트 메서드를 호출합니다. 해당 메서드에서 Realm을 사용해서 해당 객체를 삭제합니다. 물론 UI가 이미 최신 상태를 반영하고 있으므로 이 경우에는 변경 사항을 따로 알리지 않아도 됩니다.

Write Skipping Notifications

알림을 건너뛰면 비동기적으로 전달되는 데이터 레이어의 변경 알림에 반응하는 방식을 사용하지 않고도 UI를 즉시 업데이트할 수 있습니다. commitWrites를 호출하면서 withoutNotifying 매개 변수로 이번 쓰기 트랜잭션에서 “알림을 보내지 않을” 토큰 리스트를 추가하세요.

func viewDidLoad() {
  self.messages = realm.objects(Message.self)
  self.token = self.messages.addNotificationToken(tableView.applyChanges)
}

func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
  if (editingStyle == .delete) {
    realm.beginWrite()
    messages.removeAtIndex(indexPath.row)
    realm.commitWrite(withoutNotifying: [self.token])

    tableView.deleteRows(at: [indexPath], with: .automatic)
  }
}

2. 비 인터페이스 초기화 - 네트워크 클래스가 백그라운드 스레드에서 JSON을 가져오고 Realm 데이터베이스에 새 객체를 쓰는 경우 해당 변경에 대해 반드시 알림을 받아야만 화면의 테이블 뷰를 업데이트할 수 있겠죠?

이럴 때는 알림을 구독하고 일반적인 방식으로 쓰기를 실행합니다.

Write Skipping Notifications

글이 마음에 드셨나요?

이번 글에서 Realm SDK의 우수한 기능인 애플리케이션의 UI를 부분적으로 업데이트할 수 있는 정밀한 알림에 대해 다뤘습니다.

더 자세히 알고 싶은 분은 Realm 모바일 데이터베이스를 사용해 보세요.

다음 연재에서는 이 시리즈에서 다룬 모든 내용을 전반적으로 살펴보고 Realm 모바일 데이터베이스의 멋진 기능을 활용해서 애플리케이션 개발 방식을 어떻게 바꿀 수 있는지 알아보겠습니다.

다음 편에서 만나요! 👋

다음: Realm의 차별화 요소들 #6: 최신 모바일 애플리케이션을 위한 데이터베이스, Realm의 개발 전략

General link arrow white

컨텐츠에 대하여

이 컨텐츠는 저자의 허가 하에 이곳에서 공유합니다.


Marin Todorov

Marin Todorov는 iOS 컨설턴트이자 퍼블리셔입니다. “iOS Animations by Tutorials”의 저자이며 iOS Animations by Emails” 뉴스레터를 운행하고 있습니다. 20년 넘게 Apple 관련 개발을 했으며 Monster Technologies와 Native Instruments 등의 회사에서 일하면서 4개 이상의 나라에서 거주했습니다. 또한 raywenderlich.com 튜토리얼 팀의 설립 멤버이기도 하죠. 코드 개발 이외에는 블로그 게재와 책 저술, 교육과 강연에 관심이 많으며, 코드를 오픈 소스화 하기도 합니다. Santiago 순례 경험도 있습니다.

4 design patterns for a RESTless mobile integration »

close