먹고 기도하고 코딩하라

Realm 세미나 자료 본문

앱/Swift

Realm 세미나 자료

사과먹는사람 2024. 1. 27. 16:58
728x90
728x90

 

저번 달에 진행했던 Realm과 멀티 스레딩 세미나 자료를 블로그에도 올린다.

 

 

목차

  • Realm과 멀티 스레딩
  • Realm 마이그레이션
  • try? Realm()을 하면 한 스레드에 Realm이 여러 개 생기는 거 아닙니까

 

Realm과 멀티 스레딩

전제

경험으로 알고 있듯 Realm 객체는 다른 스레드에서 사용할 수 없습니다.

You cannot share realm instances across threads.

원인

  • 다른 스레드에서 접근
    • 대상 : Realm 객체 자체 / Live Object

Realm accessed from incorrect thread (RLMException) 에러와 함께 가차없이 강종됩니다.

싱글턴으로 사용하는 Realm 객체가 메인 스레드에 선언되어 있기 때문에, 다른 스레드에서 Realm에 접근하려고 하면 문제가 됩니다. 이는 realm에서 가져온 객체(Live Object)들에도 해당됩니다.

// 싱글턴 Realm 객체의 예시
class RealmDatabaseManager {
// 생성되는 스레드에 종속됩니다
    static let realm: Realm? = try? Realm()
}
    @objc public static var localClipList: [ClipData] = []

/// realm에 저장되어있는 content의 clip list를 가져온다.
    @objc class func loadLocalClipList(code: String) {
// 이 localClipList는 [Live Object]를 참조하게 됩니다
// 물론 배열 자체는 value 타입이지만, 그 안의 Object들은 reference 타입이기 때문에...
        localClipList = RealmService.selectByContentCode(contentCode: code) ?? []
    }

해결법

  1. 근본적으로는 realm에 접근해야 할 때마다 try? Realm()으로 새로 접근하는 방법이 권장됩니다.
  2. 싱글턴으로 써야 한다면 Realm에 접근할 때마다 같은 스레드인지 확인합니다.
    • 메인 스레드에서 생성된 Realm 객체일 경우 Thread.isMainThread 로 검사해서 false면 DispatchQueue.main.async 블럭에서 실행합니다.
    • 단점
      1. select할 때, DispatchQueue.main.async 블럭 내에서 값을 받을 수 없어 컴플리션 핸들러를 사용해서 값을 따로 빼야 함 → DispatchQueue.main.sync 로 해결 가능합니다.
      2. 보기 좋지 않은 중복코드 발생할 수 있습니다.
    *// 예시*
    var userData: UserData?
    if Thread.isMainThread {
        DispatchQueue.main.async { 
            RealmService.create(user)
        }
    } else { 
        RealmService.create(user)
    }
  3. Live Object를 저장해놓고 사용하는 것을 지양합니다.
    1. Realm 접근 연산을 줄이려는 목적이라면 다른 스레드에서 접근할 가능성과 비용을 계산해 신중히 결정합니다.
    2. 정 갖고 있고 싶다면, Live Object를 복사해서 사용하면 괜찮습니다.
// RealmDatabaseManager.swift
let curClip = RealmService.selectItem(clipCode, Clip.self) ?? ClipD()
// RealmObject를 상속받는 모든 클래스는 <T>init(value: T)로 복사 객체를 만들 수 있음
let newClip = Clip(value: curClip)
let realm = try? Realm() 
let person = Person(name: "Jane") 
do { 
	try realm?.write { realm?.add(person) } 
} catch { 
	Log("\\(error)") 
} 

// person 객체에 대한 레퍼런스 선언 (스레드 간 전송해도 안전)
// @ThreadSafe 변수는 항상 옵셔널이며, 참조하는 객체가 삭제되면 @ThreadSafe 변수는 null을 참조
@ThreadSafe var personRef = person 
print("Person's name: \\(personRef?.name ?? "unknown")") 

// 다른 스레드에 person 넘기기
DispatchQueue(label: "background", autoreleaseFrequency: .workItem).async { 
	let realm = try! Realm() 
    do { 
    	try? realm.write { 
        	// 트랜잭션 동안 다른 스레드에서의 최신 변경사항이 반영되는 것이 보장됨
            // 그래서 person이 지워졌다면, personRef 또한 null이 됨
            guard let person = personRef else { return } 
            person.name = "Jane Doe" 
        }
    } catch { 
    	Log("\\(error)")
    }
}

 

 

Realm 마이그레이션

  • Realm은 Realm.Configuration 객체를 받아 생성합니다.
  • 그리고 이 configuration 객체는 schemaVersion, migrationBlock 등을 담고 있습니다.
  • 이건 간단하게 링크만 하겠습니다

왜 문제인가?

  • 문서에서 설명하는 마이그레이션은 다음 케이스를 다룹니다.
    • 자동 - 필드 추가/삭제
    • 수동(migrationBlock 사용 필요) - 필드 이름 변경, 필드에 특정 값 넣기, Object → EmbeddedObject 변경
  • 우리 프로젝트는 ClipModel → ClipData로 변경해야 하는, 즉 Object → Object로 대치해서 바꿔야 하는 요구사항이 이전에 있었습니다. (AsyncMigrator의 존재 이유)

migration 관련 꿀팁

  • migration에 중간은 없습니다.
    • 구 버전이 1, 신 버전이 4면 1 → 4 식으로 단번에 업데이트. 때문에 중간 버전이란 건 없다
    • 가차없는 편
  • migrationBlock은 Realm을 처음 참조하는 순간 호출됩니다.
    • 블록 자체는 블록으로 존재할 뿐, lazy 변수처럼 realm을 처음 호출할 때 딱 1번 호출
    • 이 말인즉슨…
      • 우리 앱의 경우 싱글턴이기 때문에 메인 스레드가 아닌 다른 스레드에서 realm을 처음 접근한다면, 그 스레드에서만 Realm 작업을 해야 합니다 (끔찍하죠)
  • migration 객체는 동적
    • migrationBlock은 migration 버전과 구버전을 매개변수로 가집니다.
    • 이 때, 이 migration과 enumerateObjects의 New 객체는 migrationBlock이 수행되는 중간에도 변경될 수 있습니다.
    static let config = Realm.Configuration(schemaVersion: 10, migrationBlock: { 
        migration, oldSchemaVersion in if oldSchemaVersion < 10 { 
            migration.enumerateObjects(ofType: ClipData.className()) { 
                _, new in new?["order"] = -1 new?["offline"] = true 
            } 
        } 
    })
  • migrationBlock 내부에서 DispatchQueue.main.async 사용을 지양합니다.
    • DispatchQueue.main.async 블럭 내부 영역은 수행 시점을 정확히 예측할 수 없으며
    • 위의 사유로 async 블럭이 수행될 때는 이미 Auto migration이 끝난 다음이라 migration 객체 혹은 enumerateObjects의 new가 변경될 가능성이 있습니다.
    • 심한 경우 new가 invalid된 것으로 처리되어 enumerate 돌다가 new 접근하면 강종됩니다.
  • migrationBlock 내부에서 Realm.objects 대신 enumerateObjects를 사용합니다.
    • migration을 시도하는 realm을 자기참조하면서 강종됩니다. (락)

 

try? Realm()을 하면 한 스레드에 Realm이 여러 개 생기는 거 아닙니까

  • 문서에서는 realm 접근이 필요할 때마다 try? Realm() 혹은 try! Realm() 해서 새로 만들어서 사용합니다.
  • 그럼 Realm 객체가 한 스레드에 여러 개 생기지는 않을까?

 

 

 

728x90
반응형
Comments