아삭아삭 iOS 개발

[TIL] Threading - Swift SDK 번역정리 본문

TIL(Today I Learned)

[TIL] Threading - Swift SDK 번역정리

바닐라머스크 2022. 10. 22. 23:58

Trady 출시 플젝 작업당시 realm으로 데이터를 관리할 때 일이었습니다.

특정 데이터를 대량으로 (9만 row) 처리할 일이 있었는데, 크기가 커서 다른 thread로 보내두고 한번에 작업하려고 했었죠.

이 때, 아무 고민없이 DispatchQueue를 사용해서 global().async 로 시도해보았지만 해당 코드는 에러를 마구마구 쏟아냈고..

시간 또한 목표 출시마감일 새벽, 출시 버튼 누르기 직전이었기에

우선 해당 대량 relam 데이터 작업은 (임시방편으로) 아예 다른 화면에서 작업하도록 했었습니다 :(

 

그래서!!!

이번 기회에 realm의 threading 작업에 대해 공부해보려고 합니다.

 

해당 공식 문서부터 살펴보았는데요, 죄다 영어고 관련 자료가 많이 없어서

겸사겸사 공식문서를 정독하면서 번역 정리해보았습니다.

 

(※ 잘못된 번역이 있거나 틀린 내용이 있을 수 있습니다)

 

https://www.mongodb.com/docs/realm/sdk/swift/crud/threading/

 

Threading - Swift SDK — Realm

A realm instance is designed to work with one version at a time, not several different versions. Consider what Realm Database would have to do to support working with several different versions at once: it would need to store a potentially enormous graph t

www.mongodb.com

 

 개요 

iOS 앱을 빠르게 동작하도록 하려면 시각적 요소 배치&사용자 상호작용 등을 처리하는 데 필요한 시간과

데이터 처리&비즈니스 로직 실행 등을 처리하는데 필요한 시간의 균형을 맞추어야 한다.

일반적으로 앱 개발자들은 이 작업을 여러 스레드에 분산시킨다.

사용자 상호작용과 관련된 모든 작업들은 UI thread 혹은 메인 thread에서 처리하고,

보다 무거운 작업들은 화면에 나타내기 위해 UI thread로 보내기 전에 한 개 혹은 더 많은 background thread로 보낸다.

무거운 작업들을 background thread로 떠넘김으로써 UI thread는 작업 사이즈와 무관하게 높은 반응성을 유지할 수 있다.

하지만 deadlocking이나 race conditions와 같은 문제없이 thread-safe하고, 좋은 성능에, 유지 관리까지 할 수 있는 multithread한 코드를 짜는 것은 매우 어려울 수 있다.

Realm은 개발자를 위해 이를 단순화한다.

 

💡TIP
버전 10.26.0 기준, Realm은 background write을 위한 비동기 write 메서드를 제공한다.
(참고 : 아래 'Perform a Background Write')
비동기 write에서는 thread간 frozen objects나 thread-safe reference를 전달할 필요가 없다!

 

기억해야 할 세 가지 규칙

아래 세 가지 규칙을 따른다면 Realm의 간단하고 안전한 multithreaded 코드가 가능하다.

 

  1. Don't lock to read
     Realm 데이터베이스의 MVCC(Multiversion Concurrency Control; 다중 버전 동시성 제어) 아키텍처는 읽기 실행을 위해 lock을 할 필요성을 없앤다.
    이 때 읽은 값은 절대로 손상되거나 일부분 수정된 상태가 되지 않는다.
    locks나 mutexes(mutual exclusion objects) 없이도 어떤 thread에서든지 realms에서 자유롭게 읽을 수 있다.
    불필요한 locking은 각 thread가 읽기 전 자기 차례가 올 때까지 기다려야 하기 때문에 병목현상으로 이어질 수 있다.


  2. Avoid writes on the UI thread if you write on a background thread
     개발자는 어느 thread에서든지 realm에 write(CRUD 작업을 의미) 할 수 있지만 한 번에 한 가지 writer만 있을 수 있다.
    결과적으로 write 트랜잭션들은 서로를 차단한다.
    UI thread에서의 write은 background thread에서의 write이 완료될 때까지 앱이 응답하지 않는 것을 야기할 수 있다.
    Device Sync를 사용할 경우, background thread에 동기 write를 할 때 UI thread에는 작업하지 않도록 해라.


  3. Don't pass live objects, collections, or realms to other threads
     live objects, collections, realm 인스턴스들은 모두 thread로 제한된다.
    이 말은, 위 요소들은 모두 각각 생성된 thread에서만 유효하다는 말이다.
    실제로 이 말은 live 인스턴스들을 다른 thread들로 전달할 수 없음을 의미한다.
    하지만 Realm 데이터베이스는 thread간 객체를 공유할 수 있는 몇 가지 메커니즘을 제공한다


💡TIP
이 자료도 보세요! : Realms: File Size
(해당 자료는 아직 안봐서 정리 안함)

 


 

 Perform a Background Write 

(버전 10.26.0. 신규 내용)

writeAsync를 사용하여 background에서 객체를 추가, 수정, 삭제할 수 있다.

 

비동기 write작업에서는 thread-safe reference나 frozen objects들을 thread간 전달할 필요가 없다.

대신, realm.writeAsync를 호출한다.

write작업이 성공하거나 실패한 이후, source thread에서 실행할 메서드를 completion block으로 제공할 수 있다.

 

background write 작업시 고려해야할 사항 :

  • 비동기 writes는 realm을 닫거나 무효화하는 것을 차단한다.
  • 트랜잭션을 명시적으로 커밋 혹은 취소할 수 있다.
// 공식문서 예시

let realm = try! Realm()

// Query for a specific person object on the main thread
let people = realm.objects(Person.self)
let thisPerson = people.where {
    $0.name == "Dachary"
}.first

// Perform an async write to add dogs to that person's dog list.
// No need to pass a thread-safe reference or frozen object.
realm.writeAsync { // 여기가 writeAsync!!!
    thisPerson?.dogs.append(objectsIn: [
        Dog(value: ["name": "Ben", "age": 13]),
        Dog(value: ["name": "Lita", "age": 9]),
        Dog(value: ["name": "Maui", "age": 1])
    ])
} onComplete: { _ in // 여기가 completion block!!!
    // Confirm the three dogs were successfully added to the person's dogs list
    XCTAssertEqual(thisPerson!.dogs.count, 3)
    // Query for one of the dogs we added and see that it is present
    let dogs = realm.objects(Dog.self)
    let benDogs = dogs.where {
        $0.name == "Ben"
    }
    XCTAssertEqual(benDogs.count, 1)
}

 

Wait for Async Writes to Complete

 

SDK는 realm이 현재 비동기 write 작업을 수행하고 있는지에 대한 Bool값을 제공한다. isPerformingAsynchronousWriteOperations 변수는 아래 3개 중 하나를 호출하고 나면 true가 된다.

  • writeAsync
  • beginAsyncWrite
  • commitAsyncWrite

 

모든 스케쥴된 비동기 write 작업이 완료될 때까지 true값을 유지한다.

이 값이 true일 동안에는 realm을 닫거나 무효화하는 것을 차단한다.

 

 

Commit or Cancel an Async Write

 

비동기 write을 완료하기 위해 개발자 또는 SDK는 다음 중 하나를 호출해야 한다.

  • commitAsyncWrite
  • cancelAsyncWrite

writeAsync 메서드를 사용할 때, SDK는 트랜젝션을 커밋하거나 취소한다.

이는 객체의 범위에 연결된 상태를 수동으로 유지할 필요가 없도록 비동기 write작업을 편리하게 한다.

하지만 writeAsync 블록에 있는 동안에는 명시적으로 commitAsyncWrite 또는 cancelAsyncWrite 호출할 수 있다.

만약 이런 메서드들을 호출하지 않고 return을 하는 경우, writeAsync는 다음 중 하나로 이어진다.

  • write 블록의 명령을 수행하고 해당 작업을 커밋한다.
  • 에러를 반환한다.

 

위 두 개의 케이스 모두 writeAsync 실행을 완료한다.

 

 

비동기 write 트랜젝션을 언제 커밋하거나 취소할지에 대해 잘 제어하려면 beginAsyncWrite 메서드를 사용해라.

이 메서드를 사용할 때, 반드시 명시적으로 트랜젝션을 커밋해주어야 한다.

비동기 write을 커밋하지 않고 반환할 경우, 트랜젝션이 취소된다.

beginAsyncWritecancelAsyncWrite에 전달할 수 있는 ID를 반환한다.

 

commitAsyncWrite는 비동기적으로 write 트랜젝션을 커밋한다.

이는 데이터를 realm에 유지하는 단계이다.

commitAsyncWriteonComplete 블록을 사용할 수 있다.

해당 블록은 커밋이 완료되거나 실패하고 나면 소스 thread에서 실행된다.

 

commitAsyncWrite를 호출하면 즉시 반환된다.

이는 SDK가 background thread에서 I/O를 실행하는 동안 호출자가 계속 진행할 수 있도록 한다.

이 메서드는 cancelAsyncWrite에 전달할 수 있는 ID를 반환한다.

이는 커밋 자체를 취소하는게 아니고, 완료 블록에서 보류중인 호출을 취소한다.

 

commitAsyncWrite에 대한 순차적인 호출을 그룹화할 수 있다.

특히 일괄 트랜잭션들이 작을 경우, 이 호출들을 일괄적으로 처리하는 것은 write 성능을 향상시킨다.

그룹화 트랜잭션을 허용하려면 isGroupingAllowed 매개변수를 true로 설정해라.

 

beginAsyncWrite 또는 commitAsyncWrite에서 cancelAsyncWrite을 호출할 수 있다.

→ beginAsyncWrite에서 호출할 경우, write 트랜젝션 전체를 취소해버린다.

→ commitAsyncWrite에서 호출할 경우, commitAsyncWrite에 전달할 수 있는 onComplete 블록만 취소되며 커밋 자체가 최소되는 것은 아니다. 취소하려는 commitAsyncWrite 또는 beginAsyncWrite의 ID가 필요하다.

 


 

⭐️ Communication Across Threads ⭐️

 

여러 스레드에서 (별도의 realm 인스턴스들로동일한 realm을 열 수 있다.

 realm 인스턴스들을 처음 열었던 thread에서 해당 realm 인스턴스들로 자유롭게 읽고 쓸 수 있다.

다중 thread 환경에서 realm 데이터베이스를 작업할 때 중요한 규칙들 중 하나는 객체들이 thread로 제한된다는 것이다.

다른 thread에서 생성된 객체나 collection, realm의 인스턴스들에는 접근할 수 없다.

Realm 데이터베이스의 다중 버전 동시성 제어(MVCC) 아키텍처는 시기별로 특정 객체에 대해 여러 활성화된 버전들이 존재할 수 있음을 의미한다.

thread 제한은 해당 thread의 모든 인스턴스들이 동일한 내부 버전임을 보장한다.

 

ℹ️ NOTE

One Version at a Time

realm 인스턴스는 한 번에 여러개의 다른 버전들이 아닌, 한 가지 버전으로 작업되도록 설계되어 있다.
Realm Database가 한 번에 여러개의 다른 버전들로 작업하려면 무엇을 해야 하는지 한 번 생각해봐라.
realm 인스턴스를 내부적으로 다른 객체 버전들로 조정하려면 잠재적으로 거대한 그래프를 저장해야 할 것이다.
이를 고려하면, thread간 live 인스턴스들을 전달하는데에 제한을 두는 것이 합리적으로 보인다.
이러한 설계는 결과적으로 보다 성능이 좋고, 공간 효율적이며, 상대적으로 단순한 구현을 가능하게 한다.

( → 한 번에 한가지 버전을 갖는게 메모리적으로나 기능적으로나 더 효율적이라는 말로 이해했다)

 

 

thread간 커뮤니케이션이 필요할 경우, 사용 케이스에 따라 몇 가지 옵션을 쓸 수 있다.

  • 두 개의 thread간 데이터를 작업하려면 양 thread의 object를 query하거나, thread간 ThreadSafeReference를 전달한다.
  • thread의 변경 사항에 대응하기 위해서는 Realm Database의 notifications를 사용해라.
  • 현재 thread의 realm에 있는 다른 thread에서의 변경사항을 확인하기 위해서는 realm 인스턴스를 새로고침해라.
  • 다른 thread들로 객체의 빠른 읽기 전용 보기를 전달하려면 객체를 ‘freeze’해라.
  • 앱에서 객체의 여러 개의 읽기 전용 보기를 유지하거나 공유하기 위해서는 realm으로부터 객체를 복사해라.

 

 

Create a Serial Queue to use Realm on a Background Thread

 

Realm을 background thread에서 사용할 경우, serial queue(직렬 대기열)를 생성해라.

Realm Database는 global() queue와 같은 concurrent queues(동시 대기열)에서의 realm 사용을 지원하지 않는다.(이런😢)

 

// 공식문서 예시

// Initialize a serial queue, and
// perform realm operations on it

let serialQueue = DispatchQueue(label: "serial-queue")
serialQueue.async { // 직렬, 비동기
    let realm = try! Realm(configuration: .defaultConfiguration, queue: serialQueue)
    // Do something with Realm on the non-main thread
}

 

 

Pass Instances Across Threads

 

Realm, Results, List, 관리 객체들은 스레드 제한적이다.

이 말은 이 요소들이 생성된 thread에서만 해당 요소들을 사용할 수 있음을 뜻한다.(이런😢)

 

 

Sendable Conformance

 

(버전 10.20.0 신규사항) : @ThreadSafe wrapper와 ThreadSafeReference는 Sendable을 충족한다.

만약 Swifit5.6 이상을 사용할 경우, @ThreadSafe property wrapperThreadSafeReference 모두 Sendable을 충족한다.

 

(  Sendable이라는 프로토콜이 있습니다. 복사를 통해 동시성 도메인간 안전하게 전달이 가능하다고 하네요. 공식문서 참고해주세요!)

https://developer.apple.com/documentation/swift/sendable

 

Apple Developer Documentation

 

developer.apple.com

 

 

Use the @ThreadSafe Wrapper

 

(버전 10.17.0 신규사항)

다음과 같이 thread 제한적인 인스턴스들을 다른 thread로 전달할 수 있다.

  1. @ThreadSafe 속성 래퍼를 사용해서 기존 개체를 참조하는 변수를 선언해라.
    정의에 따르면, @ThreadSafe-wrapped 변수들은 항상 옵셔널이다.
  2. 다른 thread로 @ThreadSafe-wrapped 변수를 전달해라.
  3. @ThreadSafe-wrapped 변수를 옵션처럼 사용해라.
    만약 realm에서 참조된 객체가 삭제되면, 해당 객체를 참조한 변수는 nil이 된다.
// 공식문서 예시

let realm = try! Realm()

let person = Person(name: "Jane")
try! realm.write {
    realm.add(person)
}

// Create thread-safe reference to person
// 스레드 안전한 참조 변수 생성
@ThreadSafe var personRef = person

// @ThreadSafe vars are always optional. If the referenced object is deleted,
// the @ThreadSafe var will be nullified.
print("Person's name: \(personRef?.name ?? "unknown")") // 옵셔널이라 기본값을 미리 작성해둠

// Pass the reference to a background thread
DispatchQueue(label: "background").async {
    autoreleasepool {
        let realm = try! Realm()
        try! realm.write {
            // Resolve within the transaction to ensure you get the
            // latest changes from other threads. If the person
            // object was deleted, personRef will be nil.
            guard let person = personRef else {
                return // person was deleted
            }
            person.name = "Jane Doe"
        }
    }
}

 

다른 thread의 객체로 작업하는 또 다른 방법은 해당 thread에서 그 객체를 다시 query하는 것이다.

하지만 그 객체에 primary key가 없다면 해당 객체를 query하는 것은 그리 간단하지 않다.

이 때 primary key 보유여부와 상관없이 @ThreadSafe wrapper는 어느 객체든지 사용할 수 있다.

또한 @ThreadSafe wrapper는 함수 매개변수, 구조체나 클래스의 프로퍼티, results, lists에도 사용할 수 있다.

 

🧪EXAMPLE

아래 예시는 함수 매개변수에 @ThreadSafe를 어떻게 사용하는지 보여준다.
이것은 비동기적으로 또는 다른 thread에서 실행될 수도 있는 함수에 유용하다.
// 공식문서 예시

// person 이라는 매개변수 앞에 @ThreadSafe 키워드를 붙여줌
func loadNameInBackground(@ThreadSafe person: Person?) async {
    let newName = await someLongCallToGetNewName()
    let realm = try! await Realm()
    try! realm.write {
        person?.name = newName
    }
}

let realm = try! await Realm()

let person = Person(name: "Jane")
try! realm.write {
    realm.add(person)
}
await loadNameInBackground(person: person)​

 

 

Use ThreadSafeReference (Legacy Swift / Objective-C)

 

Realm Swift SDK 버전 10.17.0 이전 또는 Objective-C 에서는,

아래와 같이 thread 제한적인 인스턴스들을 다른 thread로 전달할 수 있다.

  1. thread 제한적인 객체로 ThreadSafeReference를 초기화해라
  2. reference를 다른 thread나 queue로 전달해라
  3. Realm.resolve(_:)를 호출함으로써 다른 thread의 realm에 대한 reference를 확인해라.
    반환된 객체를 정상적으로 사용해라.

 

ℹ️ IMPORTANT

반드시 ThreadSafeReference는 정확히 한 번만 확인해야 한다.
그렇지 않으면, 그 reference가 할당 해제될 때까지 source realm은 고정된 상태로 유지될 것이다.
이를 근거로, ThreadSafeReference는 수명이 짧도록 해야 한다.

 

// 공식문서 예시

let person = Person(name: "Jane")
let realm = try! Realm()

try! realm.write {
    realm.add(person)
}

// Create thread-safe reference to person
// ThreadSafeReference로 참조객체 생성
let personRef = ThreadSafeReference(to: person)

// Pass the reference to a background thread
DispatchQueue(label: "background").async {
    autoreleasepool {
        let realm = try! Realm()
        try! realm.write {
            // Resolve within the transaction to ensure you get the latest changes from other threads
            // 여기서 resolve는 한 번만 해야 함 주의!
            guard let person = realm.resolve(personRef) else {
                return // person was deleted
            }
            person.name = "Jane Doe"
        }
    }
}

 

 

다른 thread의 객체로 작업하는 또 다른 방법은 해당 thread에서 그 객체를 다시 query하는 것이다.

하지만 그 객체에 primary key가 없다면 해당 객체를 query하는 것은 그리 간단하지 않다.

이 때 primary key 보유여부와 상관없이 ThreadSafeReference는 어느 객체든지 사용할 수 있다.

또한 ThreadSafeReferenceresults 그리고 lists에도 사용할 수 있다.

 

ThreadSafeReference의 단점은 약간의 boilerplate가 필요하다는 것이다.

객체들이 background thread에 남아 있지 않도록 모든 객체들을 autoreleasepool로 로 감싸주어야 한다.

그래서 다음과 같이 boilerplate를 다루는 편리한 extension을 만들면 도움이 된다.

 

// 공식문서 예시

extension Realm {
    func writeAsync<T: ThreadConfined>(_ passedObject: T, errorHandler: @escaping ((_ error: Swift.Error) -> Void) = { _ in return }, block: @escaping ((Realm, T?) -> Void)) {
        
        let objectReference = ThreadSafeReference(to: passedObject) // thread-safe한 참조객체 생성
        let configuration = self.configuration
        DispatchQueue(label: "background").async { // 인스턴스를 background 스레드로 전달한다
            autoreleasepool { // autoreleasepool로 객체를 감싸주기
                do {
                    let realm = try Realm(configuration: configuration)
                    try realm.write {
                        // Resolve within the transaction to ensure you get the latest changes from other threads
                        let object = realm.resolve(objectReference)
                        block(realm, object)
                    }
                } catch {
                    errorHandler(error)
                }
            }
        }
    }
}

 

 

위와 같은 확장자는 Realm 클래스에 writeAsync() 메서드를 추가한다.

이 메서드는 인스턴스를 background thread로 전달한다.

 

🧪EXAMPLE

이메일 앱을 만들었고, background에서 읽은 이메일들은 모두 삭제하려 한다고 가정해보자.
코드 단 두 줄로 이를 구현할 수 있다!
클로저는 background thread에서 실행되며 realm과 전달된 객체 모두 자체 버전으로 받는다는 것을 주의해라.

// 공식문서 예시

let realm = try! Realm()

// 읽은 메일들만 필터링
let readEmails = realm.objects(Email.self).where {
    $0.read == true
}

// writeAsync 메서드를 사용하여
realm.writeAsync(readEmails) { (realm, readEmails) in
    guard let readEmails = readEmails else {
        // Already deleted
        return
    }
    realm.delete(readEmails) // 읽은 메일들은 삭제함
}​

 

 

Use the Same Realm Across Threads

 

thread간 realm 인스턴스들을 공유할 수 없다.(이런😢)

thread간 동일한 Realm 파일을 사용하려면, 각각의 thread에서 다른 realm 인스턴스를 열어야 한다.

동일한 configuration을 사용하는 한 모든 Realm 인스턴스들은 디스크의 동일한 파일에 map된다.

 

 

Refreshing Realms

 

realm을 열면, 해당 realm은 가장 최근에 성공한 write 커밋을 반영하며 refresh되기 전까지 해당 버전에 남아있다.

이 말은 다음 새로고침이 있을 때까지 다른 thread에서 발생한 변경사항들을 realm이 알 수 없음을 뜻한다.(반영이 안된다는 말)

UI thread에 있는 realm은 (정확히 말하자면, 모든 event loop thread에 있는) 해당 thread의 loop가 시작될 때 자동으로 자체적인 새로고침을 한다.

반면, loop threads에 존재하지 않거나 자동 새로고침이 불가능한 realm 인스턴스들은 아래처럼 수동으로 새로고침을 해주어야만 한다.

 

// 공식문서 예시
if (!realm.autorefresh) {
   // Manually refresh
   realm.refresh()
}

 

 

Frozen Objects

 

(정리중)

 


 

 

 Realm's Threading Model in Depth 

 

Realm 데이터 베이스는 MVCC(다중버전 동시 제어) 아키텍처로 인해 thread간 안전하고, 빠르고, lock-free하고, 동시 접근이 가능하다.

 

Compared and Contrasted with Git

만약 Git과 같은 분산 버전 제어 시스템에 익숙하다면, MVCC에 대한 직관적인 이해가 있을 것이다.

Git의 두 가지 근본적인 요소는 아래와 같다.

  • 커밋들은 각각 별개의 write들이다. (=atomic writes)
  • 브랜치들은 커밋 내역의 다른 버전들이다.

Realm 데이터베이스 또한 마찬가지로, 트랜잭션 형태의 write은 원자적으로 커밋된다.(=atomically-committed)

Realm 데이터베이스는 브랜치들처럼 주어진 시간별 여러개의 다른 버전들을 가진다.

 

하지만 forking을 통해 배포와 분산을 적극적으로 지원하는 Git과 다르게,

realm은 주어진 시간별 오직 하나의 최신 버전을 갖고 있으며 항상 해당 최신 버전의 head에 writes한다.

즉, Realm 데이터베이스는 이전 버전에 write 작업을 할 수 없다.

그래서 데이터는 단 하나의 최신 버전에 수렴되어야 한다.

 

Internal Structure

realm은 B+ tree 데이터 구조를 사용하여 구현된다.

최상위 노드는 realm의 버전을 나타내며, 하위 노드들은 해당 realm의 버전에 있는 객체들이다.

Git이 HEAD 커밋에 대한 포인터를 갖는 방법과 마찬가지로 realm은 가장 최신 버전에 대해 포인터를 갖는다.

 

Realm 데이터베이스는 격리 및 내구성을 보장하기 위해 copy-on-write 기술을 사용한다.

변경사항을 만들 경우, Realm 데이터베이스는 writing을 위해 tree의 관련 부분을 복사한다.

그리고 Realm 데이터베이스는 해당 변경사항들을 두 단계로 커밋한다.

  • Realm 데이터베이스는 변경사항들을 disk에 기록하고 성공을 확인한다.
  • 그러고 나서 Realm 데이터베이스는 새로 작성된 버전을 가리키도록 최신 버전 포인터를 설정한다.

이 두 단계의 커밋 과정은 write이 중간에 실패하더라도 원본 버전이 손상되지 않도록 보장한다.

왜냐하면 변경 사항들은 tree의 관련 부분의 복사본에 생성되었기 때문이다.

마찬가지로, realm의 root 포인터는 신규 버전이 유효하다고 보장될 때까지는 원본 버전을 가리킨다.

 

(공식문서 예시)
아래의 다이어그램은 커밋 과정을 나타낸다.
1. realm은 나무처럼 구조화되어 있다.
   realm은 최신 버전인 V1을 가리키는 포인터가 있다.

2. writing을 할 때, Realm 데이터베이스는 V1을 기반으로 새로운 버전 V2를 생성한다.
    Realm 데이터베이스는 수정을 위해 객체들의 복사본(A1, C1)을 만든다.
    반면, 수정되지 않은 객체들에 대한 연결은 계속 기존 버전들(B, D)을 가리킨다.

3. 커밋 유효성 검증 이후, Realm 데이터베이스는 realm의 포인터가 새로운 최신 버전인 V2를 가리키도록 업데이트한다.
    Realm 데이터베이스는 더이상 tree와 연결되지 않는 이전 노드들(A, R, C)을 버린다.

 

Realm 데이터베이스는 데이터를 처리하기 위해 memory mapping과 같은 zero-copy 기술들을 사용한다.

realm으로부터 값을 읽을 때 해당 값의 복사본이 아니라 실제 디스크의 값을 보고 있는 것이다.

이것이 live 객체들의 기본이다.

그래서 realm의 헤드 포인터가 (디스크에 write의 유효성을 검사한 이후에) 새로운 버전을 가리키도록 설정될 수 있다.

 


 

 

 Summary 

 

  • Realm은 아래 세 가지 규칙을 지킬 경우, 단순하고 안전한 다중 스레드 코드를 가능하게 한다.
    • 읽기를 위해 잠그지 마라…? (Don’t lock to read)
    • 만약 background thread에 write 작업을 하거나 Device Sync 작업을 할 경우, UI thread에 write 작업하는 것은 피해라.
    • thread간 live 객체를 전달하지 마라
  • 케이스별로 thread간 객체를 전달하는 적절한 방법이 있다.
  • realm 인스턴스의 다른 thread들에서 발생한 변경 사항들을 확인하려면,
    자동 refresh가 불가능하거나 “loop” thread에 존재하지 않는 realm 인스턴스들을 수동으로 refresh해 주어야만 한다.
  • 반응형, 이벤트 기반 아키텍처를 기반으로 하는 앱들의 경우,
    서로 다른 thread에 얕은 복사본을 효율적으로 전달하여 처리하기 위해 객체들/collections/realms들을 고정할 수 있다.
  • Realm 데이터베이스의 MVCC 아키텍쳐는 Git의 아키텍처와 비슷하다.
    하지만 Git과 다르게, Realm 데이터베이스는 각 realm별로 단 한 개의 가장 최신 버전만 갖는다.
  • Realm 데이터베이스는 독립과 내구성을 보장하기 위해 두 단계로 커밋한다.

 


 

 

한글로 읽어도 무슨 말인지 아직 감이 잘 안잡히네요😂

 

그래도 MVCC 아키텍처 개념 부분을 읽고나니, 지난주에 배웠던 realm의 migration 작동 구조에 대해 좀 더 명확하게 이해가 되는 듯해 재밌었습니다.

아키텍처 관련 종류들은 처음 들어보는데 여러 종류가 있는 것 같아 더 공부해보고 싶네요.

 

무튼 realm threading은 자료를 더 찾아봐고 추가 포스팅을 차차 해볼 예정입니다.

참고용으로만 봐주세요! :)

 

'TIL(Today I Learned)' 카테고리의 다른 글

[TIL] 2022.08.3 (SeSAC iOS)  (0) 2022.08.03
[TIL] 2022.08.2 (SeSAC iOS)  (0) 2022.08.02
[TIL] 2022.07.30~31 (SeSAC iOS)  (0) 2022.07.31
[TIL] 2022.07.29 (SeSAC iOS)  (0) 2022.07.29
[TIL] 2022.07.28 (SeSAC iOS)  (0) 2022.07.28