이 글에서는 Objective-C로 SQLite3 데이터베이스에 액세스하는 API를 래핑한 간단한 클래스를 작성해보겠다. 사실 Objective-C로 SQLite3를 사용하는 것은 C API를 그대로 사용하면 되는 부분인데, 이렇게 래퍼를 만들면 브릿징헤더만 작성해주면 래퍼 클래스를 Swift에서 그대로 사용할 수 있게 되기 때문에 좀 더 쉽게 사용할 수 있다.
Swift에서 C헤더를 바로 반입할 수 있기 때문에 이 방식은 오히려 번거로울 수 있다. (특히 스펙이 약간만 변경되어도 Objective-C 클래스를 수정해야 한다.) 선택은 각자가 알아서 하시면 되겠다.
기본적으로 SQLite3를 사용하는데 주로 사용되는 함수는 다음 7개이다. 자세한 사용법에 대해서는 SQLite3 공식문서를 참고하도록 한다. 대략의 코멘트만 덧붙였다.
sqlite3_open()
– DB에 연결sqlite3_prepare() / sqlite3_prepare_v2()
– 쿼리 컴파일sqlite3_bind_* ()
– 컴파일 된 쿼리에 변수 바인딩sqlite3_step()
– 쿼리를 실행한 결과 중 1개 Row를 가져옴sqlite3_column_* ()
– 가져온 Row에서 칼럼값을 가져옴sqlite3_finalize()
– 컴파일된 쿼리 해제sqlite3_close()
– 연결닫기
만들고자 하는 래퍼는 간단한 테이블에서 저장된 레코드들로부터 텍스트 정보를 모두 반환한다. 데이터 베이스와 관련한 정보는 다음과 같다고 가정하겠다.
- 데이터베이스 테이블은 아주 단순하게
id(INTEGER)
,text(TEXT)
의 두 개 칼럼을 가진다. - 앱 내에 디폴트데이터를 담은 샘플 데이터베이스 파일을 포함하고 있어서 기본적으로는 이 파일의 사본을 만들어서 사용한다. (실제 많은 앱들이 사용하는 방법이기도 하다)
그리고 래퍼 컨테이너는 다음의 한가지 API만을 구현하겠다. 그외에 조건에 의한 검색이나, 데이터 추가/변경/삭제는 쿼리만 다를 뿐이고 여기서는 래퍼를 만들어서 Swift에서 사용하게끔 하는 부분만 참고하면 되겠다.
- 디폴트 메모 목록을 가져온다
- (NSArray *)getDefaultList;
그외에 추가적인 프로퍼티가 있다. 데이터 베이스 파일의 경로와, 파일이 해당 경로에 없을 때 번들로부터 샘플 데이터베이스를 복사하도록 하는 것이다. 단 이 프로퍼티들은 모듈 외부에서 참고할 필요는 없으므로 내부 인터페이스로 작성한다.
- 실제로 액세스할 파일 시스템 상의 데이터베이스 파일 경로 (
dbPath
) 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]
타입으로 변환되어 반입될 것이다.