Wireframe

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

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

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

기본적으로 SQLite3를 사용하는데 주로 사용되는 함수는 다음 7개이다. 자세한 사용법에 대해서는 SQLite3 공식문서를 참고하도록 한다. 대략의 코멘트만 덧붙였다.

만들고자 하는 래퍼는 간단한 테이블에서 저장된 레코드들로부터 텍스트 정보를 모두 반환한다. 데이터 베이스와 관련한 정보는 다음과 같다고 가정하겠다.

그리고 래퍼 컨테이너는 다음의 한가지 API만을 구현하겠다. 그외에 조건에 의한 검색이나, 데이터 추가/변경/삭제는 쿼리만 다를 뿐이고 여기서는 래퍼를 만들어서 Swift에서 사용하게끔 하는 부분만 참고하면 되겠다.

그외에 추가적인 프로퍼티가 있다. 데이터 베이스 파일의 경로와, 파일이 해당 경로에 없을 때 번들로부터 샘플 데이터베이스를 복사하도록 하는 것이다. 단 이 프로퍼티들은 모듈 외부에서 참고할 필요는 없으므로 내부 인터페이스로 작성한다.

  1. 실제로 액세스할 파일 시스템 상의 데이터베이스 파일 경로 (dbPath)
  2. isPrepared : 1의 경로에 파일이 준비되어 있는지 여부를 나타내는 프로퍼티.

헤더정의

이상의 내용을 바탕으로 먼저 헤더를 정의해보자. 별 것 없이 외부에 노출될 메소드 하나만 정의해주면 된다.

/// DBConnector.h
#import <Foundation/Foundation.h>
@interface DBConnector : NSObject
- (NSArray *)getDefaultList;
@end

Private 프로퍼티 정의

private한 프로퍼티는 .m파일에서 내부의 익명 인터페이스를 통해서 정의한다. 참고로 이 파일에서는 SQLite3의 인터페이스를 사용해야 하므로 sqlite.h 파일을 임포트해야 한다.

/// DBConnector.m
#import "DBConnector.h"
#import <sqlite3.h>
@interface DBConnector ()
@property (readonly, nonatomic) NSString *DBPath;
@property (readonly, nonatomic) BOOL isPrepared;
@end
/// ... implementation ...

DB파일 경로

dbPath는 데이터베이스 파일의 경로를 의미하는데, 보통 앱에서 사용자가 내용을 수정할 수 있다면, 번들 내 경로가 아닌 앱의 사용자 문서 디렉토리에 생성해준다. 생성 부분은 isPrepared 에서 체크하면 되니까, 여기서는 경로만 만들어주겠다.

@implementation DBConnector
- (NSString *)DBPath
{
  if (!_DBPath) {
    // 초기화된적이 없다면, 경로를 초기화한다.
    // 파일매니저를 통해서 사용자문서 디렉토리 URL을 얻고
    // 여기에 파일명을 덧붙여 문자열로 변환한다.
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSURL docDir = [[fileManager URLsForDirectory:NSDocumentDirectory
                                     inDomainMask:NSUserDomainMask]
                      lastObject];
    _DBPath = [[docDir URLByAppendingPathComponent:@"data.sqlite"] string];
  }
  return _DBPath;
}

DB 파일 검사하기

DB를 실제로 액세스하기 전에 isPrepared 프로퍼티를 체크할 것이다. 이 프로퍼티는 데이터베이스 파일이 해당 경로에 있는지를 검사한다. 체크과정에서 파일이 존재하지 않으면 번들로부터 복사하여 설치한다. 설치에 실패하면 NO를 리턴한다.

- (BOOL)isPrepared
{
  // self.DBPath에 파일이 있는지 검사하고
  // 없는 경우 번들 내의 샘플 파일을 해당 경로로 복사
  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;
}

데이터 조회하기

다음은 실제로 데이터를 가져오는 메소드이고, 실제 액세스부분은 C함수 콜이고 얻어온 데이터를 NSMutableArray에 쌓아서 리턴한다.

- (NSArray *)getDefaultList
{
  // DB에 저장된 전체 텍스트를 가져온다.
  // 결과를 담을 배열
  NSMutableArray *list = [NSMutableArray array];
  if (self.isPrepared) {
    sqlite3 *db;
    const char* dbfile = [self.DBPath UTF8String];
    // DB에 연결
    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];
        }
        // statement 객체 해제
        sqlite3_finalize(stmt)
      }
      // 연결해제
      sqlite3_close(db)
    }
  }
  return list;
}

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

Swift 로 작성하는 프로젝트에서는 Objective-C 클래스를 그대로 사용할 수 있다. 단, 이 때 필요한 것이 브릿징-헤더이다. Xcode의 프로젝트 빌드 세팅에서 브릿징 헤더 (Objective-C Bridging Header files) 항목에 해당 파일의 경로를 넣어주면 된다. 브릿징 헤더에는 Swift 프로젝트로 가져오고 싶은 Objecitve-C 헤더들 및 함수 선언등이 들어가면 된다.

브릿징 헤더를 만들기 전에, Objective-C 파일들과 번들에 포함할 sqlite 파일을 프로젝트에 미리 복사해두자.

Bridging-Header 만들기

File > New File > Source > header file을 선택하여 확장자가 .h 인 파일을 만든다. 이름을 적당히 Bridging-Header.h로 두자. 이 내용에는 DBController.h 파일을 다시 반입하는 한 줄만 있으면 되겠다.

#import "DBController.h"

그리고 프로젝트의 빌드 세팅에서 브릿징 헤더 키를 찾아서 해당 파일의 경로를 넣어준다. 이때 파일의 이름만 달랑 들어가는게 아니라 프로젝트명/파일명의 형식이 되어야 한다. 예를 들어 프로젝트명이 TestMemo 이면 TestMemo/Bridging-Header.h를 입력해주자.

이제 DBConnector 클래스는 마치 Swift의 클래스인 것처럼 그대로 쓰면 된다. 참고로 getDefaultList의 리턴타입을 NSArray*로 했기 때문에 Array<AnyObject>로 반입될 것인데, 이를 적절히 다운캐스팅하자.

func resetWordList() -> Void {
    let conn = DBConnector()
    DispatchQueue.global.async {
        guard let contents = conn.getDefaultList() as? [String]
        else { return }
        self.wordList = contents
        DispatchQueue.main.sync {
            tableView.reloadData()
        }
    }
}

Xcode 7.0 부터 Objective-C 코드를 작성할 때 경량 제네릭이 지원된다. 즉 getDefaultList의 리턴타입이 NSArray<NSString *> * 이라면 [String] 타입으로 변환되어 반입될 것이다.

Exit mobile version