문서 메뉴
문서 홈
/ /
Atlas Device SDK
/

액터와 함께 Realm 사용 - Swift SDK

이 페이지의 내용

  • 전제 조건
  • 해당 페이지의 예제에 대한 정보
  • 행위자 격리 영역 열기
  • 사용자 지정 영역 행위자 정의
  • 고립된 함수에서 영역 행위자를 동기적으로 사용
  • 비동기 함수에서 영역 행위자 사용
  • 행위자 고립 영역에 쓰기
  • 행위자 경계를 가로지르는 영역 데이터 전달
  • ThreadSafeReference 전달
  • 전송 가능한 유형 전달
  • 다른 행위자에 대한 알림 관찰
  • 관찰 제한 사항
  • 컬렉션 변경 리스너 등록
  • 객체 변경 리스너 등록

Realm Swift SDK 버전 10.39.0부터 Realm은 Swift 행위자와 함께 Realm을 사용하기 위한 기본 기능을 지원합니다. Realm의 행위자 지원은 비동기 작업을 수행하기 위해 스레드나 디스패치 대기열을 관리하는 대안을 제공합니다. 몇 가지 방법으로 행위자와 함께 Realm을 사용할 수 있습니다.

  • 행위자 격리 영역이 있는 특정 행위자에서만 영역으로 작업

  • 애플리케이션의 필요에 따라 여러 행위자에서 Realm을 사용합니다.

모든 영역 액세스를 단일 행위자로 제한하려는 경우 행위자 격리 영역을 사용할 수 있습니다. 이렇게 하면 행위자 경계를 넘어 데이터를 전달할 필요가 없어지고 데이터 레이스 디버깅을 단순화할 수 있습니다.

다양한 행위자에 대해 다양한 유형의 작업을 수행하려는 경우 여러 행위자에 걸쳐 영역을 사용할 수 있습니다. 예를 들어 메인 행위자의 객체를 읽고 싶지만 대규모 쓰기에는 배경 행위자를 사용하고자 할 수 있습니다.

Swift 행위자에 대한 일반 정보는 Apple의 행위자 문서를 참조하세요.

Swift 행위자에서 Realm을 사용하려면 프로젝트가 다음을 수행해야 합니다.

  • Realm Swift SDK 버전 10.39.0 이상 사용

  • Swift 5.8/Xcode 14.3 사용

또한 프로젝트에서 다음 설정을 사용하도록 설정하는 것이 좋습니다.

  • SWIFT_STRICT_CONCURRENCY=complete: 엄격한 동시성 검사 활성화

  • OTHER_SWIFT_FLAGS=-Xfrontend-enable-actor-data-race-checks: 런타임 행위자 데이터 레이스 감지 활성화

이 페이지의 예시에서는 다음 모델을 사용합니다.

class Todo: Object {
@Persisted(primaryKey: true) var _id: ObjectId
@Persisted var name: String
@Persisted var owner: String
@Persisted var status: String
}

Swift async/await 구문을 사용하여 영역이 열릴 때까지 기다릴 수 있습니다.

try await Realm()으로 영역을 초기화하면 메인 행위자가 격리된 영역이 열립니다. 또는 await 구문을 사용하여 영역을 열 때 행위자를 명시적으로 지정할 수 있습니다.

@MainActor
func mainThreadFunction() async throws {
// These are identical: the async init produces a
// MainActor-isolated Realm if no actor is supplied
let realm1 = try await Realm()
let realm2 = try await Realm(actor: MainActor.shared)
try await useTheRealm(realm: realm1)
}

행위자 격리 영역을 열 때 기본 구성을 지정하거나 구성을 사용자 지정할 수 있습니다.

@MainActor
func mainThreadFunction() async throws {
let username = "Galadriel"
// Customize the default realm config
var config = Realm.Configuration.defaultConfiguration
config.fileURL!.deleteLastPathComponent()
config.fileURL!.appendPathComponent(username)
config.fileURL!.appendPathExtension("realm")
// Open an actor-isolated realm with a specific configuration
let realm = try await Realm(configuration: config, actor: MainActor.shared)
try await useTheRealm(realm: realm)
}

Realm 구성에 대한 일반적인 내용은 Realm 구성 및 열기 - Swift SDK를 참조하세요.

동기화된 영역을 행위자 격리 영역으로 열 수 있습니다.

@MainActor
func mainThreadFunction() async throws {
// Initialize the app client and authenticate a user
let app = App(id: APPID)
let user = try await app.login(credentials: Credentials.anonymous)
// Configure the synced realm
var flexSyncConfig = user.flexibleSyncConfiguration(initialSubscriptions: { subs in
subs.append(QuerySubscription<Todo>(name: "all_todos"))})
flexSyncConfig.objectTypes = [Todo.self]
// Open and use the synced realm
let realm = try await Realm(configuration: flexSyncConfig, actor: MainActor.shared, downloadBeforeOpen: .always)
try await useTheSyncedRealm(realm: realm)
}

동기화된 영역 열기에 대한 자세한 내용은 동기화된 영역 구성 및 열기 - Swift SDK를 참조하세요.

특정 행위자를 정의하여 비동기 컨텍스트에서 Realm을 관리할 수 있습니다. 이 행위자를 사용하여 영역 액세스를 관리하고 쓰기 작업을 수행할 수 있습니다.

actor RealmActor {
// An implicitly-unwrapped optional is used here to let us pass `self` to
// `Realm(actor:)` within `init`
var realm: Realm!
init() async throws {
realm = try await Realm(actor: self)
}
var count: Int {
realm.objects(Todo.self).count
}
func createTodo(name: String, owner: String, status: String) async throws {
try await realm.asyncWrite {
realm.create(Todo.self, value: [
"_id": ObjectId.generate(),
"name": name,
"owner": owner,
"status": status
])
}
}
func getTodoOwner(forTodoNamed name: String) -> String {
let todo = realm.objects(Todo.self).where {
$0.name == name
}.first!
return todo.owner
}
struct TodoStruct {
var id: ObjectId
var name, owner, status: String
}
func getTodoAsStruct(forTodoNamed name: String) -> TodoStruct {
let todo = realm.objects(Todo.self).where {
$0.name == name
}.first!
return TodoStruct(id: todo._id, name: todo.name, owner: todo.owner, status: todo.status)
}
func updateTodo(_id: ObjectId, name: String, owner: String, status: String) async throws {
try await realm.asyncWrite {
realm.create(Todo.self, value: [
"_id": _id,
"name": name,
"owner": owner,
"status": status
], update: .modified)
}
}
func deleteTodo(id: ObjectId) async throws {
try await realm.asyncWrite {
let todoToDelete = realm.object(ofType: Todo.self, forPrimaryKey: id)
realm.delete(todoToDelete!)
}
}
func close() {
realm = nil
}
}

액터 분리 realm은 로컬 또는 글로벌 액터와 함께 사용될 수 있습니다.

// A simple example of a custom global actor
@globalActor actor BackgroundActor: GlobalActor {
static var shared = BackgroundActor()
}
@BackgroundActor
func backgroundThreadFunction() async throws {
// Explicitly specifying the actor is required for anything that is not MainActor
let realm = try await Realm(actor: BackgroundActor.shared)
try await realm.asyncWrite {
_ = realm.create(Todo.self, value: [
"name": "Pledge fealty and service to Gondor",
"owner": "Pippin",
"status": "In Progress"
])
}
// Thread-confined Realms would sometimes throw an exception here, as we
// may end up on a different thread after an `await`
let todoCount = realm.objects(Todo.self).count
print("The number of Realm objects is: \(todoCount)")
}
@MainActor
func mainThreadFunction() async throws {
try await backgroundThreadFunction()
}

기능이 특정 행위자에 국한되면 행위자 격리 영역을 동기적으로 사용할 수 있습니다.

func createObject(in actor: isolated RealmActor) async throws {
// Because this function is isolated to this actor, you can use
// realm synchronously in this context without async/await keywords
try actor.realm.write {
actor.realm.create(Todo.self, value: [
"name": "Keep it secret",
"owner": "Frodo",
"status": "In Progress"
])
}
let taskCount = actor.count
print("The actor currently has \(taskCount) tasks")
}
let actor = try await RealmActor()
try await createObject(in: actor)

함수가 특정 행위자에만 국한되지 않는 경우 Swift의 async/await 구문을 사용하여 Realm 행위자를 사용할 수 있습니다.

func createObject() async throws {
// Because this function is not isolated to this actor,
// you must await operations completed on the actor
try await actor.createTodo(name: "Take the ring to Mount Doom", owner: "Frodo", status: "In Progress")
let taskCount = await actor.count
print("The actor currently has \(taskCount) tasks")
}
let actor = try await RealmActor()
try await createObject()

행위자 격리형 영역은 비동기 쓰기를 위해 Swift async/await 구문을 사용할 수 있습니다. try await realm.asyncWrite { ... }를 사용하면 현재 작업이 일시 중단되고 현재 스레드를 차단하지 않고 쓰기 락(write lock)을 획득한 다음 블록을 호출합니다. Realm은 배경 스레드의 디스크에 데이터를 쓰고 완료되면 작업을 재개합니다.

위에 정의된 RealmActor 예시의 이 함수는 행위자 격리 영역에 쓸 수 있는 방법을 보여줍니다.

func createTodo(name: String, owner: String, status: String) async throws {
try await realm.asyncWrite {
realm.create(Todo.self, value: [
"_id": ObjectId.generate(),
"name": name,
"owner": owner,
"status": status
])
}
}

그리고 Swift의 비동기 구문을 사용하여 이 쓰기를 수행할 수도 있습니다.

func createObject() async throws {
// Because this function is not isolated to this actor,
// you must await operations completed on the actor
try await actor.createTodo(name: "Take the ring to Mount Doom", owner: "Frodo", status: "In Progress")
let taskCount = await actor.count
print("The actor currently has \(taskCount) tasks")
}
let actor = try await RealmActor()
try await createObject()

이렇게 하면 쓰기를 기다리는 동안 호출 스레드가 차단되지 않습니다. 호출 스레드에서 I/O를 수행하지 않습니다. 소규모 쓰기의 경우 UI를 막지 않고 @MainActor 함수에서 안전하게 사용할 수 있습니다. 복잡성 및/또는 플랫폼 리소스 제약으로 인해 앱 성능에 부정적인 영향을 미치는 쓰기는 배경 스레드에서 수행하는 것이 유리할 수 있습니다.

비동기 쓰기는 행위자가 격리된 영역 또는 @MainActor 함수에 대해서만 지원됩니다.

Realm 객체는 전송 가능하지 않으며 행위자 경계를 직접 넘을 수 없습니다. 행위자 경계를 넘어 Realm 데이터를 전달하려면 다음 두 가지 옵션이 있습니다.

  • 행위자에게 ThreadSafeReference를 전달하거나 행위자로부터 전달받습니다.

  • 값을 직접 전달하거나 행위자 경계를 넘나드는 구조체를 생성하는 등 전송이 가능한 다른 유형을 전달합니다.

객체에 액세스할 수 있는 행위자에 대해 ThreadSafeReference 를 만들 수 있습니다. 이 경우 MainActorThreadSafeReference 을 생성합니다. 그런 다음 ThreadSafeReference 를 대상 행위자에게 전달합니다.

// We can pass a thread-safe reference to an object to update it on a different actor.
let todo = todoCollection.where {
$0.name == "Arrive safely in Bree"
}.first!
let threadSafeReferenceToTodo = ThreadSafeReference(to: todo)
try await backgroundActor.deleteTodo(tsrToTodo: threadSafeReferenceToTodo)

대상 행위자에서는 쓰기 트랜잭션 내의 참조를 resolve()한 후에 사용해야 합니다. 이는 해당 행위자에 로컬한 객체 버전을 조회합니다.

actor BackgroundActor {
public func deleteTodo(tsrToTodo tsr: ThreadSafeReference<Todo>) throws {
let realm = try! Realm()
try realm.write {
// Resolve the thread safe reference on the Actor where you want to use it.
// Then, do something with the object.
let todoOnActor = realm.resolve(tsr)
realm.delete(todoOnActor!)
}
}
}

중요

ThreadSafeReference는 정확히 한 번만 해결해야 합니다. 그렇지 않으면 참조 할당이 해제될 때까지 소스 영역이 고정된 상태로 유지됩니다. 따라서 ThreadSafeReference는 수명이 짧아야 합니다.

동일한 Realm 객체를 여러 행위자에서 두 번 이상 공유해야 하는 경우, 프라이머리 키를 공유하고 이를 사용하려는 행위자에서 해당 키에 대한 쿼리를 수행하는 것을 선호할 수 있습니다. 예시는 이 페이지의 "다른 행위자의 객체에 대한 프라이머리 키 전달 및 쿼리" 섹션을 참조하세요.

Realm 객체는 전송 가능하지 않지만 행위자 경계를 넘어 전송 가능한 유형을 전달하여 이 문제를 해결할 수 있습니다. 몇 가지 전략을 사용하여 전송 가능 유형을 전달하고 행위자 경계를 넘어 데이터를 처리할 수 있습니다.

  • 완전한 Realm 객체 대신 전송 가능한 Realm 유형이나 기본 값을 전달합니다.

  • 객체의 프라이머리 키를 전달하고 다른 행위자에서 객체에 대한 쿼리를 수행합니다.

  • 구조체와 같은 Realm 객체의 전송 가능 표현을 만듭니다.

String 또는 Int 등 Realm 객체의 정보 일부만 필요한 경우 Realm 객체를 전달하는 대신 행위자 전체에 직접 값을 전달할 수 있습니다. 전송 가능한 Realm 유형의 전체 목록은 전송 가능, 전송 불가능, 스레드 제한 유형을 참고하세요.

@MainActor
func mainThreadFunction() async throws {
// Create an object in an actor-isolated realm.
// Pass primitive data to the actor instead of
// creating the object here and passing the object.
let actor = try await RealmActor()
try await actor.createTodo(name: "Prepare fireworks for birthday party", owner: "Gandalf", status: "In Progress")
// Later, get information off the actor-confined realm
let todoOwner = await actor.getTodoOwner(forTodoNamed: "Prepare fireworks for birthday party")
}

다른 행위자에서 Realm 객체를 사용하려면 프라이머리 키를 공유하고 사용하려는 행위자에서 해당 키에 대한 쿼리를 수행하면 됩니다.

// Execute code on a specific actor - in this case, the @MainActor
@MainActor
func mainThreadFunction() async throws {
// Create an object off the main actor
func createObject(in actor: isolated BackgroundActor) async throws -> ObjectId {
let realm = try await Realm(actor: actor)
let newTodo = try await realm.asyncWrite {
return realm.create(Todo.self, value: [
"name": "Pledge fealty and service to Gondor",
"owner": "Pippin",
"status": "In Progress"
])
}
// Share the todo's primary key so we can easily query for it on another actor
return newTodo._id
}
// Initialize an actor where you want to perform background work
let actor = BackgroundActor()
let newTodoId = try await createObject(in: actor)
let realm = try await Realm()
let todoOnMainActor = realm.object(ofType: Todo.self, forPrimaryKey: newTodoId)
}

단순한 값 이상으로 작업해야 하지만 ThreadSafeReferences를 전달하거나 다른 행위자에서 객체를 쿼리하는 오버헤드를 원하지 않는 경우, 행위자 경계를 넘어 전달할 데이터의 구조체 또는 기타 전송 가능 표현을 만들 수 있습니다.

예를 들어, 행위자에는 Realm 객체의 구조체 표현을 생성하는 함수가 있을 수 있습니다.

struct TodoStruct {
var id: ObjectId
var name, owner, status: String
}
func getTodoAsStruct(forTodoNamed name: String) -> TodoStruct {
let todo = realm.objects(Todo.self).where {
$0.name == name
}.first!
return TodoStruct(id: todo._id, name: todo.name, owner: todo.owner, status: todo.status)
}

그런 다음 함수를 호출하여 다른 행위자에서 데이터를 구조체로 가져올 수 있습니다.

@MainActor
func mainThreadFunction() async throws {
// Create an object in an actor-isolated realm.
let actor = try await RealmActor()
try await actor.createTodo(name: "Leave the ring on the mantle", owner: "Bilbo", status: "In Progress")
// Get information as a struct or other Sendable type.
let todoAsStruct = await actor.getTodoAsStruct(forTodoNamed: "Leave the ring on the mantle")
}

Swift의 async/await 구문을 사용하여 행위자가 격리된 영역에서 알림을 관찰할 수 있습니다.

await object.observe(on: Actor) 또는 await collection.observe(on: Actor) 호출은 객체나 컬렉션이 변경될 때마다 호출될 블록을 등록합니다.

SDK는 지정된 행위자의 실행기에서 블록을 비동기적으로 호출합니다.

다른 스레드나 다른 프로세스에서 수행된 쓰기 트랜잭션의 경우, 영역이 변경 사항이 포함된 버전으로 (자동) 새로 고침될 때 SDK가 블록을 호출합니다. 로컬 쓰기의 경우 SDK는 쓰기 트랜잭션이 커밋된 후 향후 특정 시점에 블록을 호출합니다.

다른 영역 알림과 마찬가지로 영역에서 관리하는 객체 또는 컬렉션만 관찰할 수 있습니다. 반환된 토큰은 업데이트를 확인하려는 기간 동안 보관해야 합니다.

기본 스레드나 다른 행위자에서 관찰 영역의 상태를 수동으로 진행해야 하는 경우 await realm.asyncRefresh()를 호출합니다. 이렇게 하면 Realm이 관리하는 영역과 처리되지 않은 객체가 최신 데이터를 가리키고 적용 가능한 알림을 전달하도록 업데이트됩니다.

다음과 같은 경우 .observe() 메서드를 호출할 수 없습니다.

  • 쓰기 트랜잭션 중

  • 포함 영역이 읽기 전용인 경우

  • 행위자 외부에서 행위자가 한정된 영역

SDK는 각 쓰기 트랜잭션 후에 다음과 같은 컬렉션 알림 블록을 호출합니다.

  • 컬렉션에서 객체를 삭제합니다.

  • 컬렉션에 객체를 삽입합니다.

  • 컬렉션에 있는 객체의 관리 속성을 수정합니다. 여기에는 속성을 기존 값으로 설정하는 자체 할당이 포함됩니다.

중요

알림 순서의 중요성

컬렉션 알림 핸들러에서는 항상 삭제, 삽입, 수정의 순서로 변경 사항을 적용합니다. 삭제하기 전에 삽입을 처리하면 예기치 않은 동작이 발생할 수 있습니다.

이러한 알림은 변경이 발생한 행위자에 대한 정보를 제공합니다. 행위자가 격리되지 않은 컬렉션 알림 과 마찬가지로, change 매개변수도 제공하여 쓰기 트랜잭션(write transaction) 중에 삭제, 추가 또는 수정된 객체를 보고합니다. 이 RealmCollectionChangeUITableView 의 배치 업데이트 메서드에 전달할 수 있는 인덱스 경로의 배열로 해석됩니다.

// Create a simple actor
actor BackgroundActor {
public func deleteTodo(tsrToTodo tsr: ThreadSafeReference<Todo>) throws {
let realm = try! Realm()
try realm.write {
// Resolve the thread safe reference on the Actor where you want to use it.
// Then, do something with the object.
let todoOnActor = realm.resolve(tsr)
realm.delete(todoOnActor!)
}
}
}
// Execute some code on a different actor - in this case, the MainActor
@MainActor
func mainThreadFunction() async throws {
let backgroundActor = BackgroundActor()
let realm = try! await Realm()
// Create a todo item so there is something to observe
try await realm.asyncWrite {
realm.create(Todo.self, value: [
"_id": ObjectId.generate(),
"name": "Arrive safely in Bree",
"owner": "Merry",
"status": "In Progress"
])
}
// Get the collection of todos on the current actor
let todoCollection = realm.objects(Todo.self)
// Register a notification token, providing the actor where you want to observe changes.
// This is only required if you want to observe on a different actor.
let token = await todoCollection.observe(on: backgroundActor, { actor, changes in
print("A change occurred on actor: \(actor)")
switch changes {
case .initial:
print("The initial value of the changed object was: \(changes)")
case .update(_, let deletions, let insertions, let modifications):
if !deletions.isEmpty {
print("An object was deleted: \(changes)")
} else if !insertions.isEmpty {
print("An object was inserted: \(changes)")
} else if !modifications.isEmpty {
print("An object was modified: \(changes)")
}
case .error(let error):
print("An error occurred: \(error.localizedDescription)")
}
})
// Update an object to trigger the notification.
// This example triggers a notification that the object is deleted.
// We can pass a thread-safe reference to an object to update it on a different actor.
let todo = todoCollection.where {
$0.name == "Arrive safely in Bree"
}.first!
let threadSafeReferenceToTodo = ThreadSafeReference(to: todo)
try await backgroundActor.deleteTodo(tsrToTodo: threadSafeReferenceToTodo)
// Invalidate the token when done observing
token.invalidate()
}

SDK는 각 쓰기 트랜잭션 후에 다음과 같은 객체 알림 블록을 호출합니다.

  • 객체를 삭제합니다.

  • 객체의 관리 속성을 수정합니다. 여기에는 속성을 기존 값으로 설정하는 자체 할당이 포함됩니다.

블록에는 변경된 내용에 대한 정보와 함께 요청된 행위자에게 격리된 객체의 복사본이 전달됩니다. 이 객체는 해당 행위자에 안전하게 사용할 수 있습니다.

기본적으로 객체 속성을 직접 변경하는 경우에만 알림이 생성됩니다. 연결된 객체에 대한 변경 사항은 알림을 생성하지 않습니다. nil이 아니고 비어 있지 않은 키 경로 배열이 전달되면 해당 키 경로로 식별된 속성에 대한 변경 사항만 변경 알림을 생성합니다. 키 경로는 링크 속성을 통과하여 링크된 객체의 변경 사항에 대한 정보를 수신할 수 있습니다.

// Execute some code on a specific actor - in this case, the MainActor
@MainActor
func mainThreadFunction() async throws {
// Initialize an instance of another actor
// where you want to do background work
let backgroundActor = BackgroundActor()
// Create a todo item so there is something to observe
let realm = try! await Realm()
let scourTheShire = try await realm.asyncWrite {
return realm.create(Todo.self, value: [
"_id": ObjectId.generate(),
"name": "Scour the Shire",
"owner": "Merry",
"status": "In Progress"
])
}
// Register a notification token, providing the actor
let token = await scourTheShire.observe(on: backgroundActor, { actor, change in
print("A change occurred on actor: \(actor)")
switch change {
case .change(let object, let properties):
for property in properties {
print("Property '\(property.name)' of object \(object) changed to '\(property.newValue!)'")
}
case .error(let error):
print("An error occurred: \(error)")
case .deleted:
print("The object was deleted.")
}
})
// Update the object to trigger the notification.
// This triggers a notification that the object's `status` property has been changed.
try await realm.asyncWrite {
scourTheShire.status = "Complete"
}
// Invalidate the token when done observing
token.invalidate()
}

돌아가기

SwiftUI 미리 보기와 함께 Realm 사용

다음

Swift 동시성