SQLite3 C API를 Swift에서 사용하는 방법

Swift에서 SQLite3를 사용하는 방법은 크게 두 가지이다. 하나는 Objective-C에서 SQLite3를 액세스하는 래퍼 클래스를 작성하고, 이 것을 Swift 프로젝트에 포함시켜서 컴파일하는 것이다. Swift는 Objective-C와 자연스럽게 상호호환되기 때문에 Objective-C에 친숙하다면 이 방법도 나쁘지 않다.

관련글 : Objective-C 래퍼를 통해 Swift에서 SQLite3를 사용하는 법

다른 한가지 방법으로는 sqlite3.h 헤더를 Swift에서 반입하여 Swift에서 SQLite3 C API를 바로 사용하는 것이다. 결국 업어치나 메치나 똑같은 것이기는 한데, Swift에서 C API를 직접 사용하는 것은 Swift와 C의 연계 방식에 대한 이해가 필요하다. 이 글에서는 Swift에서 Sqlite3를 사용하기 위해 필요한 배경 지식들에 대해서 살펴볼 예정이다. SQLite3 API에 대한 자세한 설명이 필요할 수 있는데, 이 내용은 Objective-C에서 SQLite3를 사용하는 법을 다룬 위 링크에서 대략 소개하고 있으며, 이 글에서는 최소한 SQLite3 API에 대해 알고 있다고 전제하겠다.

SQLite3 C API를 사용하여 쿼리를 실행하는 결과에 대해서는 다음의 과정을 거친다.

  1. 데이터베이스 연결
  2. 쿼리 컴파일 및 쿼리 바인딩
  3. 쿼리 실행 및 각 Row의 데이터 획득
  4. 연결닫기
SQLite3 C API를 Swift에서 사용하는 방법 더보기

Swift에서 SQLite3 사용하기

SQLite3는 C/C++API를 제공하고 있으며, 사용하기도 그리 어렵지 않다. Swift에서 SQLite3를 사용하기 위한 가장 간단한 방법은 Obejctive-C로 DB를 액세스하는 함수나 클래스를 작성하고, Xcode 프로젝트에서 이 클래스를 추가해 Swift에서 사용하는 방법이다. 특히 코코아 클래스들과 Swift 타입들 간에는 바로 브릿징되면서 자동으로 변환되는 것들이 있기 때문에 사용하기에 편리한 점은 있지만, Objective-C에 익숙하지 않거나, 혹은 그냥 아무 이유없이 C API와의 연동을 해보고 싶은 경우가 있을 수 있기에 방법을 소개한다.

SQLite3의 C API와 연동하는 방법에 대해서는 별도의 포스팅으로 내용을 분리하였다.

이 글에서는 C API를 직접 사용하면서 쿼리를 실행할 수 있는 클래스를 하나 작성해보도록 하겠다. 먼저 클래스 내에서 별도로 사용할 enum 타입들을 정의한다. 하나는 에러들이고, 다른 하나는 칼럼의 타입을 구분하기 위한 값으로 쓴다.

class SQLite {
  enum SQLError: Error {
    case connectionError
    case queryError
    case otherError
  }
  enum ColumnType {
    case int
    case double
    case text
  }
...
}

데이터베이스를 연결하고, 쿼리를 저장할 두 개의 객체 포인터가 필요하다. 그리고 파일 경로의 경우에는 바꾸는 사람 마음이겠지만, 간단히 앱 라이브러리 디렉토리에 저장되도록 하겠다.

var db: OpaquePointer?
var stmt: OpaquePointer?
let path: String = {
  let fm = FileManager.default
  return fm.urls(for:.libraryDirectory, in:.userDomainMask).last!
           .appendingPathComponent("db.sqlite").path
}()

이 클래스는 초기화 될 때 자동으로 파일을 찾아서 열고 쿼리를 받을 준비를 하도록 한다. 또 제거될 때 DB를 닫도록한다.

init() throws {
  if sqlite3_open(path, &db) == SQLITE_OK {
    return 
   }
   throw SQLError.connectionError
}

deinit {
  sqlite3_finalize(stmt)
  sqlite3_close(db)
}

이전의 예들에서는 간단하게 고정된 스키마의 테이블을 쓰면서 정수값 하나를 읽고, 쓰고 하는 예를 보였는데 여기에서는 필요할 때마다 쿼리를 인스톨하고 (또 필요하면 바인딩하고) 쿼리를 실행하도록 해보자.

func install(query: String) throws {
  sqlite3_stmt(stmt); stmt = nil
  if sqlite3_prepare_v2(db, query, -1, &stmt, nil) != SQLITE_OK {
    throw SQLError.queryError
  }
}

쿼리의 내용에 맞게 바인딩을 할 수 있게 해준다. 바인딩되는 칼럼의 순서와 타입은 쓰는 사람이 정확하게 쓰는 수 밖에…

func bind(data:Any, withType type:ColumnType at col:Int32 = 1) {
  switch type {
  case .int:
    if let value = data as? Int {
      sqlite3_bind_int(stmt, col, Int(value))
     }
  case .double:
    if let value = data as? Double {
      sqlite3_bind_double(stmt, col, value)
    }
  case .text:
    if let value = data as? String {
      sqlite3_bind_text(stmt, col, value, -1, nil)
    }
}

다음은 쿼리를 실행하는 함수인데, 이 결과는 SQLITE_DONE이거나 SQLITE_ROW일 것이다. (그 외에는 뭔가 에러가 발생했다는 의미가 될테고) 따라서 이 두 값에 따라서 DONE이면 실행을 종료하고, ROW이면 읽어온 row가 하나 있으니 그것을 핸들러로 처리하도록 한다. 사실 x 번째 칼럼에서 y 타입의 값을 얻어오는 메소드를 만드는 것도 나쁘지 않을 것 같기는 하다.

func execute(rowHanlder:((OpaquePointer) -> Void)? = nil) throws {
  while true {
    switch sqlite3_step(stmt) {
    case SQLITE_DONE: return
    case SQLITE_ROW: rowHandler?(stmt!)
    default:
      throw SQLError.otherError
    }
  }
}

최종적으로 테스트 코드이다.

do {
    let db = try SQLite()

    // 테이블 생성
    try db.install(query:"CREATE TABLE IF NOT EXISTS test (num INTEGER)")
    try db.execute()
    
    // 데이터를 삽입해보자.
    for i in 10..<20 {
        try db.install(query:"INSERT INTO test VALUES (?)")
        db.bind(data: i, withType: .int)
        try db.execute()
    }
    
    /// 조회
    try db.install(query:"select * from test")
    try db.execute(){ stmt in
        let n = sqlite3_column_int(stmt, 0)
        print(n)
    }
} catch {
    print(error)
}

다음은 전체 코드를 하나로 묶은 Gist이다.

Objective-C 래퍼를 통해 Swift에서 SQLite3를 사용하는 법

이 글에서는 Objective-C로 SQLite3 데이터베이스에 액세스하는 API를 래핑한 간단한 클래스를 작성해보겠다. 사실 Objective-C로 SQLite3를 사용하는 것은 C API를 그대로 사용하면 되는 부분인데, 이렇게 래퍼를 만들면 브릿징헤더만 작성해주면 래퍼 클래스를 Swift에서 그대로 사용할 수 있게 되기 때문에 좀 더 쉽게 사용할 수 있다.

Swift에서 C헤더를 바로 반입할 수 있기 때문에 이 방식은 오히려 번거로울 수 있다. (특히 스펙이 약간만 변경되어도 Objective-C 클래스를 수정해야 한다.) 선택은 각자가 알아서 하시면 되겠다.

Objective-C 래퍼를 통해 Swift에서 SQLite3를 사용하는 법 더보기

iOS에서 SQLite3 사용하는 방법 (Objective-C)

Swift3 버전에 대한 SQLite3 사용법은 이 포스트를 참고하시라.

예전에 쓴 글이 있기는 하지만, 그냥 요리법처럼 쓴 글이기도 하거니와 소스코드에서 뭔가 글자가 빠지는 등(syntax highlighter를 안써야 겠지만 기존 글 고치기가 귀찮아…) 문제가 많아 내용을 보충해서 다시 작성.

애플은 SQLite3를 직접 인터페이스하는 것보다는 코어데이터를 사용하라고 권장하고 있고, (실제로 있다가 빠진 것인지는 알 수 없으나 그런 주장을 하는 사람들이 종종 있다) 애플 개발자 문서에서도 관련 내용을 내렸다고 한다. (하지만 이는 사실이 아닐 거라 생각한다. 왜냐면 iOS에서 SQLite3를 인터페이스 하는 부분은 전적으로 libsqlite3를 사용하는 것이고, 이에 대한 문서는 SQLite3 홈페이지에 가면 있기 때문이다) iOS에서 SQLite3 사용하는 방법 (Objective-C) 더보기

[Python] 초간단 Sqlite3 사용 예제

다음 코드는 SQLite3를 통해 테이블을 만들고 (그전에 있으면 없애버리고) INSERT, SELECT, UPDATE, DELETE를 모두 한 번씩 해보는 예제이다. 쿼리를 만들고 실행하는 방법에 대해 보기 편하라고 만든 예제라 별도의 함수화 같은 건 하지 않았다. [Python] 초간단 Sqlite3 사용 예제 더보기