Swift에서 sqlite3 사용하는 법

iOS에서 sqlite3 사용하기 – Swift로 옮겨가기

크리스마스 특집편

sqlite3를 Objective-C에서 사용하는 것은 Objective-C가 C이기도 하므로, C/C++ API를 그대로 써서 사용하면 됐다. 여기서 필요한 것은 크게 7종류의 함수이다. (각 함수에 대해서는 이전 글들이나 레퍼런스 문서를 참고하자)

  • sqlite3_open()
  • sqlite3_prepare() / sqlite3_prepare_v2()
  • sqlite3_step()
  • sqlite3_column_* ()
  • sqlite3_bind_* ()
  • sqlite3_finalize()
  • sqlite3_close()

Swift에서 sqlite3를 사용하는 것에 대해 고민해보자. Objective-C는 C의 수퍼셋이었으므로 C API를 사용하는데 아무런 제약이 (그저 C문법만 알고 있다면) 없었다. 하지만 Swift는 C와는 다른 환경을 제공하고 있어서 이를 그대로 쓰는 게 어렵다. 그래서 몇가지 방법을 강구해보도록 한다.

  1. Objective-C에서 Model-Controller에 해당하는 클래스를 작성하고 (이 클래스가 DB 액세스를 담당) 이 클래스를 Swift 프로젝트로 반입한다.
  2. Swift에서 sqlite3.h 파일을 통해 라이브러리 전체를 반입하여 Swifty하게 작성해본다.

Model Controller Class (Objective-C)

어쨌든 복습도 할 겸, 모델 컨트롤러 클래스를 한 번 작성해 보도록하자. 이 클래스는 sqlite3 데이터베이스 파일에 접근하여 데이터 목록을 가져온다고 가정한다. 데이터베이스는 아주 단순하게 id(INTEGER), text(TEXT)의 두 개 칼럼을 가진 tbl_memo라는 테이블을 가지고 있고, 기본적인 데이터는 이미 들어있다고 가정한다. 그리고 구현은 하지 않겠지만 새로운 레코드를 추가하거나 기존 레코드를 편집하는 기능이 있다고 가정한다. (그래서 데이터베이스 파일을 복사하는 과정을 추가할 것이다.)

먼저 이 클래스를 외부에서 봤을 때 필요한 기능은 다음과 같다.

  1. 디폴트 메모 목록을 가져온다. >> - (NSArray *)getDefaultList;
  2. 검색어에 대해 검색한 메모 목록을 가져온다. >> - (NSArray *)getSearchResultFor:(NSString *)searchTerm;

2번은 사실상 1번과 쿼리 구문만 살짝 다를테니 구현하지 않겠다.

그리고 이 클래스가 가져야 할 내부 인터페이스에 대해 살펴보자.

  1. 데이터베이스 파일의 파일 시스템 상의 경로
  2. 번들 내의 샘플 데이터 파일의 이름 (1과 동일한 이름을 쓰기로 한다. “data.sqlite”)
  3. isPrepared : 1의 경로에 파일이 준비되어 있는지 여부를 나타내는 프로퍼티

대략 이정도가 되겠다.

헤더: DBController.h

#import <Foundation/Foundation.h>

@interface DBConnector : NSObject
- (NSArray *)getDefaultList;
@end

헤더 자체는 매우 간단하다.

구현 : DBController.m

내부 인터페이스에서는 private한 프로퍼티를 추가로 선언한다.

#import "DBConnector.h"
#import <sqlite3.h>

@interface DBConnector ()
@property (readonly, nonatomic) NSString *DBPath;
@property (readonly, nonatomic) BOOL isPrepared;
@end

또한, sqlite3 라이브러리를 사용하는 부분도 이 부분이므로 헤당 라이브러리의 헤더를 반입해둔다.

DBPath는 데이터베이스 파일의 경로를 의미하는데, 번들 내 경로가 아닌 앱의 사용자 문서 디렉토리 내의 경로를 구한다.

@implementation DBConnector
@synthesize DBPath=_DBPath

- (NSString *)DBPath {
    if (!_DBPath) {
        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSURL docDir = [[fileManager URLsForDirectory:NSDocumentDirectory inDomainMask:NSUserDomainMask] 
                            lastObject];
        _DBPath = [[docDir URLByAppendingPathComponent:@"data.sqlite"] string]
    }
    return _DBPath;
}

간단하다. 이번에는 DB 액세스를 하기 이전에 DB 파일이 준비되었는지 검사하는 과정이다. 이 과정이 필요한 이유는 앱 설치후 최초 실행시에는 해당 DB 파일이 없기 때문에 이 파일을 번들로부터 복사하는 작업이 필요하기 때문이다.

- (BOOL)isPrepared
{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if ( ![fileManager fileExistsAtPath:self.DBPath]) {
        NSURL *predefinedDB = [[[NSBundle MainBundle] URLForResource:@"data" withExtension:@".sqlite"] path];
        NSError *error = nil;
        [fileManager copyItemAtPath:predefinedDB toPath:self.DBPath error:&error];
        if (error) {
            return NO;
        }
    } 
    return YES;
}

다음은 실제로 데이터를 가져오는 액세스 부분이다.

- (NSArray *)getDefaultList
{
    NSMutableArray *list = [NSMutableArray array];
    if (self.isPrepared) {
        sqlite3 *db;
        const char* dbfile = [self.DBPath UTF8String];
        if (sqlite3_open(dbfile, &db) == SQLITE_OK) {
            sqlite3_stmt *stmt;
            const char* query_str = [@"SELECT text FROM tbl_memo ORDER BY id ASC LIMIT 50" UTF8String];
            if (sqlite3_prepare(db, query_str, -1, &stmt, NULL) == SQLITE_OK) {
                while (sqlite3_step(stmt) == SQLITE_ROW) {
                    NSString *col = [NSString stringWithUTF8String:sqlite3_column_text(stmt, 0)];
                    [list addObject:col];
                }
                sqlite3_finalize(stmt)
            }
            sqlite3_close(db)
        }
        return list;
    }
}

뭐 딱히 어려울 것 없다.

Swift에서 Objective-C 클래스를 그대로 사용하기

Swift 로 작성하는 프로젝트에서는 Objective-C 클래스를 그대로 사용할 수 있다. 이 때 필요한 것이 브릿징-헤더이다. 이는 시스템적으로 자동으로 생성하는 것은 아니고 빌드 세팅에서 이 브릿징 헤더 (Objective-C Bridging Header files) 항목에 해당 파일의 경로를(모듈/파일이름 식의 경로도 된다) 기술해둔다. 그리고 이 파일 내에서 Swift 상에서 참조해야 할 클래스나 라이브러리의 헤더 파일을 모두 반입하면 된다.

프로젝트가 Swift로 만들어지고, 위의 Objective-C 클래스 및 데이터 파일(data.sqlite)을 프로젝트로 복사했다고 가정하고 진행하겠다.

Bridging-Header 만들기

이는 간단하다. 만약 없다면 (만들지 않았다면 없다) File > New File > Source > header file을 선택하여 확장자가 .h 인 파일을 만든다. 이름을 적당히 Bridging-Header.h로 두자.

이 파일의 내용에 다음과 같이..

#import "DBController.h"

한줄만 넣어주면 된다.

그리고 프로젝트의 빌드 세팅에서 브릿징 헤더 키를 찾아서 해당 파일의 경로를 넣어준다. 프로젝트명이 TestMemo 이면 TestMemo/Bridging-Header.h를 입력해주자.

만약 정상적으로 처리되었다면 ViewController.swift 파일에서 다음과 같은 식으로 처리가 가능하다.

func resetWordList() -> Void {
    self.wordList = []
    let dbcon = DBController()
    let obj_list = dbcon.getDefaultList()
    for i in obj_list {
        self.wordList.append(i as String)
    }
    self.tableView.reloadData()
}

그냥 Swift의 클래스인 것처럼 그대로 쓰면 된다. 다만, 리턴타입이 NSArray *인데 이것은 Swift에서는 [AnyObject]로 변환되므로 이를 다시 Swift 타입으로 만들어주는 작업이 필요하다.1

C-API를 사용하기

Objective-C를 잘 모르거나… 아니면 Swift 상에서 직접 SQLite3를 인터페이스 하고 싶다면, 이 글을 참고할 것. 사실 Objective-C 보다도 훨씬 깔끔하게 코드가 만들어질 수 있다.


  1. 이제는 Objective-C에서도 Generic이 도입되었기 때문에, 반드시 AnyObject를 상정할 필요도 없다.