Realm Blog

Realm Objective-C와 Swift 2.2 버전의 신기능, 스레드 간 객체 전달과 관계 정렬 기능 추가

Realm의 초창기 목표는 일관성있고 간단한 스레딩 모델을 만드는 것이었습니다. 오늘 저희는 스레드에서 스레드로 안전하게 객체를 전달할 수 있는 구조를 도입하여 스레딩 모델을 개선한 Realm Objective‑CRealm Swift 2.2 버전을 배포합니다. 관계 프로퍼티로 정렬할 수 있는 기능과 동기화 개선, 버그 픽스도 포함됩니다.

스레드 이슈

Realm을 사용하면 멀티 스레드에서 객체를 쉽고 안전하게 사용할 수 있습니다. 다년간의 꾸준한 연구와 신중한 설계 결정으로 어려운 스레딩 문제를 해결하고 기존 버전의 멀티 스레딩 지원 기능이 가능하게 됐습니다.

기존의 영문 기사, Threading Deep Dive에서는 사용자가 락이나 자원 조정을 고민하지 않도록 Realm이 동시성을 안전하게 처리하는 다양한 원리에 대해 전체 Realm에서 제공하는 동시성있는 뷰와 객체 그래프를 통해 설명했습니다. 주목할 것은 이 디자인 구조로 다른 ORM이나 데이터 프레임워크에서 나타나는 ‘오류’(fault)💥라는 개념을 해결했다는 점입니다.

동시 환경에서 Realm을 제대로 사용하는 방법을 깊게 이해하려면 저희 공식 문서의 Threading 항목을 꼭 참고해 주세요. 이 리소스를 사용하면 멀티 스레딩과 Realm을 정말로 효율적으로 사용할 수 있습니다. 하지만 이번 배포 전까지는 사용하는 객체가 스레드를 넘지 못했었죠.

스레드 국한

Realm이 그렇게 안전한 스레드 사용이 가능하다면, 왜 스레드 간에 객체를 넘기려고 할 때 예외가 발생하나요?!

Realm의 일관성과 안전성을 보장하기 위해서는 간단한 제약이 부과됩니다. 모든 Realm과 객체, 탐색 결과, 리스트는 작성한 스레드로 제한된다는 것이었죠.

스레드 국한은 정확한 코드를 쉽게 작성하기 위한 디자인의 핵심 부분이며, Realm의 내부에 따른 임시 제한이나 인공적인 제약이 아닙니다.

사실, Realm 객체를 스레드 간에 자유롭게 전달할 수 있도록 구현하는 것은 아주 쉽지만, 사용하기 매우 위험하고 예측이 어렵다는 단점이 있습니다.

Realm은 앱에서 디스크에 부분적으로만 유효한 데이터를 쓰지 않고도 쓰기 도중에 크래시가 발생하는 것을 허용하는 트랜젝션 데이터베이스입니다. write transaction 범위를 생각하면 이해가 쉽습니다.

ACID 중의 isolation, 즉 고립성을 보장하려면 한 트랜젝션에서의 변화가 최신 버전으로 진행될 때까지 다른 트랜젝션에 영향을 미치지 않아야 합니다. 그렇지 않으면 Realm에서 없애둔 ‘오류’(fault)가 발생할 수 있습니다.

이런 고립성을 위해 Realm들을 만든 스레드 안에서의 Realm들은 동일한 버전입니다. 내부적으로는 스레드 당 단 하나의 Realm이 있게 됩니다. Realm이나 Realm의 객체, 쿼리 등을 스레드 간에 자유롭게 교환하는 것은 데이터베이스의 다른 버전을 섞는 것이 되고, 이는 심각한 결과로 이어질 수 있습니다. 예를 들어 객체를 삭제한 스레드에 객체를 전달하면 크래시가 일어날 수도 있고, 스레드 경계를 넘어갈 때 값이 변경될 수도 있으며, 객체 그래프가 각 트랜젝션 버전마다 다른 관계를 가지게 될 수도 있습니다.

스레드 간 데이터를 전달하는 기존 방법

지금까지는 스레드 간에 데이터를 전달하기 위해서 Realm이 지원하지 않는 데이터를 전달해야 했습니다.

주로 관리되지 않는 Realm 객체의 인스턴스를 넘기거나 기본 키 등 Realm이 지원하는 속성을 통해 데이터를 읽는 방법을 사용했죠.

let realm = try! Realm()
let person = Person(name: "Jane", primaryKey: 123)
let pk = person.primaryKey
try! realm.write {
  realm.add(person)
}
DispatchQueue(label: "com.example.myApp.bg").async {
  let realm = try! Realm()
  guard let person = realm.object(ofType: Person.self,
                                  forPrimaryKey: pk) else {
    return // person was deleted
  }
  try! realm.write {
    person.name = "Jane Doe"
  }
}

그러나 객체에 기본 키가 없는 경우 이 방법을 사용할 수 없고, 부실한 데이터를 작업하는 결과가 될 수 있습니다. List, Results, LinkingObjects 등 Realm 객체가 아닌 것을 전달하는 것도 이 접근법으로는 쉽지 않습니다.

스레드 사용이 안전한 참조 사용

이 번 배포부터는 Realm에서 한 스레드로만 국한되던 모든 타입들에 안전한 스레드 사용이 안전한 참조를 만들어서, 간단한 3단계 과정만으로 스레드 간에 객체들을 넘길 수 있습니다.

  1. ThreadSafeReference를 스레드 국한 객체로 초기화
  2. 원하는 스레드나 큐로 ThreadSafeReference 전달
  3. Realm.resolve(_:) 호출로 해당 참조를 원하는 목적지 Realm에서 푼 후, 반환 객체를 기존처럼 사용

아래 예문을 보시죠.

let realm = try! Realm()
let person = Person(name: "Jane") // no primary key required
try! realm.write {
  realm.add(person)
}
let personRef = ThreadSafeReference(to: person)
DispatchQueue(label: "com.example.myApp.bg").async {
  let realm = try! Realm()
  guard let person = realm.resolve(personRef) else {
    return // person was deleted
  }
  try! realm.write {
    person.name = "Jane Doe"
  }
}

실 사용예 🌏

RealmTasks가 오픈 소스이므로 기존 스레드 전달 코드에서 안전한 스레드 사용이 가능한 참조로 바뀐 코드를 다음 GitHub iOS PR #374에서 볼 수 있습니다.

관련 코드는 아래와 같으며, List 프로퍼티를 자동으로 중복 제거하고 백그라운드 스레드에서 쓰기 트랜젝션을 수행합니다.

realm.addNotificationBlock { _, realm in
  let items = realm.objects(TaskListList.self).first!.items
  guard items.count > 1 && !realm.isInWriteTransaction else { return }
  let itemsReference = ThreadSafeReference(to: items)
  DispatchQueue(label: "io.realm.RealmTasks.bg").async {
    let realm = try! Realm()
    guard let items = realm.resolve(itemsReference), items.count > 1 else {
      return
    }
    realm.beginWrite()
    let listReferenceIDs = NSCountedSet(array: items.map { $0.id })
    for id in listReferenceIDs where listReferenceIDs.count(for: id) > 1 {
      let id = id as! String
      let indexesToRemove = items.enumerated().flatMap { index, element in
        return element.id == id ? index : nil
      }
      indexesToRemove.dropFirst().reversed().forEach(items.remove(objectAtIndex:))
    }
    try! realm.commitWrite()
  }
}

관계 프로퍼티 정렬

지금까지는 자신이 가진 프로퍼티로만 Realm 컬렉션을 정렬할 수 있었습니다.

Realm 2.2 버전부터는 객체와 1 대 1 혹은 다 대 1 관계(to-one relationships)를 가지는 다른 객체의 속성으로 컬렉션을 정렬할 수 있습니다.

예를 들어, Person을 자신이 가진 dog 프로퍼티의 age 값으로 정렬하려면 dogOwners.sorted(byKeyPath: "dog.age")와 같이 호출하면 됩니다.

class Person: Object {
  dynamic var name = ""
  dynamic var dog: Dog?
}
class Dog: Object {
  dynamic var name = ""
  dynamic var age = 0
}

realm.beginWrite()

let lucy = realm.create(Dog.self, value: ["Lucy", 7])
let freyja = realm.create(Dog.self, value: ["Freyja", 6])
let ziggy = realm.create(Dog.self, value: ["Ziggy", 9])

let mark = realm.create(Person.self, value: ["Mark", freyja])
let diane = realm.create(Person.self, value: ["Diane", lucy])
let hannah = realm.create(Person.self, value: ["Hannah"])
let don = realm.create(Person.self, value: ["Don", ziggy])
let diane_sr = realm.create(Person.self, value: ["Diane Sr", ziggy])

let dogOwners = realm.objects(Person.self)
print(dogOwners.sorted(byKeyPath: "dog.age").map({ $0.name }))
// Prints: ["Mark", "Diane", "Don", "Diane Sr", "Hannah"]

이전 Realm 버전에서 이렇게 정렬하려면 dog.age 속성을 Person 객체에 저장하거나, Results의 장점을 포기하고 Realm 바깥에서 정렬할 수밖에 없었죠.

API 관점에서의 변화 사항을 하나 알려드립니다. 앞으로는 ‘properties’ 대신 보다 일반적인 ‘key paths’ 용어를 사용할 예정입니다.

  • 다음 Objective-C API는 이후 버전에서 더 이상 사용되지 않습니다.
Deprecated API New API
-[RLMArray sortedResultsUsingProperty:] -[RLMArray sortedResultsUsingKeyPath:]
-[RLMCollection sortedResultsUsingProperty:] -[RLMCollection sortedResultsUsingKeyPath:]
-[RLMResults sortedResultsUsingProperty:] -[RLMResults sortedResultsUsingKeyPath:]
+[RLMSortDescriptor sortDescriptorWithProperty:​ascending] +[RLMSortDescriptor sortDescriptorWithKeyPath:​ascending:]
RLMSortDescriptor​.property RLMSortDescriptor​.keyPath
  • 다음 Swift API는 이후 버전에서 더 이상 사용되지 않습니다.
Deprecated API New API
LinkingObjects​.sorted(byProperty:​ascending:) LinkingObjects​.sorted(byKeyPath:​ascending:)
List.sorted(byProperty:​ascending:) List.sorted(byKeyPath:​ascending:)
RealmCollection.sorted(byProperty:​ascending:) RealmCollection.sorted(byKeyPath:​ascending:)
Results.sorted(byProperty:​ascending:) Results.sorted(byKeyPath:​ascending:)
SortDescriptor(property:​ascending:) SortDescriptor(keyPath:​ascending:)
SortDescriptor​.property SortDescriptor​.keyPath

단, Realm 버전이 3.x에 도달할 때까지는 SemVer에 따라 이들 사용되지 않는 메서드는 계속 지원될 예정입니다.

다른 변경 사항

동기화 변경 사항 (Beta)

  • 기본 동기화 엔진이 BETA-6.5 버전으로 업그레이드됐습니다.
  • 동기화 관련 에러 리포팅 동작이 변경됐습니다. 특정 사용자나 세션에 관련이 없는 오류는 기본 동기화 엔진이 ‘치명적’(fatal)으로 분류한 경우에만 보고됩니다.

버그 픽스

  • 마지막으로 Realm 0.x로 액세스한 파일에서 1.x로 옮기거나, 1.x에서 2.x로 옮기는 경우와 같이 파일 형식 마이그레이션이 필요한 경우 deleteRealmIfMigrationNeeded를 설정하면 Realm까지 삭제됩니다.
  • 중첩 SUBQUERY 표현식을 포함한 쿼리가 수정됐습니다.
  • 이전 스레드의 RLMRealm 인스턴스가 여전히 존재하는 동안 스레드 ID가 재사용될 때 발생하던 잘못된 스레드 예외를 수정했습니다.

레거시 Swift 버전 지원

가능한 Xcode 7.3.1과 Swift 2.x를 계속 지원할 예정이지만, 조속히 모든 사용자가 Swift 3으로 마이그레이션하기를 권장합니다.


읽어 주셔서 감사합니다. 이제 Realm과 함께 멋진 앱을 만들어 보세요! Stack Overflow, GitHub, Twitter에서 저희를 만날 수 있습니다.


Realm Team

Realm의 미션은 더 나은 앱을 빠르게 개발할 수 있도록 돕는 것입니다. 이를 위해 저희는 개발자들이 실시간 협업, 가상 현실, 라이브 데이터 동기화, 오프라인 경험, 메시징 등 정교하고 강력한 기능을 쉽게 개발할 수 있도록 하는 개발 도구와 플랫폼을 제공하고 있습니다.

저희는 모바일 인터넷이 수많은 사용자와 보다 많은 디바이스가 속한 개방형 네트워크와 이들 간의 실시간 상호 작용으로 진화할 것이라고 믿으며, 개발자가 이같은 방향으로 발전할 수 있도록 돕기 위해 저희 제품들을 개발하고 있습니다.

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