출시 프로젝트 중 API 통신으로 받아온 데이터를 앱 내부에 저장해놓고 사용해야 했는데
Realm SDK를 사용해서 데이터 모델을 설계했던 경험을 정리한 글이다.
1. 클래스
Realm은 DB에 저장할 타입으로 클래스를 사용한다.
구조체를 사용하지 않는 가장 큰 이유는 "live"에 포커스를 맞춰서 그렇다고 한다.
Realm DB에 저장하려는 클래스는 Object
클래스를 상속해야하고
내부 프로퍼티에 @Persisted
라는 property wrapper
속성을 사용한다.
class Place: Object {
@Persisted(primaryKey: true) var contentId: Int
@Persisted var image: String
}
위의 Place 클래스는 DB에서 하나의 테이블처럼 취급되고 자동으로 Realm Scheme에 등록된다.
이 클래스의 인스턴스는 하나의 레코드이며 내부 프로퍼티들은 테이블의 컬럼에 해당된다.
기본키로 등록된 컬럼(contentId)은 테이블 내에서 중복된 값을 가질 수 없기 때문에
해당 테이블의 레코드를 조회할 때 주로 사용한다.
2. EmbeddedObject
EmbeddedObject
를 상속한 클래스는 별도의 테이블로 등록되지 않고
다른 Object
를 상속한 클래스의 프로퍼티로서 사용된다.
따라서 EmbeddedObject
를 상속한 클래스는 독립적으로 존재할 수 없고
자신을 프로퍼티로 갖는 다른 클래스와 같은 생명주기를 갖는다.
class Place: Object {
@Persisted(primaryKey: true) var contentId: Int
@Persisted var image: String
@Persisted var intro: Intro?
}
class Intro: EmbeddedObject {
@Persisted var zipcode: Int
@Persisted var tel: String
}
Place 클래스가 Intro 클래스를 내부 프로퍼티로 갖고 있으므로 구조상 테이블이 중첩된 형태이지만
Place 객체가 사라질 때 Intro 객체도 자동으로 함께 삭제된다.
그리고 EmbeddedObject
는 반드시 테이블 내에서 Optional
타입으로 선언되어야 한다.
그렇지 않으면 오류가 발생한다.
3. Codable
위에서 살펴본 Realm Object 객체를 생성할 때 편의 초기자를 사용해서 원하는 값으로 커스텀할 수 있다.
class Place: Object {
@Persisted(primaryKey: true) var contentId: Int
@Persisted var image: String
convenience init(id: Int, url: String){
self.init()
contentId = id
image = url
}
}
만약 네트워크 통신으로 받아온 데이터(ex. json)를 Codable
을 사용해서 바로 Realm Object로
Decoding하고 싶다면 아래처럼 init(from decoder:)
초기자를 구현해주면 된다.
class Place: Object, Codable {
@Persisted(primaryKey: true) var contentId: Int
@Persisted var image: String
enum CodingKeys: String, CodingKey {
case contentId = "contentid"
case image = "firstimage"
}
required convenience init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
contentId = Int(try container.decode(String.self, forKey: .contentId)) ?? 0
image = try container.decode(String.self, forKey: .image)
}
}
Place 타입으로 Decoding 할 때 init(from decoder:)
초기자가 호출되고 내부에서 1단계 초기화가 끝나면
CodingKeys에 설정된 키를 사용해 Decoding이 과정이 이뤄지면서 각 프로퍼티에 값이 저장된다.
이 초기자에서 응답으로 받아온 데이터와 다른 타입으로 변환할 수도 있다.
위 예시에서 "contentid" 키에 해당하는 값이 String 타입이지만 변환되는 클래스에는 Int 타입으로 저장했다.
4. 배열
테이블에 배열을 저장하려면 List
라는 Realm에서 정의된 타입을 사용해야 한다.
List
는 Swift의 Array처럼 제네릭 타입으로 Int, Bool, String 등 여러 기본 타입을 원소로 가질 수 있고
다른 Realm Object를 담는 것도 가능하지만 Swift와 다르게 참조 타입이다.
편의를 위해 클래스에 연산 프로퍼티를 정의하여 Swift의 Array처럼 사용할 수도 있다.
class Place: Object {
@Persisted(primaryKey: true) var contentId: Int
@Persisted var imageList: List<String>
var images: [String] {
get { imageList.map { $0 } }
set {
imageList.removeAll()
imageList.append(objectsIn: newValue)
}
}
}
5. Realm 파일
앱에서 Realm을 사용하려면 먼저 Realm 파일을 열어줘야 한다.
이 때 Configuration
값을 변경해서 기본 경로가 아닌 특정 경로에 있는 Realm 파일을 열거나
Realm Scheme에 직접 클래스를 추가할 수도 있다.
private let localRealm: Realm = {
var config = Realm.Configuration.defaultConfiguration
config.deleteRealmIfMigrationNeeded = true
return try! Realm(configuration: config)
}()
deleteRealmIfMigrationNeeded
는 true
일 경우 Realm 파일에 저장된 테이블과 현재 테이블이 다를 때
즉, 스키마가 다를 때 기존의 Realm 파일을 삭제하고 다시 만들어준다.
개발 중에 스키마가 달라질 때 마다 앱을 삭제하고 다시 설치하거나 마이그레이션할 필요가 없어서 편하지만
반드시 디버깅 모드에서만 활성화 하는 것을 권장하고 있다.
NOTE
이 설정을 활성화한 상태이고 Realm Studio가 열려 있다면 Realm 파일이 삭제되는 과정에서 오류가 발생한다.
6. 이슈
1) 제네릭 Object 클래스
테이블을 만들다 보니 하나의 클래스 구조를 여러 Object 클래스에 사용할 수 있을 것 같아서
제네릭 타입의 Object 클래스를 선언하고 여러 타입의 테이블을 만들었다.
class SomeClass<T: EmbeddedObject>: Object {
@Persisted var key: String?
@Persisted var type: T?
}
그런데 Realm에서 테이블을 인식하지 못하는 문제가 발생했다.
찾아본 글에 따르면 Realm은 Objective-C
와 호환 가능한 멤버만 노출시키는데
제네릭은 호환되지 않아서 그렇다고 한다.
2) 백그라운드 스레드 트랜잭션
네트워크 응답을 백그라운드 스레드에서 처리할 때 동시에 Realm 트랜잭션을 수행했는데
아래와 같은 런타임 오류 메시지가 출력되며 앱이 강제 종료됐다.
cannot schedule async transaction. make sure you are running from inside a run loop
Realm 문서에나와있는 writeAsync
메서드를 사용해봤지만 효과가 없었다.
공식 문서에는 백그라운드 스레드에서 쓰기가 가능하다고 돼있는데 구글에 검색해보니
나와 같은 문제를 겪고 있는 사람들이 꽤 있었다.
원인을 찾지 못했지만 최근에 추가된 기능이라서 오류가 있을 수 있다고 생각했다.
결국 모든 트랜잭션을 메인 스레드에서 수행했고 이후 오류는 발생하지 않았다.
참고 문서
https://github.com/realm/realm-swift/issues/7812
https://stackoverflow.com/questions/54892943/can-not-add-class-with-generic-type-to-swift-realm
'IOS' 카테고리의 다른 글
[IOS] UIGestureRecognizer 사용해서 지도 조작하기 (0) | 2022.10.19 |
---|---|
[IOS] API 통신에서 겪은 SSL 인증서 및 ATS 관련 이슈에 대하여 (0) | 2022.10.05 |
[IOS] NaverMap SDK 사용 중 알게된 Git LFS에 대하여 (0) | 2022.10.03 |
[IOS] CocoaPod의 pod install 오류에 대하여 (0) | 2022.09.09 |
[IOS] UIView의 tintColor 프로퍼티에 대하여 (0) | 2022.07.27 |