[iOS] Realm을 알아보자.

!·2023년 3월 14일
0

iOS

목록 보기
18/22

Overview

안녕하세요. 오랜만입니다. 개강 시즌과 개인적으로 진행하는 프로젝트때문에 며칠동안 바빠서 공부한 내용을 포스트하지 못했습니다. 진행하는 프로젝트에서는 Core Data를 LocalDB로써 사용하고자 하였지만, Realm에 대해 이것 저것 알아보면서 Realm이 생각보다 사용하기 매우 쉽고, 성능도 무지막지하게 빨라 결국은 Realm으로 사용하기로 결정했습니다. 이번 포스트에서는 Realm의 특징과 객체 정의 방법, CRUD에 대해 알아보겠습니다.


Realm의 특징

Realm은 iOS, 안드로이드 및 여러 플랫폼에서 사용할 수 있는 LocalDB중 하나입니다. 일반적으로 DB는 크게 3가지 종류가 있습니다.

  • 관계형 데이터베이스 : 테이블에 데이터가 저장되고, 각 테이블의 행의 Foreign Key로 다른 테이블과 관계를 정의하는 데이터베이스입니다. SQLite가 이에 포함됩니다.

  • 키-값 데이터베이스 : 작은 데이터를 저장할 때 유용한 데이터베이스입니다. 키가 유일한 식별자로써 사용되며, 각 키에 해당하는 Value를 저장하는 방식입니다. iOS의 UserDefaults가 이에 포함됩니다.

  • 객체 지향 데이터베이스 : 객체 지향 프로그래밍 언어 패러다임과 같이, 데이터를 객체형식으로 저장합니다. Core Data와, Realm이 이에 해당합니다.

엄밀히 말하면 Core Data는 DB가 아닙니다. SQLite를 래핑한 ORM(Object Relational Mapper)입니다. 즉, 관계형 데이터베이스인 SQLite를 사용자가 사용할 때 마치 객체 지향 데이터베이스처럼 사용할 수 있도록한 인터페이스라고 볼 수 있습니다.

속도면에서도 Realm은 다른 데이터베이스를 압도합니다. 특히나, 데이터양이 많아지고 요청되는 쿼리문이 늘어날 수록 이 차이는 더욱 두드러집니다.


객체 모델 관계 용어

  • Primary Key: 각 객체 모델 당 최대 1개만 가질 수 있으며, 해당 객체 모델들의 인스턴스들을 식별할 수 있는 유일한 값이다. 따라서, 중복된 Primary Key를 갖는 인스턴스가 추가되면 Error가 발생된다.

  • To-One Relationship : 각 객체 모델의 관계가 1 vs 1임을 의미합니다. 예를 들어 Person객체와, Dog객체가 존재할 때 Person객체가 반드시 한 마리의 Dog객체와의 관계를 맺어야하는 경우를 의미합니다.

  • To-Many Relationship: 각 객체 모델의 관계가 1 vs N임을 의마힙니다. Person객체가 여러마리의 개를 가질 수 있는 경우를 의미합니다.

  • Inverse Relationship: 관계를 맺는 두 객체들의 관계가 역참조가 된다는 것을 의미합니다. 일반적으로 Realm은 역참조를 지원하지 않습니다. 예를 들어, User 객체와 Task 객체가 있다고 가정해봅시다. User 객체는 여러개의 Task 객체와 관계를 가진다면, 이 Task 객체들은 User 객체들을 참조하지 않습니다.


객체 모델 정의

Realm에 저장하고자 하는 객체들은 반드시 Object클래스를 상속해야합니다. 몇몇 부가적인 기능을 사용할 때는 다른 클래스를 상속할 수 있지만, 이번 포스팅에서는 Object클래스를 상속해 객체 모델을 정의하겠습니다.

  • To-One Relationship: 객체 모델의 프로퍼티에 관계를 갖는 객체를 정의합니다. 반드시 옵셔널이어야합니다. 만약, nil을 할당해도 관계가 있던 객체는 Realm내에서 삭제되지 않습니다.
class Person: Object {
    @Persisted var name: String = ""
    @Persisted var birthdate: Date = Date(timeIntervalSince1970: 1)
    // A person can have one dog
    @Persisted var dog: Dog?
}

class Dog: Object {
    @Persisted var name: String = ""
    @Persisted var age: Int = 0
    @Persisted var breed: String?
    // No backlink to person -- one-directional relationship
}
  • To-Many Relationship: 객체 모델의 프로퍼티에 관계를 갖는 객체를 List타입으로 정의합니다.
class Person: Object {
    @Persisted var name: String = ""
    @Persisted var birthdate: Date = Date(timeIntervalSince1970: 1)
    // A person can have many dogs
    @Persisted var dogs: List<Dog>
}

class Dog: Object {
    @Persisted var name: String = ""
    @Persisted var age: Int = 0
    @Persisted var breed: String?
    // No backlink to person -- one-directional relationship
}
  • Inverse Relationship: Realm은 기본적으로, 객체들간의 관계에 Update가 발생하면 자동적으로 Update합니다. 따라서 역참조 관계를 명시적으로 설정할 필요가 없습니다. 하지만, 비즈니스 로직적으로 역참조가 필요한 상황이라면 다음과 같이 객체 모델을 설계할 수 있습니다.
class User: Object {
    @Persisted(primaryKey: true) var _id: ObjectId
    @Persisted var _partition: String = ""
    @Persisted var name: String = ""
    // A user can have many tasks.
    @Persisted var tasks: List<Task>
}

class Task: Object {
    @Persisted(primaryKey: true) var _id: ObjectId
    @Persisted var _partition: String = ""
    @Persisted var text: String = ""
    // Backlink to the user. This is automatically updated whenever
    // this task is added to or removed from a user's task list.
    @Persisted(originProperty: "tasks") var assignee: LinkingObjects<User>
}

트랜잭션

Realm의 기능을 사용하기에 앞서, Transaction 용어에 대한 정의가 필요합니다. 트랜잭션이란 최소 단위 작업을 의미합니다. 예를 들어, A라는 사람이 B라는 사람에게 송금을 한다고 가정해봅시다. 그럼 다음과 같은 작업이 순차적으로 시행되어합니다.
A의 통장 잔고 확인 -> A의 잔고에서 송금액 만큼 뺌 -> B의 잔고에서 송금액 만큼 더함

이 세 단계의 작업을 하나의 트랜잭션이라고 합니다. 즉, 이 중 하나라도 실패핸다면 송금이라는 작업은 실패합니다.

Realm에서는 이를 write 트랜잭션이라고 합니다. write 블록 내에 작업(Add, Delete, Update 등)을 수행해야 합니다. 만약, write 블록이 error를 던진다면, 자동적으로 롤백됩니다.

let realm = try! Realm()
// Prepare to handle exceptions.
do {
    // Open a thread-safe transaction.
    try realm.write {
        // Make any writes within this code block.
        // Realm automatically cancels the transaction
        // if this code throws an exception. Otherwise,
        // Realm automatically commits the transaction
        // after the end of this code block.
    }
} catch let error as NSError {
    // Failed to write to realm.
    // ... Handle error ...
}

Query & CRUD

  • Create
let dog = Dog()
dog.name = "Rex"
dog.age = 10
// Get the default realm. You only need to do this once per thread.
let realm = try! Realm()
// Open a thread-safe transaction.
try! realm.write {
    // Add the instance to the realm.
    realm.add(dog)
}

  • Read : Dog타입의 모든 객체 인스턴스들을 dogs에 담습니다.
let realm = try! Realm()
// Access all dogs in the realm
let dogs = realm.objects(Dog.self)

  • Query by Condition
let realm = try! Realm()
// Access all dogs in the realm
let dogs = realm.objects(Dog.self)
// Query by age
let puppies = dogs.where {
    $0.age < 2
}
// Query by person
let dogsWithoutFavoriteToy = dogs.where {
    $0.favoriteToy == nil
}
// Query by person's name
let dogsWhoLikeTennisBalls = dogs.where {
    $0.favoriteToy.name == "Tennis ball"
}
  • Query by Relation
let realm = try! Realm()
// Establish a relationship
let dog = Dog()
dog.name = "Rex"
dog.age = 10

let person = Person()
person.id = 12345
person.dogs.append(dog)

try! realm.write {
    realm.add(person)
}
// Later, query the specific person
let specificPerson = realm.object(ofType: Person.self, forPrimaryKey: 12345)
// Access directly through a relationship
let specificPersonDogs = specificPerson!.dogs
let firstDog = specificPersonDogs[0]

print("# dogs: \(specificPersonDogs.count)")
print("First dog's name: \(firstDog.name)")
  • Query by Inverse Relation
let realm = try! Realm()
// Establish an inverse relationship
let person = Person()
person.id = 12345

let club = DogClub()
club.name = "Pooch Pals"
club.members.append(person)

try! realm.write {
    realm.add(club)
}
// Later, query the specific person
let specificPerson = realm.object(ofType: Person.self, forPrimaryKey: 12345)
// Access directly through an inverse relationship
let clubs = specificPerson!.clubs
let firstClub = clubs[0]

print("# memberships: \(clubs.count)")
print("First club's name: \(firstClub.name)")
  • Query by Condition and Sort
// Open the default realm
let realm = try! Realm()

// Get all contacts in Los Angeles, sorted by street address
let losAngelesPeople = realm.objects(Person.self)
    .where {
        $0.address.city == "Los Angeles"
    }
    .sorted(byKeyPath: "address.street")
    
print("Los Angeles Person: \(losAngelesPeople)")

  • Update : Update는 반드시, write 트랜잭션 안에서 수행되어야 합니다.
let realm = try! Realm()
// Get a dog to update
let dog = realm.objects(Dog.self).first!
// Open a thread-safe transaction
try! realm.write {
    // Update some properties on the instance.
    // These changes are saved to the realm
    dog.name = "Wolfie"
    dog.age += 1
}
  • Update : Key - Value 형식으로도 업데이트가 가능합니다.
let realm = try! Realm()
let allDogs = realm.objects(Dog.self)
try! realm.write {
    allDogs.first?.setValue("Sparky", forKey: "name")
    // Move the dogs to Toronto for vacation
    allDogs.setValue("Toronto", forKey: "currentCity")
}

  • Delete: Delete도 반드시, write 트랜잭션 안에서 수행되어야 합니다.
let realm = try! Realm()
try! realm.write {
    realm.add(dog)
}
// Delete the instance from the realm.
try! realm.write {
    realm.delete(dog)
}
  • Delete an Object and Its Related Objects: Realm은 관계를 맺는 객체 중, 부모 객체가 삭제되면 관계를 맺는 다른 객체들을 Realm 내에서 삭제하지 않습니다. 따라서, 이를 명시적으로 구현해야합니다. 일반적으로 관계를 맺음 당하는(?) 객체들을 삭제한 뒤, 메인 객체를 삭제합니다.
let person = realm.object(ofType: Person.self, forPrimaryKey: 1)!
try! realm.write {
    // Delete the related collection
    realm.delete(person.dogs)
    realm.delete(person)
}

참고자료

https://realm.io/realm-swift/

profile
개발자 지망생

0개의 댓글