GRDB의 기본

inu
daily-monster
Published in
10 min readMar 4, 2024

안녕하세요. 월요괴물 inu입니다. 최근 회사에서 GRDB를 사용할 일이 생겨서 기본적인 사항에 대해 공부했는데, 오늘은 이 부분에 대해 공유해보겠습니다.

GRDB (GR Database)는 Swift로 작성된 iOS, macOS, tvOS, watchOS 앱을 위한 SQLite 데이터베이스 라이브러리입니다. GRDB는 SQLite의 강력한 기능을 활용하면서, Swift의 타입 안전성과 객체 지향적 특성을 통해 데이터베이스 작업을 용이하게 만듭니다. 따라서 빠른 성능을 추구할 때 많이 선택하게 되는 데이터베이스 라이브러리입니다. 이번 포스팅에서는 GRDB의 기본적인 내용에 대해서 알아보는 시간을 가지겠습니다.

GRDB Github : https://github.com/groue/GRDB.swift

1. 기본사항

  • GRDB를 사용하여 데이터베이스와 연결하는 첫 번째 단계는 DatabaseQueue 또는 DatabasePool 인스턴스를 생성하는 것입니다. 이 객체들은 데이터베이스 파일에 대한 연결을 관리합니다. 자세한 내용은 아래에서 더 자세히 다루겠습니다.
  • Swift에서 GRDB를 사용할 때, 데이터베이스 테이블과 관련된 데이터를 저장하고 관리하기 위한 모델 클래스 또는 구조체를 정의할 수 있습니다. GRDB는 Codable 프로토콜을 활용하여 모델 객체와 SQLite 테이블 간의 데이터 매핑을 용이하게 합니다.
  • GRDB를 통해 데이터를 쿼리, 삽입, 업데이트 및 삭제하는 작업을 수행할 수 있습니다. 이러한 작업은 DatabaseQueue 또는 DatabasePool을 사용하여 수행됩니다.
  • 쿼리: fetchOne, fetchAll 메소드를 사용하여 데이터베이스에서 데이터를 조회할 수 있습니다.
  • 삽입 및 업데이트: insert, update 메소드를 사용하여 데이터를 삽입하거나 업데이트할 수 있습니다.
  • 삭제: delete 메소드를 사용하여 데이터를 삭제할 수 있습니다.
  • GRDB는 데이터베이스 작업을 그룹화하고, 데이터 일관성을 유지하기 위해 트랜잭션을 지원합니다. DatabaseQueue 또는 DatabasePool을 사용하여 트랜잭션을 시작하고, 커밋 또는 롤백할 수 있습니다.
  • GRDB는 데이터베이스 스키마의 버전 관리를 위한 마이그레이션 도구를 제공합니다. 이를 통해 데이터베이스 스키마를 안전하게 변경하고, 버전을 관리할 수 있습니다.

2. 핵심 객체

DatabaseQueueDatabasePool은 GRDB에서 데이터베이스 작업을 동기화하고 관리하는 핵심 객체입니다. 이 두 객체는 데이터베이스 연결을 관리하는 방법과 멀티스레딩 환경에서의 작동 방식에 차이가 있습니다.

DatabaseQueue

  • 용도: DatabaseQueue는 단일 SQLite 데이터베이스 연결을 순차적으로 관리합니다. 이는 여러 스레드에서 동시에 발생할 수 있는 데이터베이스 요청을 차례로 처리하여, 데이터베이스 작업의 동기화를 보장합니다.
  • 동작 방식: DatabaseQueue는 FIFO(First-In-First-Out) 큐를 사용하여 데이터베이스 작업을 순차적으로 실행합니다. 이는 동시성 문제를 방지하고, 데이터 일관성을 유지하는 데 도움이 됩니다.
  • 사용 시나리오: DatabaseQueue는 복잡하지 않은 데이터베이스 작업이 필요하거나, 동시에 여러 데이터베이스 작업이 발생하지 않는 애플리케이션에 적합합니다. 간단한 CRUD(Create, Read, Update, Delete) 작업이 주를 이루는 앱에서 효율적입니다.

DatabasePool

  • 용도: DatabasePool은 여러 SQLite 데이터베이스 연결을 관리하여, 병렬로 데이터베이스 작업을 수행할 수 있게 합니다. 이는 동시에 여러 스레드에서 데이터베이스 작업을 수행할 수 있도록 지원하며, DatabaseQueue에 비해 더 높은 동시성과 성능을 제공합니다.
  • 동작 방식: DatabasePool은 연결 풀(connection pool)을 사용하여 여러 데이터베이스 연결을 관리합니다. 읽기 작업은 여러 스레드에서 병렬로 실행할 수 있지만, 쓰기 작업은 동시에 하나씩만 수행됩니다. 이를 통해 높은 동시성을 유지하면서도 데이터 무결성을 보장합니다.
  • 사용 시나리오: DatabasePool은 동시에 다수의 데이터베이스 작업을 처리해야 하거나, 복잡한 쿼리와 대량의 데이터 처리가 필요한 애플리케이션에 적합합니다. 높은 성능과 효율적인 자원 사용이 요구되는 경우에 주로 사용됩니다.

선택 기준

  • DatabaseQueueDatabasePool 중 어느 것을 사용할지 결정할 때는 애플리케이션의 요구 사항을 고려해야 합니다. 예를 들어, 동시성 요구 사항, 데이터베이스 작업의 복잡성, 성능 요구 사항 등을 기준으로 적절한 선택을 할 수 있습니다.
  • 일반적으로, 단순한 애플리케이션에서는 DatabaseQueue가 충분할 수 있으며, 더 복잡하거나 성능이 중요한 경우 DatabasePool을 고려해야 합니다.

각각의 객체를 선택할 때는 애플리케이션의 구체적인 요구 사항과 성능 목표를 고려하는 것이 중요합니다.

3. Record

GRDB에서 Record는 데이터베이스 테이블 레코드를 모델링하기 위한 기본 클래스입니다. 이 클래스를 상속받아 사용자 정의 모델을 구현함으로써, 데이터베이스 테이블과 Swift 코드 사이의 매핑을 용이하게 합니다. Record를 사용하면 데이터베이스 작업을 보다 객체 지향적으로 처리할 수 있으며, GRDB의 다양한 기능을 활용하여 코드의 간결성과 효율성을 높일 수 있습니다.

Record의 핵심 기능

  • 자동 매핑: Record 클래스는 테이블의 컬럼과 모델의 프로퍼티 간의 자동 매핑을 지원합니다. 이를 통해 데이터베이스의 데이터를 모델 객체로 쉽게 변환할 수 있으며, 반대의 과정도 간단합니다.
  • CRUD 작업의 간소화: Record를 상속받은 모델 객체는 삽입(insert), 조회(fetch), 업데이트(update), 삭제(delete) 등의 CRUD 작업을 메서드 호출만으로 수행할 수 있습니다. 이러한 메서드들은 Record 클래스에 구현되어 있어, 사용자는 복잡한 SQL 쿼리를 작성할 필요 없이 데이터베이스 작업을 할 수 있습니다.
  • 트랜잭션 및 변경 추적: Record 클래스는 트랜잭션 관리와 변경 추적을 지원합니다. 모델 객체의 변경 사항을 자동으로 감지하고, 필요에 따라 데이터베이스에 적용할 수 있습니다.

사용 예제

import GRDB
// Record를 상속받아 사용자 정의 모델 생성
class User: Record {
var id: Int64?
var name: String
var email: String
// 필수 초기화 메서드
init(id: Int64? = nil, name: String, email: String) {
self.id = id
self.name = name
self.email = email
super.init()
}
// Record의 메서드를 오버라이드하여 데이터베이스 매핑 설정
override class var databaseTableName: String {
return "User"
}
required init(row: Row) {
id = row["id"]
name = row["name"]
email = row["email"]
super.init(row: row)
}
override func encode(to container: inout PersistenceContainer) {
container["id"] = id
container["name"] = name
container["email"] = email
}
}
// 사용 예: 사용자 모델을 데이터베이스에 삽입
let user = User(name: "Jane Doe", email: "jane.doe@example.com")
try dbQueue.write { db in
try user.insert(db)
}

이 예제는 User 모델이 어떻게 Record를 상속받아 구현되는지 보여줍니다. Record의 기능을 활용하여, 데이터베이스 테이블과 모델 사이의 매핑, 데이터의 삽입과 조회 등을 손쉽게 수행할 수 있습니다.

4. CRUD

이번엔 직접 이들은 활용한 CRUD(Create, Read, Update, Delete) 예제를 살펴보겠습니다.

모델 정의

먼저, GRDB의 Record를 상속받는 모델 클래스를 정의합니다. 이 클래스는 데이터베이스 테이블에 대응합니다.

import GRDB

class Person: Record {
var id: Int64?
var name: String
var age: Int
init(id: Int64? = nil, name: String, age: Int) {
self.id = id
self.name = name
self.age = age
super.init()
}
override class var databaseTableName: String {
return "persons"
}
required init(row: Row) {
id = row["id"]
name = row["name"]
age = row["age"]
super.init(row: row)
}
override func encode(to container: inout PersistenceContainer) {
container["id"] = id
container["name"] = name
container["age"] = age
}
}

Create (생성)

데이터베이스에 새로운 레코드를 삽입합니다.

let newPerson = Person(name: "John Doe", age: 30)
try dbQueue.write { db in
try newPerson.insert(db)
}

Read (읽기)

데이터베이스에서 레코드를 조회합니다.

let allPersons = try dbQueue.read { db in
try Person.fetchAll(db)
}
// 특정 조건에 맞는 레코드 조회
let personsNamedJohn = try dbQueue.read { db in
try Person.fetchAll(db, sql: "SELECT * FROM persons WHERE name = ?", arguments: ["John Doe"])
}

Update (업데이트)

데이터베이스의 기존 레코드를 업데이트합니다.

try dbQueue.write { db in
if var personToUpdate = try Person.fetchOne(db, key: 1) { // id가 1인 Person을 조회
personToUpdate.age = 31
try personToUpdate.update(db)
}
}

Delete (삭제)

데이터베이스에서 레코드를 삭제합니다.

try dbQueue.write { db in
if let personToDelete = try Person.fetchOne(db, key: 1) { // id가 1인 Person을 조회
try personToDelete.delete(db)
}
}

각 작업은 dbQueue 또는 dbPool을 통해 데이터베이스와의 연결을 관리하며, GRDB의 Record 기능을 활용하여 모델 객체와 데이터베이스 테이블 간의 상호 작용을 간단하게 구현할 수 있습니다.

이상 GRDB에 대해서 알아봤습니다. 감사합니다~

--

--