NSDate를 SQLite DB에 저장을 해야 할일이 생겼습니다.

어떻게 할까 고민을 좀 해봤습니다.

NSDateFormatter를 이용해서, String으로 변환시킨후 저장을 하고, 다시 Formatter를 이용해서 NSDate로 Parsing하는 방법이 있는데,
Raw Data가 아닌 변환된값을쓴다는점과, Parsing에 들아가는 Cost등을 고려할때 좋은 방법이 아닐듯 싶습니다.

그래서 생각을 해보았는데, NSTimeInterval을 이용하기로 하였습니다.

NSDate에는 NSTimerInterval을 구해주는 여러 메소드가 있습니다.



timeIntervalSince1970을 이용하면, 1970년 1월 1일 부터의 duration을 second 단위로 얻을수 있습니다.

이걸 SQLite에 저장해두고, 다시 불러다가 쓰면 깔끔하게 구현이 될것 같습니다.

NSTimeInterval은 실제론 double 을 typedef 해서 정의된 자료형이기때문에, SQLITE의 column type을 'REAL'로 정의해서 저장하도록합니다.

DB Schema는 정의되어 있다고 가정하고, 아래는 NSTimeInterval을 DB에 삽입하는 코드 입니다.



그럼 DB에서 가져오는 코드를 보도록 하겠습니다.



첨에는 좀 까다롭지 않을까 싶었는데, 뭐 딱히 어려운게 없습니다 ㅎ
그냥 스윽~하면 스윽~되는거죠 ㅎㅎ

필요하시면 아래 예제 소스를 참조 하시고, 좋은 어플들 많이 개발하세요 ^^




제가 좋아하는 홍익대학교의 박지헌 교수님은 이런 말씀을 하셨습니다. ㅎ

"무협지에서 주인공이 무공비급을 보면서 절세의 고수로 거듭나는것처럼

우리는 세상에 널려있는 공개소스를 봐야한다"



그래서 저는 공개된 소스를 자주 분석 하는 편입니다.

최근에는 iPhone SDK에 대해 공부 하기떄문에 iPhone Sample code들을 분석하려고 노력하고 있습니다.

Apple에서 공개한 소스 인 만큼 가장 정석적인 코드라고 봐도 무방하지 않을까 합니다.  (얘들도 사람이기때문에 실수는 하겠지만 ㅎ)


iPhone SDK에서 DB 를 사용하는 법을 공부 하기 위해서 SQLiteBooks 라는 Sample Code 를결정했습니다.

프로그램 이름에도 나와 있듯이 iPhone SDK는 SQLite 를 DBMS로 사용하고 있습니다.

이번 리뷰는 DBMS 공부가 목적이기 때문에 UI part 는 제외하고 DBMS 사용 즉 sqlite3 쪽 사용에만 초점을 두고 진행하겠습니다.

목차는 아래와 같습니다 :)

1. Introduction
a. What does this App. do ?
2. Analyze
a. DB- AppDelegate
b. DB- Book Class


[1-a What does this App. do ( SQLiteBooks 는 어떤 일을 하는가 )? ]

제목에서 알수 있듯이, 책을 관리 하는 프로그램입니다.
책을 하나의 list 로 관리 하며, 하나의 책 object 는 세가지 속성 ( Title, Copyright, Author ) 을 가지게 됩니다.

직관적으로 알수 있듯이, 책 추가, 책 삭제, 책의 정보 보기, 정보 수정하기의 기능을 합니다.



[2-a DB - AppDelegate] 

ApplDelegate 는 DB 제어 용이 아니라, App. 시작과 종료등에 날라오는 Delegate 즉, Application Delegate Protocol 을 구현하는 Class 이다.
이 sample 에서는 AppDelegate에서 DB 제어에 사용되고 있다.
사실 DB 제어 하는 부분의 소스코드는 썩 맘에 들지는 않는다.
DB 접근이 Class AppDelegate 와 Class Book에서 나누어져 이루어져 있다.
DBM Class 를 구현해서 한곳으로 모았으면 어땠을까 하는 생각이 든다.

어찌되었건, Class 의 선언 부분을 보도록 하자.

// This includes the header for the SQLite library.
#import <sqlite3.h>

// Inform the compiler that the following classes are defined in the project:
@class Book, MasterViewController, DetailViewController, AddViewController, EditingViewController;

@interface AppDelegate : NSObject {
    IBOutlet UIWindow *window;
    IBOutlet UINavigationController *navigationController;
    NSMutableArray *books;
    // Opaque reference to the SQLite database.
    sqlite3 *database;
}

@property (nonatomic, retain) UIWindow *window;
@property (nonatomic, retain) UINavigationController *navigationController;

// Makes the main array of book objects available to other objects in the application.
@property (nonatomic, retain) NSMutableArray *books;

// Removes a book from the array of books, and also deletes it from the database. There is no undo.
- (IBAction)removeBook:(Book *)book;
// Creates a new book object with default data.
- (void)addBook:(Book *)book;

@end

먼저 Attributes 들을 보면
IB에서 생성된, window 와 navigationController,
Data 관리를 위한 books, database 가 있습니다.

UI 와 관련된 부분은 관심사가 아니기 때문에 Data 와 관련된 부분만 살펴 보도록 하겠습니다..

books 는 DB 에서 load 된 book list 들을 가지고 있는것이고,
database attribute 는 DB 접근을 위한 Object 입니다..
SQLite 는 많이들 사용하는 Mysql  과 달리, Server-Client 구조가 아니라,
하나의 파일을 open-read-write-close 하는 구조입니다..
sqlite 파일을 sqlite3_open 을 통해서 open 하고, query 를 통해서 read/write 하고 , sqlite3_close 를 통해서 close 합니다.
sqlite3 * 는 sqlite3_open 의 return type 이고, sqlite3_open 이외의 대부분의 api 가 sqilte3 *를 아규먼트로 요구 합니다.

더 자세한 정보는 sqlite3 홈페이지 에서 확인하시기 바랍니다.

자그럼 AppDelegate에서 구현하고 있는 Method 들에 대해서 살펴 보도록 하겠습니다.

@interface AppDelegate
// Removes a book from the array of books, and also deletes it from the database. There is no undo.
- (IBAction)removeBook:(Book *)book;
// Creates a new book object with default data.
- (void)addBook:(Book *)book;
@endif

// Private interface for AppDelegate - internal only methods.
@interface AppDelegate (Private)
- (void)createEditableCopyOfDatabaseIfNeeded;
- (void)initializeDatabase;
@end

// Implementation of Application Delegate Protocol
- (void)applicationDidFinishLaunching:(UIApplication *)application
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application
- (void)dealloc
- (void)applicationWillTerminate:(UIApplication *)application


여러가지가 있지만 우선 Application Delegate Protocol 을 구현하고 있는 메소드들 부터 살펴보도록 하겠습니다.
우선 applicationDidFinishiLaunching 메소드를 살펴 보도록하겠습니다.
applicationDidFinishiLaunching Delegate 는 다들 잘아시겠지만.
Application 이 실행된후 최초로 불러지는 Delegate 입니다.
최초로 불러 지는것인 만큼 주로 초기화가 필요 한 코드들을 실행하게 됩니다.
이 Application 에서는 MainView를 윈도우에 할당하고, DBMS 샘플 소스 인만큼 DB 관련 초기화 코드인
createEditableCopyOfDatabaseIfNeeded 와 initializeDatabase 를 불러 줍니다.

소스는 아래와 같습니다.

- (void)applicationDidFinishLaunching:(UIApplication *)application {
    // The application ships with a default database in its bundle. If anything in the application
    // bundle is altered, the code sign will fail. We want the database to be editable by users,
    // so we need to create a copy of it in the application's Documents directory.    
    [self createEditableCopyOfDatabaseIfNeeded];
    // Call internal method to initialize database connection
    [self initializeDatabase];
    // Add the navigation controller's view to the window
    [window addSubview:navigationController.view];
    [window makeKeyAndVisible];
}


다른 Delegate Protocol 들은 메모리 해제 등의 상투적인 일을 하고, 이번 소스코드 분석이 AppDelegate분석에 초점을 맞춘것이 아니기 때무에 넘어 가도록 하겠습니다


그럼 createEditableCopyOfDatabaseIfNeeded 와 initializeDatabase Method를 살펴 보도록하겠습니다.

우선 createEditableCopyOfDatabaseIfNeeded를 보도록하겠습니다.
이름에서 보다 시피 EditableDatabase를 생성하는 함수 입니다.
Project를 살펴보시면, Resource->bookdb.sql 이라는 파일로 DB 파일이 존재 하는걸 알수 있습니다.
그런데 왜 Editable 한 DB 가 필요 하냐고 생각하실수 있습니다.
" 그럼 Bundle로 들어간 DB 는 Read-Only 란 말이냐? " 라고 생각하실수 있는데.
네 Read-Only 입니다. Application Executable File 과 번들들은 iPhone의 Sandbox 라는 Read-Only 영역에 들어 가있습니다.
Application 에서 파일 생성및 수정등의 Write 작업이 필요 한 경우에는 $(APP_HOME) Directory 밑에서 이루어져야 합니다.
즉 DB를 수정하기 위해서는 $(APP_HOME) 로 옮기는 작업이 필요 합니다.
이작업은 App.이 최초에 실행될때 한번만 이루어지게 됩니다.

자 그럼 소스를 보도록 하겠습니다.

// Creates a writable copy of the bundled default database in the application Documents directory.
- (void)createEditableCopyOfDatabaseIfNeeded {
    // First, test for existence.
    BOOL success;
    // NSFileManager 는 iPhone SDK 에서 File I/O 를 담당하는 Class 입니다.
    // 이 Class 의 object 를 통해 Editable한 DB 의 존재 유무를 확인하거나 Copy할수 있습니다.
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSError *error;
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *writableDBPath = [documentsDirectory stringByAppendingPathComponent:@"bookdb.sql"];
    //NSFileManager를 통해 Writable DB 의 존재 유무를 확인합니다.
    success = [fileManager fileExistsAtPath:writableDBPath];
    // Writable한 DB 가 존재 할때는 아무작업도 하지 않습니다.
    if (success) return;
    // The writable database does not exist, so copy the default to the appropriate location.
    NSString *defaultDBPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"bookdb.sql"];
   //NSFileManager를 통해 Copy 하는 코드 입니다
    success = [fileManager copyItemAtPath:defaultDBPath toPath:writableDBPath error:&error];
    if (!success) {
        NSAssert1(0, @"Failed to create writable database file with message '%@'.", [error localizedDescription]);
    }
}


자 그럼 initializeDatabase Method 를 살펴보도록하겠습니다.

initializeDabase를 이름만 보고 판단했을때는, DB object를 초기화 하는구나 싶습니다.
하지만 이게 다가 아닙니다 =ㅁ=.
DB object 를 초기화 하고, DB 로 부터 UI display에 필요한 최소한의 정보들을 Load하는 일을 함께 합니다.
이런 명명법은 딱히 좋아 보이지 않습니다.

그럼 소스를 보면서 살펴보도록 하겠습니다.

// Open the database connection and retrieve minimal information for all objects.
- (void)initializeDatabase {
    // BookList 를 관리할 NSMutableArray Object인 bookArray를 생성하고, AppDelegate Object에 링크 시킵니다.
    NSMutableArray *bookArray = [[NSMutableArray alloc] init];
    self.books = bookArray;
    [bookArray release];
    // The database is stored in the application bundle.
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *path = [documentsDirectory stringByAppendingPathComponent:@"bookdb.sql"];
    // sqlite3_open 을 통해 DB 를 오픈 합니다.
    // 실패한 경우의 코드를 봐도 알수 있듯이, 실패 하더라도 꼭 sqlite3_close를 불러줘야,
    // Memory Leak이 발생하지 않습니다.
    // Open the database. The database was prepared outside the application.
    if (sqlite3_open([path UTF8String], &database) == SQLITE_OK) {
        // Get the primary key for all books.
        const char *sql = "SELECT pk FROM book";
        sqlite3_stmt *statement;
        //sqlite3 DB 파일에 쿼리를 던져주고, 처리 하기 위해서 sqlite3_stmt 라는 구조체를 사용합니다.
        //query 를 바인딩하고, sqlite3_step 등의 api 를 통해서 행을 처리하게 됩니다.
       // statement 를 이용해서 작업이 끝난 후에는 sqlite3_finalize를 불러줘서 Memory를 해제 해야합니다.
        // Preparing a statement compiles the SQL query into a byte-code program in the SQLite library.
        // The third parameter is either the length of the SQL string or -1 to read up to the first null terminator.       
        if (sqlite3_prepare_v2(database, sql, -1, &statement, NULL) == SQLITE_OK) {
            // We "step" through the results - once for each row.
            while (sqlite3_step(statement) == SQLITE_ROW) {
                // The second parameter indicates the column index into the result set.
                int primaryKey = sqlite3_column_int(statement, 0);
                // We avoid the alloc-init-autorelease pattern here because we are in a tight loop and
                // autorelease is slightly more expensive than release. This design choice has nothing to do with
                // actual memory management - at the end of this block of code, all the book objects allocated
                // here will be in memory regardless of whether we use autorelease or release, because they are
                // retained by the books array.
                Book *book = [[Book alloc] initWithPrimaryKey:primaryKey database:database];
                [books addObject:book];
                [book release];
            }
        }
        // "Finalize" the statement - releases the resources associated with the statement.
        sqlite3_finalize(statement);
    } else {
        // Even though the open failed, call close to properly clean up resources.
        sqlite3_close(database);
        NSAssert1(0, @"Failed to open database with message '%s'.", sqlite3_errmsg(database));
        // Additional error handling, as appropriate...
    }
}


자 DB 초기화 하는 부분까지 살펴 봤고, 이제 Public Method 인 addBook 과 removeBook 에 대해서 살펴보도록하겠습니다.
addBook 은 Book object 를 받아서 class Book 의 method insertIntoDataBase를 통해 DB에 추가 해주고,
Book List 를 관리하는 bookArray에 추가를 해주는것으로 끝이 납니다.
간단함으로 소스는 살펴보지 않도록 하겠습니다.

removeBook 의 경우 IBAction 즉 IB 를 통해 정의된 Method 입니다.
IB 를 열어서 살펴보면 아시겠지만 Connection 은 이루어 져 있지 않습니다.


addBook 함수 처럼 왜 그냥 선언하지 않고 IB 를 통해서 정의 했는지는 잘 모르겠습니다.
혹시 아시는 분은 좀 알려주시기 바랍니다 :)
removeBook은 addBook과 반대의 일을 합니다. DB 로부터 삭제, bookArray로 부터 제거.
addBook 과 마찬가지로 코드가 간단하기 떄문에 소스를 샆펴보지 않겠씁니다.

자 이것으로 AppDelegate Class 의 구현을 모두 살펴 보았습니다 :)

[2-b DB- Class Book]

자 그럼 Book Class 에 대해서 살펴 보도록 하겠습니다.

Book Class 의 선언은 다음과 같습니다.

@interface Book : NSObject {
    // Opaque reference to the underlying database.
    sqlite3 *database;
    // Primary key in the database.
    NSInteger primaryKey;
    // Attributes.
    NSString *title;
    NSDate *copyright;
    NSString *author;
    // Internal state variables. Hydrated tracks whether attribute data is in the object or the database.
    BOOL hydrated;
    // Dirty tracks whether there are in-memory changes to data which have no been written to the database.
    BOOL dirty;
    NSData *data;
}

// Finalize (delete) all of the SQLite compiled queries.
+ (void)finalizeStatements;

// Creates the object with primary key and title is brought into memory.
- (id)initWithPrimaryKey:(NSInteger)pk database:(sqlite3 *)db;
// Inserts the book into the database and stores its primary key.
- (void)insertIntoDatabase:(sqlite3 *)database;

// Brings the rest of the object data into memory. If already in memory, no action is taken (harmless no-op).
- (void)hydrate;
// Flushes all but the primary key and title out to the database.
- (void)dehydrate;
// Remove the book complete from the database. In memory deletion to follow...
- (void)deleteFromDatabase;
@end

Attribute 의 다른 부분들은 직관적으로 아실꺼라 생각합니다. 눈여겨봐야 할것은
hydrated , dirty 가 아닌가 생각합니다.

Mobile 환경에서의 부족한 메모리를 효율적으로 상용하기 위해 Book Class 의 경우 Cache Concept을 사용하였습니다.
기본적으로 Display 하기에는 title 만 있으면 됩니다.
그래서 Class Appdegate 의 메소드 initializeDatabase에서 살펴 봤듯이 최초에 key와 title만 가져옵니다.
이때 hydrated = NO 로 세팅하고 실제 copyright 등의 data 가 필요 할때 DB 로 부터 가져와서 채워주게 됩니다.
이때 hydrated = YES 로 표기 하게 되는것이죠.
그리고 Application Delegate Protocol 중 하나인 applicationDidReceiveMemoryWarning:(UIApplication *)application 이 불러 졌을때
hydrated 된 object 들을 dehydrate 해줌으로써 메모리 효율을 늘려주는식의 구현이 되어 있습니다.

그리고 dirty 필드는 edit 가 일어 났을때 ( 이때는 항상 hydrated=YES 임) 바로 DB 에 써주는것은 성능에 좋지 않기때문에
기왕 Memory에 관리 하고 있는 book object 에만 기록하고 DB 에는 나중에 쓰는 delayed-Write를 구현하기 위한 attribute입니다.

applicationDidReceiveMemoryWarning 프로토콜이 불러졌을때 혹은 App.을 종료할때 Write 해줌으로써 프로그램 성능을 높이는 형태입니다.

자 그럼 Method 들에대해서 살펴 봐야 하는데 그전에 Class Book 에서 sqlite3_statement 를 관리 하는 구조를 살펴 보도록하겠습니다.

book.m 파일 상단에 보면 아래와 같은 코드를 볼수 있습니다.

static sqlite3_stmt *insert_statement = nil;
static sqlite3_stmt *init_statement = nil;
static sqlite3_stmt *delete_statement = nil;
static sqlite3_stmt *hydrate_statement = nil;
static sqlite3_stmt *dehydrate_statement = nil;

이 코드는 book object 별로 db 제어를 해야 하는데, object 별로 statement 를 관리하면, 같은 statement 가 여러번 할당되어
낭비를 막고자 작성된 코드 입니다.

사견을 덧붙히자면, DB 제어를 한곳으로 모았다면 사실 이런 귀찮은 방법을 써가면서 메모리 효율을 생각할 필요까진 없지 않았을까 생각합니다.

여튼 최초에 nil 로 되어 있다가 최초에 statement 가 할당될때, 초기화 하고 ( 사용하지 않을수도 있기 때문에 )
프로그램 종료시에 해제 해주게 됩니다.

자 그럼 Method들을 살펴 보도록 하겠습니다.

- (id)initWithPrimaryKey:(NSInteger)pk database:(sqlite3 *)db

당연히 book object 를 initiailize 하는 함수 입니다 ㅎ

그럼 코드를 살펴 보도록 하겠습니다

        if (init_statement == nil) {
            // 앞서 설명 드린대로 init_statement는 global variable 이고, 최초에 사용할때 이를 할당하게 됩니다.
            // query 의 마지막 부분 pk=? 를 보시면 pk 는 primary key field 이고 value가 ? 인것을 알수 있습니다.
            // '?' 는 printf 같은 함수를 쓸대 '%s'등 과 같다고 보시면 될듯합니다 값은 sqlite3_bind_xx 함수로 채우게 됩니다.
// Note the '?' at the end of the query. This is a parameter which can be replaced by a bound variable.
            // This is a great way to optimize because frequently used queries can be compiled once, then with each
            // use new variable values can be bound to placeholders.
            const char *sql = "SELECT title FROM book WHERE pk=?";
            if (sqlite3_prepare_v2(database, sql, -1, &init_statement, NULL) != SQLITE_OK) {
                NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database));
            }
        }
        // For this query, we bind the primary key to the first (and only) placeholder in the statement.
        // Note that the parameters are numbered from 1, not from 0.
        // query 의 ? 부분을 채워주는 부분
        sqlite3_bind_int(init_statement, 1, primaryKey);
        if (sqlite3_step(init_statement) == SQLITE_ROW) {
            self.title = [NSString stringWithUTF8String:(char *)sqlite3_column_text(init_statement, 0)];
        } else {
            self.title = @"No title";
        }
        // 재사용을 위해 reset 을 해주는 아마도 bind를 해제 하는것 같습니다.
        // Reset the statement for future reuse.
        sqlite3_reset(init_statement);

- (void)insertIntoDatabase:(sqlite3 *)db
- (void)deleteFromDatabase

 이 Method 들은 init함수와 마찬가지로 DB 제어 를 통해서 read/write 하는 부분들이기때문에 initWithPrimaryKey 와 모습이 거의 비슷합니
다. 그러므로 생략하고 넘어 가도록 하겠습니다.

- (void)hydrate
- (void)dehydrate

이 Method 들 역시 앞서 설명드린, Cache 컨셉과 , Dirty Filed 컨셉을 위해 DB 와 Book Object 연동을 다룬 Method이기때문에
앞의 설명을 숙지하고 코드를 보시면 충분히 이해하실수 있따고 생각하여 넘어가도록 하겠습니다.

이것으로 SQLibteBooks DB 제어관련 소스 리뷰는 마치도록 하겠습니다

장문의 글 읽어주셔서 감사합니다 :)

+ Recent posts