한밤 중 1시 51분,

잘까 말까 잠시 망설이다가,

DB에 대한 포스팅을 마무리하기 위해 세수를 하고 왔습니다.


이 포스팅에는 대단한 스키마가 나오는 것도 아니고,

어마무시한 쿼리문도 나오지 않습니다.


그저 비몽사몽 간에,

앱단에서 사용할만한 함수 몇가지를 공유하고자 합니다.


타이젠에서 공식적으로 지원하는 DB는 SQLite3입니다.

(참고, "Tizen 플랫폼 DB 엿보기-", http://storycompiler.tistory.com/25)

오래전부터 SQLite였고 그 기조는 당분간 바뀌지 않을겁니다.

따라서 SQLite를 다른 DB로 포팅할 일도 없겠군요.


그렇지만, 순전히 앱사이드에서 sqlite3를 덕지덕지 소스에 붙여 놓고 싶지도 않습니다.

sqlite3_open() / sqlite3_close()를 매번 수행하며,

그 때마다 온갖 예외처리를 하다보면,

분명 비슷한 루틴이 반복될 것이고,

그러는 와중에 버그라는 친숙한 녀석을 만나게 될겁니다.


따라서 sqlite3를 앱 내부 함수에 캡슐로 감춰서

조금이라도 노출을 막고,

조금이라도 쉽게 사용하고자 합니다.


static struct {
    sqlite3 *db;
} db_info = {
    .db = NULL,
};


우선, sqlite3_open()으로 나오는 db 핸들은 static 전역 변수로 선언해볼까요?

전역 변수는 매우 위험하여 가급적이면 사용하진 않지만,

- 개발하고자 하는 앱은 오직 한 프로세스에서만 사용하고,

- 단일스레드로 동작하며,

- reentrant 따위는 일어날리도 없기에,

단일 파일에서만 접근할 수 있는 static 전역 변수로 선언하곤 합니다.


이로써 모든 함수의 첫번째 인자(sqlite3 *db)는 생략할 수 있게 된 셈이지요.

덕분에 API 사용이 간단해졌습니다.

하지만, 이러한 제한적인 상황 외에 전역변수를 남용한다면,

숨겨진 버그를 중요한 순간마다 튀어나와 정신건강에 치명적인 독이 되고 맙니다. :)

특히, 라이브러리에서 전역변수는 고민에 고민을 거듭하여 제거하는게 좋겠지요.


db_open()은 이전 포스팅에서 설명한 바 있습니다.

Tizen 플랫폼에서 앱의 데이터 저장공간을 app_get_data_path()로 얻어와 DB 파일을 저장합니다.

(참고, "Tizen 앱 DB는 언제 어디에 초기화할까", http://storycompiler.tistory.com/29)


HAPI appl_error_e db_open(void)
{
    char *path = NULL;
    char db_file[FILE_LEN] = {0, };
    int ret = SQLITE_OK;

    path = app_get_data_path();
    retv_if(!path, APPL_ERROR_FAIL);
   
    snprintf(db_file, sizeof(db_file), "%s/%s", path, APP_DB_FILE);

    ret = sqlite3_open(db_file, &db_info.db);
    if (SQLITE_OK != ret) {
        _E("%s", sqlite3_errmsg(db_info.db));
        free(path);
        return APPL_ERROR_FAIL;
    }

    free(path);
    return APPL_ERROR_NONE;
}

HAPI void db_close(void)
{
    if (!db_info.db) {
        _D("DB is already NULL");
        return;
    }

    sqlite3_close(db_info.db);
    db_info.db = NULL;
}


db_open()과 db_close()는 db 핸들을 전역으로 뽑아놨기 때문에 함수 패러미터도 없습니다.

따라서 원하는 시점에 인자에 대한 고민없이 open / close 하면 됩니다.

편하군요~


HAPI sqlite3_stmt *db_prepare(const char *query)
{
    sqlite3_stmt *stmt = NULL;
    int ret = SQLITE_OK;

    retv_if(!query, NULL);

    ret = sqlite3_prepare_v2(db_info.db, query, strlen(query), &stmt, NULL);
    if (SQLITE_OK != ret) {
        _E("%s, %s", query, sqlite3_errmsg(db_info.db));
        return NULL;
    }

    return stmt;
}
HAPI appl_error_e db_next(sqlite3_stmt *stmt)
{
    int ret = SQLITE_OK;

    retv_if(!stmt, APPL_ERROR_FAIL);

    ret = sqlite3_step(stmt);
    switch (ret) {
    case SQLITE_ROW:
        return APPL_ERROR_NONE;
    case SQLITE_DONE:
        return APPL_ERROR_NO_DATA;
    default:
        _E("%s", sqlite3_errmsg(db_info.db));
        return APPL_ERROR_FAIL;
    }

    return APPL_ERROR_NONE;
}
HAPI appl_error_e db_reset(sqlite3_stmt *stmt)
{
    int ret = SQLITE_OK;

    retv_if(!stmt, APPL_ERROR_INVALID_PARAMETER);

    ret = sqlite3_reset(stmt);
    if (SQLITE_OK != ret) {
        _E("%s", sqlite3_errmsg(db_info.db));
        return APPL_ERROR_FAIL;
    }

    sqlite3_clear_bindings(stmt);

    return APPL_ERROR_NONE;
}


db_prepare(), db_next(), db_reset()은 sqlite3_prepare_v2(), sqlite3_next(), sqlite3_reset() 기능을 수행하기 위한 함수입니다.

우선, db_prepare()에서 패러미터로 전달받은 쿼리문을 파싱하여 sqlite3 *stmt를 리턴하죠.

stmt는 db_next()에 인자로 들어가서 실행됩니다.

db_reset()에서는 stmt를 재사용하기 위해 bind된 인자가 있으면 clear 합니다.

반드시 clear를 하고 다시 bind를 해야합니다.

어렴풋 reset을 제대로 하지 않아서 삽질을 했던 기억이 떠오르네요.

여기서 bind는 이어서 설명하는 db_bind_xxxx()함수와 연결됩니다.


HAPI appl_error_e db_bind_bool(sqlite3_stmt *stmt, int idx, bool value)
{
    int ret = SQLITE_OK;

    retv_if(!stmt, APPL_ERROR_FAIL);

    ret = sqlite3_bind_int(stmt, idx, (int) value);
    if (SQLITE_OK != ret) {
        _E("%s", sqlite3_errmsg(db_info.db));
        return APPL_ERROR_FAIL;
    }

    return APPL_ERROR_NONE;
}
HAPI appl_error_e db_bind_int(sqlite3_stmt *stmt, int idx, int value) {
    int ret = SQLITE_OK;

    retv_if(!stmt, APPL_ERROR_FAIL);

    ret = sqlite3_bind_int(stmt, idx, value);
    if (SQLITE_OK != ret) {
        _E("%s", sqlite3_errmsg(db_info.db));
        return APPL_ERROR_FAIL;
    }

    return APPL_ERROR_NONE;
}
HAPI appl_error_e db_bind_double(sqlite3_stmt *stmt, int idx, double value)
{
    int ret = SQLITE_OK;

    retv_if(!stmt, APPL_ERROR_FAIL);

    ret = sqlite3_bind_double(stmt, idx, value);
    if (SQLITE_OK != ret) {
        _E("%s", sqlite3_errmsg(db_info.db));
        return APPL_ERROR_FAIL;
    }

    return APPL_ERROR_NONE;
}
HAPI appl_error_e db_bind_str(sqlite3_stmt *stmt, int idx, const char *str)
{
    int ret = SQLITE_OK;

    retv_if(!stmt, APPL_ERROR_FAIL);
    retv_if(!str, APPL_ERROR_FAIL);

    ret = sqlite3_bind_text(stmt, idx, str, strlen(str), SQLITE_TRANSIENT);
    if (SQLITE_OK != ret) {
        _E("%s", sqlite3_errmsg(db_info.db));
        return APPL_ERROR_FAIL;
    }

    return APPL_ERROR_NONE;
}


바인드는 db_prepare()에서 파싱한 쿼리문 중에 '?'로 처리한 부분에 치환되어 들어갑니다.

문자열 쿼리문을 더 빨리 처리하기 위해,

확정된 부분은 db_prepare()로 먼저 파싱하고,

확정할 수 없는 부분은 sqlite3_bind_xxxx()로 차후에 입력하게 됩니다.


예를 들어,

query = "select sequence from bookmarks where parent=? order by sequence desc";

(참고, git://review.tizen.org/apps/web/browser, src/database/browser-bookmark-db.cpp)

위와 같은 쿼리문의 where 절에 '?'로 처리된 부분은,

sqlite3_bind_int로 차후에 채워넣게 됩니다.


단, 항상 헛갈리는 부분인데 bind에 들어가는 index는 0이 아닌 1로 시작합니다. 응?

위의 예에서 parent의 '?'에 바인드하기 위해서는 '0'이 아니라 '1'을 index로 넣어야 합니다.

오래 전에 SQLite 서적을 보며 1부터 시작하는 납득할 만한 이유를 찾았던 것으로 기억하는데,

더 이상 기억이 나지 않는 것으로 보아 충분히 납득하지 못했었나 봅니다.


HAPI bool db_get_bool(sqlite3_stmt *stmt, int index)
{
    retv_if(!stmt, false);
    return (bool) sqlite3_column_int(stmt, index);
}
HAPI int db_get_int(sqlite3_stmt *stmt, int index)
{
    retv_if(!stmt, 0);
    return sqlite3_column_int(stmt, index);
}
HAPI int db_get_double(sqlite3_stmt *stmt, int index)
{
    retv_if(!stmt, 0);
    return sqlite3_column_double(stmt, index);
}
HAPI const char *db_get_str(sqlite3_stmt *stmt, int index)
{
    retv_if(!stmt, NULL);
    return (const char *) sqlite3_column_text(stmt, index);
}


위의 함수군은 select 쿼리의 결과로 나온 값들을 얻기 위해 사용합니다.

index는 0부터 차례대로 카운팅됩니다.

bind처럼 1부터 시작하지 않고 0부터 시작하니 헛갈리지 마세요.


HAPI appl_error_e db_finalize(sqlite3_stmt *stmt)
{
    int ret = SQLITE_OK;

    retv_if(!stmt, APPL_ERROR_INVALID_PARAMETER);

    ret = sqlite3_finalize(stmt);
    if (SQLITE_OK != ret) {
        _E("%s", sqlite3_errmsg(db_info.db));
        return APPL_ERROR_FAIL;
    }

    return APPL_ERROR_NONE;
}


stmt를 더 이상 사용할 필요가 없다면 db_finalize를 해야합니다.

이로써 db_prepare()에서 생성된 stmt의 라이프사이클이 드디어 종료됩니다.

db_prepare()로 핸들을 생성하였으면,

반드시 db_finalize()까지 진행해주세요.


HAPI appl_error_e db_exec(const char *query)
{
    sqlite3_stmt *stmt = NULL;

    retv_if(!query, APPL_ERROR_INVALID_PARAMETER);

    stmt = db_prepare(query);
    retv_if(!stmt, APPL_ERROR_FAIL);

    goto_if(APPL_ERROR_FAIL == db_next(stmt), ERROR);
    goto_if(APPL_ERROR_FAIL == db_finalize(stmt), ERROR);

    return APPL_ERROR_NONE;

ERROR:
    if (stmt) db_finalize(stmt);
    return APPL_ERROR_FAIL;
}


db_exec()는 create / drop / insert / update / delete 처럼,

유의미한 실행결과를 얻을 필요가 없는 쿼리문에 사용합니다.


쿼리문과 db_exec()만 사용하면 얼마든지 원하는 쿼리문을 수행할 수 있습니다.

하지만, 너무나 쉽게 사용할 수 있다보니 해킹의 위험에 노출될 수도 있죠.

개발자가 최초에 쿼리문을 하나 만들어 sqlite3_exec()에 넣었다고 해보죠.

해커가 쿼리문의 "%s" 같은 부분을 조작하여 다중 쿼리문으로 바꿔치기하면,

력된 다중 쿼리문이 sqlite3_exec()에 의해 모두 실행됩니다.

난리나게 되는 거죠.


따라서 언제나 오직 하나의 쿼리문만 실행할 수 있도록,

db_exec() 내부에서 prepare, next, finalize를 수행하도록 변경하였습니다.

next는 하나의 쿼리문만 수행하도록 설계가 되어 있답니다.


HAPI appl_error_e db_begin_transaction(void)
{
    int ret = SQLITE_BUSY;

    while (1) {
        ret = sqlite3_exec(db_info.db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL);
        if (SQLITE_BUSY != ret) {
            break;
        }
        /* FIXME : we have to fix this sleep */
        sleep(1);
    }

    if (SQLITE_OK != ret) {
        _E("sqlite3_exec() Failed(%d)", ret);
        return APPL_ERROR_FAIL;
    }

    return APPL_ERROR_NONE;
}



HAPI appl_error_e db_end_transaction(void)
{
    int ret = SQLITE_OK;

    while (1) {
        ret = sqlite3_exec(db_info.db, "COMMIT TRANSACTION", NULL, NULL, NULL);
        if (SQLITE_BUSY != ret) {
            break;
        }
        /* FIXME : we have to fix this sleep */
        sleep(1);
    }

    if (SQLITE_OK != ret) {
        _E("sqlite3_exec() Failed(%d)", ret);
        return APPL_ERROR_FAIL;
    }

    return APPL_ERROR_NONE;
}


트랜잭션이 필요할 수도 있겠네요.

BEGIN TRANSACTION - COMMIT TRANSACTION 사이에 벌어지는 디비 루틴은 한 번에 처리됩니다.


다만, 상기 함수에서 트랜잭션 쿼리를 수행할 때,

SQLITE_BUSY가 나는 경우 sleep()을 걸어놓았습니다.

sleep()은 프로세스 전체를 멈추게 할 수도 있으니 신중하게 사용해야 합니다.

프로세스의 디자인에 따라 이 부분은 각자의 mainloop()에 사용할 수 있는 timer 등으로 치환할 수도 있습니다.


이제는 정말 취침에 들어가야겠네요.

집중력이 바닥났습니다.


끝_


* 2015. 6. 2 정신차리고 일어나서 한밤중에 멋대로 휘갈긴 문장을 수정.


  1. developer 2016.06.29 16:12

    애정이 느껴지네요 잘보고 갑니다 ㅎㅎ

    • 안녕하세요, 개발자님. 애정은 넘치죠. 하하하. 제가 지금은 러시아에 나와 있어서 잠시 타이젠과 멀어져 있긴 한데... 언제나 애정만큼은 충만해있어요.

git에 대한 변변한 서적이 한 권 없던 시절,

한 선구자가 프로젝트에 전격적으로 git을 도입하였습니다.

이미 다른 형상관리툴에 익숙한 개발자들은 git이 결코 달갑지 않았습니다.

기존 툴을 장점을 수없이 열거하며 자신들에게 익숙한 과거로 회귀하자는 르네상스운동을 벌였습니다.

하지만 몇 년이 지난 지금...

이제는 git에 익숙해진 대다수 개발자들은,

'아직도 git을 사용하지 않는' 개발자들을 나무랄 정도로 git 신봉자가 되어버렸습니다.

이는 비단 우리 집단에서만 벌어진 일은 아닐 것입니다.

 

 

git은 가볍고 빠르죠.

'쉘환경이 윈도우보다 편한 개발자들'에게는 다른 툴보다 쉬울 지도 모릅니다-

 

다른 형상관리툴을 압도하기 위해,

git은 영악하게 설계되었습니다.

 

그리고 그 설계의 핵에는 4가지 원소가 있습니다.

commit, tree, blob, tag.

 

C언어에 char, int, long, float, double과 같은 데이터 타입이 있는 것처럼,

git은 내부적으로 commit, tree, blob, tag의 4가지 오브젝트 타입을 관리합니다.

 

이러한 오브젝트는 .git/objects에 개별적인 파일들로 존재합니다.

하나의 commit, 하나의 tree, 하나의 blob 그리고 하나의 tag는 각각 하나의 파일입니다.

두개의 commit은- 당연히 두개의 파일입니다.

 

오브젝트가 담긴 파일의 이름은 git이 오브젝트 컨텐츠의 내용을 참고하여 생성하는 40자리 문자열입니다.

git에 "hello.txt"라는 파일을 하나 추가하면,

"hello.txt"라는 이름의 오브젝트를 생성하는 것이 아닙니다.

"hello.txt"의 내용 전부를 해시테이블에 넣어,

40자리의 해시값을 뽑아내어 오브젝트 파일 이름으로 사용합니다.

 

그렇다면, "hello.txt"라는 이름은 어디에 저장되는 것일까요?

"hello.txt"를 위한 오브젝트인 blob에는 파일이름인 "hello.txt"라는 문자열이 저장되지 않습니다.

대신 디렉토리 구조를 나타내는 tree 오브젝트에서 "hello.txt"라는 문자열을 찾을 수 있습니다.

이는 리눅스 파일시스템에서 흔히 사용하는 inode - dentry의 관계와 동일합니다.

(이에 대한 설명은 본 주제와 무관하므로 생략합니다.)

리눅스를 개발한 리누즈가 git에도 동일한 개념을 차용했다는 점은 쉽게 유추할 수 있습니다.

 

이러한 오브젝트들은 하나의 파일로 .git/objects에 차곡차곡 쌓이게 되는데,

한 디렉토리에 너무 많은 파일이 있으면 파일 시스템의 성능이 저하될 수 있기 때문에,

오브젝트의 파일이름 중 앞 2글자는 디렉토리 이름으로 사용하고,

나머지 38글자를 파일이름으로 사용하게 됩니다.

 

각각의 오브젝트 타입마다 담고 있는 내용은 아래와 같습니다.

 

 blob(binary large object)

- 타입 : "blob" 타입
- 사이즈 : 컨텐츠의 용량을 bytes로 표시
- 컨텐츠 : blob의 컨텐츠에는 텍스트, 이미지, 음악 혹은 단순 이진 파일처럼 다양한 형식의 파일이 저장될 수 있다.
  파일이름이나 파일형식은 blob에 저장되지 않는다.
  파일의 메타정보를 제외한 파일의 내용 전체를 품는다.

 

 tree

- 타입 : "tree" 타입
- 사이즈 : 트리 오브젝트의 용량을 bytes로 표시
- tree 객체 : 하위 디렉토리의 트리 객체를 재귀적으로 참조할 수 있다.
- blob 객체 : 한 디렉토리에 있는 모든 blob을 담고 있다.
   객체에 대한 접근권한, 파일이름은 여기서 관리한다.

 

 commit

- 작성자
- 커밋 실행자
- 커밋 날짜
- 로그 메시지
- tree 객체 : 해당 커밋에서의 dir/file의 상태를 알 수 있다.

 

 tag

- 객체종류
- 태그이름
- tagger
- 태그메시지
- PGP 서명정보


git에는 고맙게도 오브젝트의 상태정보를 얻을 수 있는 명령어가 노출되어 있습니다.

git cat-file을 오브젝트에 사용하여 오브젝트가 품고 있는 정보를 자세히 알아보려 합니다.

usage: git cat-file (-t|-s|-e|-p|<type>|--textconv) <object>
   or: git cat-file (--batch|--batch-check) < <list_of_objects>

<type> can be one of: blob, tree, commit, tag
    -t                    show object type
    -s                    show object size
    -e                    exit with zero when there's no error
    -p                    pretty-print object's content
    --textconv            for blob objects, run textconv on object's content
    --batch[=<format>]    show info and content of objects fed from the standard input
    --batch-check[=<format>]
                          show info about objects fed from the standard input


- 우선, 새로운 git repository를 하나 만듭니다.

$ git init
$ ls -la .git/objects/
.git/objects/:
합계 16
drwxrwxr-x 4 jinux jinux 4096 4월 6 00:57 .
drwxrwxr-x 7 jinux jinux 4096 4월 6 00:57 ..
drwxrwxr-x 2 jinux jinux 4096 4월 6 00:57 info
drwxrwxr-x 2 jinux jinux 4096 4월 6 00:57 pack

위의 .git/object 디렉토리에는 info와 pack 디렉토리가 있습니다.

이 두 디렉토리가 오브젝트는 아닙니다.

 

- "hello.txt" 를 추가해보자.

$ echo "Hello" > Hello.txt
$ git add Hello.txt
$ git commit -m "Add Hello.txt"
[master (root-commit) 44cac0a] Add Hello.txt
1 file changed, 1 insertion(+)
create mode 100644 Hello.txt
$ ls -la .git/objects/
합계 28
drwxrwxr-x 7 jinux jinux 4096 4월 6 02:03 .
drwxrwxr-x 8 jinux jinux 4096 4월 6 02:03 ..
drwxrwxr-x 2 jinux jinux 4096 4월 6 02:03 2e
drwxrwxr-x 2 jinux jinux 4096 4월 6 02:03 44
drwxrwxr-x 2 jinux jinux 4096 4월 6 02:03 e9
drwxrwxr-x 2 jinux jinux 4096 4월 6 00:57 info
drwxrwxr-x 2 jinux jinux 4096 4월 6 00:57 pack

위에 총 3개의 오브젝트(2e, 44, e9)가 생겼습니다.

파일 하나(e9)를 추가했고,

파일이 담긴 tree(2e)가 추가되었고,

커밋(44)이 하나 추가되었습니다.

우선, 커밋의 내용을 살펴보죠.

 

$ git cat-file -p 44cac0afe16f0819bba6cc2332b9418ba3c1ce8b
tree 2ea873e13e84497d7459150a0b2b662403e3bc2b
author Storycompiler <storycompiler@hanmail.net> 1428253401 +0900
committer Storycompiler <storycompiler@hanmail.net> 1428253401 +0900

Add Hello.txt

커밋이 가리키는 tree ID가 명시되어 있습니다.

author와 committer가 시간과 함께 기록되어 있습니다.

그리고 마지막 줄은 메시지가 있습니다.

위의 커밋이 가리키는 트리도 살펴보죠.

 

$ git cat-file -p 2ea873e13e84497d7459150a0b2b662403e3bc2b
100644 blob e965047ad7c57865823c7d992b1d046ea66edf78 Hello.txt

트리는 저장된 오브젝트가 blob인지 tree이지 가리는 인자가 권한정보를 뒤이어 나옵니다.

그리고 오브젝트의 ID와 파일 혹은 디렉토리이름이 나옵니다.

파일명 혹은 디렉토리명을 트리에서 관리한다는 것을 다시 확인할 수 있습니다.

 

 

blob가 가리키는 파일을 아래와 같습니다.

 

$ git cat-file -p e965047ad7c57865823c7d992b1d046ea66edf78
Hello

blob에는 파일의 내용만 저장되어 있습니다.

그 외의 메타정보는 찾을 수가 없습니다.

 

- "world.txt"를 추가해보죠.

 

$ echo "World" > "world.txt"
$ git add world.txt
$ git commit -m "Add world.txt"
[master 839b975] Add world.txt
1 file changed, 1 insertion(+)
create mode 100644 world.txt
$ ls -la .git/objects/
합계 40
drwxrwxr-x 10 jinux jinux 4096 4월 6 02:34 .
drwxrwxr-x 8 jinux jinux 4096 4월 6 02:34 ..
drwxrwxr-x 2 jinux jinux 4096 4월 6 02:34 08
drwxrwxr-x 2 jinux jinux 4096 4월 6 02:33 21
drwxrwxr-x 2 jinux jinux 4096 4월 6 02:03 2e
drwxrwxr-x 2 jinux jinux 4096 4월 6 02:03 44
drwxrwxr-x 2 jinux jinux 4096 4월 6 02:34 83
drwxrwxr-x 2 jinux jinux 4096 4월 6 02:03 e9
drwxrwxr-x 2 jinux jinux 4096 4월 6 00:57 info
drwxrwxr-x 2 jinux jinux 4096 4월 6 00:57 pack

 

오브젝트가 6개가 되었습니다.

기존의 오브젝트(2e, 44, e9)는 그대로 있고,

커밋(83), 트리(08), 새로운 파일(21) 3개의 오브젝트 파일이 추가되었습니다.

커밋이나 파일은 모두 새로운 내용이므로 새로운 파일이 당연합니다.

그렇다면 트리는 왜 추가되었을까요?

왜냐하면 트리는 blob에 대한 내용물을 담고 있는데,

파일이 하나 늘면서 해당 blob을 트리에 추가하였기 때문입니다.

$ git cat-file -p 08be0f512375756b66561c0dcfb8429cd2ab5193
100644 blob e965047ad7c57865823c7d992b1d046ea66edf78	Hello.txt
100644 blob 216e97ce08229b8776d3feb731c6d23a2f669ac8	world.txt

- "subdir" 디렉토리를 추가한 후, "hello.txt"와 "world.txt"를 복사해보죠.

$ mkdir subdir
$ ls
Hello.txt  subdir  world.txt
$ cp Hello.txt subdir/
$ cp world.txt subdir/
$ git add subdir
$ git commit -m "Add subdir"
[master 2f5bfb4] Add subdir
 2 files changed, 2 insertions(+)
 create mode 100644 subdir/Hello.txt
 create mode 100644 subdir/world.txt
$ ls -al .git/objects/
합계 48
drwxrwxr-x 12 jinux jinux 4096  4월  6 02:44 .
drwxrwxr-x  8 jinux jinux 4096  4월  6 02:44 ..
drwxrwxr-x  2 jinux jinux 4096  4월  6 02:34 08
drwxrwxr-x  2 jinux jinux 4096  4월  6 02:33 21
drwxrwxr-x  2 jinux jinux 4096  4월  6 02:03 2e
drwxrwxr-x  2 jinux jinux 4096  4월  6 02:44 2f
drwxrwxr-x  2 jinux jinux 4096  4월  6 02:03 44
drwxrwxr-x  2 jinux jinux 4096  4월  6 02:44 6f
drwxrwxr-x  2 jinux jinux 4096  4월  6 02:34 83
drwxrwxr-x  2 jinux jinux 4096  4월  6 02:03 e9
drwxrwxr-x  2 jinux jinux 4096  4월  6 00:57 info
drwxrwxr-x  2 jinux jinux 4096  4월  6 00:57 pack


subdir을 생성하고,

파일 2개를 복사해서 넣었습니다.

하지만 현재 총 오브젝트 파일은 기존에서 2개만 추가된 8개입니다.

기존의 오브젝트 파일(2e, 44, e9, 83, 08, 21)에서 2f, 6f 오브젝트만 추가되었습니다.

이 파일 두개는 무엇일까요?


$ git cat-file -p 2f5bfb452a671dc72b9f1ac17865212c98311262 
tree 6f3f9a776e764914ce54a476e39155d1fe4ddf65
parent 839b97545a2ec5a34a8c6f0152be72b418a82df3
author Storycompiler  1428255851 +0900
committer Storycompiler  1428255851 +0900

우선 2f 파일은 신규로 생성한 커밋 오브젝트 파일입니다.


$ git cat-file -p 6f3f9a776e764914ce54a476e39155d1fe4ddf65
100644 blob e965047ad7c57865823c7d992b1d046ea66edf78	Hello.txt
040000 tree 08be0f512375756b66561c0dcfb8429cd2ab5193	subdir
100644 blob 216e97ce08229b8776d3feb731c6d23a2f669ac8	world.txt

다른 하나는 subdir이 생겨서 내용이 변경된 root tree의 오브젝트입니다.

그렇다면, subdir의 객체(08be0f512375756b66561c0dcfb8429cd2ab5193)는 어디에 있을까요?

subdir은 "Hello.txt"와 "world.txt"를 가지는 디렉토리로,

좀 전의 root 디렉토리를 지칭한 트리객체와 그 내용이 동일합니다.

따라서 08be로 시작하는 기존의 트리 오브젝트를 그대로 사용하게 됩니다.

 

git의 속사정을 몰라도 개발하는데 아무 지장이 없습니다.

사실 세상 대부분을 몰라도 개발하는데 아무 지장이 없습니다.

하지만, 그 많은 개발자 중에 어쩌면 한두 명이라도 궁금해할지도 모른다는 생각에 포스팅을 남깁니다.

혹시 당신이 바로 그 개발자라면...

당신의 존재가 이 포스팅을 월요일 오전 3시 20분에 작성하고 있는 날 위안시켜주네요.

출근 2시간 40분 전.

 

 

_끝

  1. 개발자 2015.06.24 02:43

    훌륭한 설명 감사드립니다. 진정한 개발자 이십니다.

  2. 안녕하세요 2015.08.10 13:07

    정말 잘 읽었습니다! 명쾌한 설명이었어요! 실습 예제도 자세하게 설명해주시고 너무나도 큰 도움이 되었습니다. 감사합니다!

  3. 슈웅~ 2016.05.14 21:10

    포스팅 말미에 어쩌면 한두 명이라도 궁금해할지도 모른다는 생각 이란 부분이 지식탐구,나눔 를 향한 올바른자세? 라고 생각하는바 입니다. 큰 깨달음을 얻고 갑니다 포스팅 감사해요!

  4. 지하철 코더 2016.11.25 15:26

    잘 읽고 갑니다. 큰 도움이 되었습니다 :D
    (댓글 버튼을 못찾아서 한참 헤멨어요..ㅋㅋ)

  5. 건대CE 2017.02.02 14:05

    정말 감사합니다.. 너무 궁금해서 3시간 찾아봤는데.. 진짜 가슴이 뻥뚫렸습니다..

+ Recent posts