어플리케이션의 동작에 필요한 환경 설정 값등을 저장하고 읽으려고 할때, 추가적으로 다운받은 리소스등의 데이터를 읽어야 할때, 게임중 현재까지의 진행 상태를 저장하고 나중에 다시 읽으려고 할때...등등 이런 경우에 파일에 쓰고 읽는 방법을 통해 필요한 데이터를 읽고 쓰고 하게 됩니다. 이번 시간에는 이러한 파일 시스템과 더불어 Clet 이 제공하는 데이터베이스 API등을 살펴보도록 하겠습니다. 어떤 프로그램을 작성하던 자주 사용하게 되는 내용이니만큼 잘 살펴보면서 직접 실습도 해보시고 이렇게 저렇게 바꿔보면서 익혀보시길 바랍니다.
Database API & File System API
- 데이터베이스 관련
MC_dbOpenDataBase MC_dbCloseDataBase MC_dbDeleteDataBase
MC_dbGetAccessMode MC_dbListDataBases - 데이터 레코드 관련
MC_dbInsertRecord MC_dbSelectRecord MC_dbUpdateRecord MC_dbDeleteRecord
MC_dbLisRecords MC_dbSortRecords MC_dbGetNumberOfRecords MC_dbGetRecordSize - FILE 관련 (기본)
MC_fsOpen MC_fsClose MC_fsFileAttribute MC_fsRemove MC_fsRename
MC_fsRead MC_fsWrite MC_fsSeek - FILE 관련 (디렉토리,정보)
MC_fsMkDir MC_fsRmDir MC_fsList MC_fsSetMode MC_fsGetCounts MC_fsIsExist MC_fsTell
MC_fsTotalSpace MC_fsAvailable
파일을 직접 접근해서 데이터를 읽고 쓸 수도 있지만, Clet 이 지원하는 데이터베이스 API 를 이용하면 좀 더 간단하게 필요한 용도에 맞춰서 데이터를 기록할 수 있습니다. 선택은 각자의 판단에 따르겠지만, 일단 좀 더 편한 환경을 위해 지원하는 API 이니 만큼 먼저 살펴보도록 하겠습니다.
파일 시스템관련해서는 일단 기본적으로 파일을 열고 닫고 읽고 쓰는 API들을 살펴보고 그외 API들도 알아보도록 하겠습니다. 오늘은 그리 어려운 개념은 없을 것 같으니 맘 편히 시작해보죠.^^
Database API
데이터베이스(Database)라는 단어는 꽤 많이 들어 보셨을껍니다. 어느정도 컴퓨터를 경험해보셨다면(^^) MS-SQL 이나 Oracle 과 같은 이름도 들어보셨겠죠. MS-OFFICE에도 ACCESS 라는 개인용 데이터베이스(뭐 여러의미로^^) 어플을 제공하고 있죠. 아무튼, 많이 들어 본 단어이기는 하지만, 과연 데이터베이스(줄여서 DB)라는건 어떻게 정의되는 걸까요. 단순히 데이터를 저장하는 툴(?)정도로 생각한다면 왠지 너무 광범위 할것 같기도 하고...괜히 어려울 것만 같은 단어인 트랜젝션(딱히 생각나는게 이런거군요^^)같은 단어를 알아야 Clet 의 DB API를 쓸 수 있는걸까요~ ^^
용어상의 의미에서는 DB는 '여러 사람이 사용할 목적으로 통합적으로 관리되는 데이터 집합' 정도 되는 것 같습니다만, 사실 Clet 이 제공하는 DB API는 그런 거창한 의미나 위에서 이야기한 뭔가 이상한 단어(^^)들을 몰라도 전혀 관계없는 아주 간단한 API 입니다.
Clet 의 DB API 는 데이터 관리의 편의를 위해 데이터를 레코드 단위로 관리하기 위한 API 입니다.
여기서 레코드는 간단히 미리 정해둔 길이의 바이트단위 데이터 저장 공간이라고 생각하면 좋을 것 같네요. 결국 특정 단위로의 데이터 관리를 좀 더 편하게하는 방법을 제공하기 위해 만들어진 API 라는 거겠죠.
서론이 길었습니다만, 아무튼 제목만 거창했지, 실제 내용은 그리 어렵지 않은 동네라는 의미로 쓴 말이니깐요. 맘 편히 API들을 보도록 하겠습니다.
- M_Int32 MC_dbOpenDataBase(M_Char *dataBaseName, M_Int32 recordSize, M_Boolean create, M_Int32 mode);
M_Int32 MC_dbCloseDataBase(M_Int32 dbId);
M_Int32 MC_dbDeleteDataBase(M_Char *dataBaseName, M_Int32 mode);
M_Int32 MC_dbGetAccessMode(M_Char *dataBaseName);
M_Int32 MC_dbListDataBases(M_Byte *buf, M_Int32 len);
일단 DB API들은 전부 MC_db 로 시작한다는 이미 아셨죠? ^^ Clet API는 정말 이름 하나는 직관적으로 잘 풀어쓰기는 했습니다.(가끔 글자 틀리는것 빼고^^)
일단 DB를 사용하려면 MC_dbOpenDataBase 를 호출해야 합니다. 사용자가 이미 만들어둔 데이터베이스를 열거나 혹은 새로운 데이터베이스를 생성할때 사용하는 함수죠.
파라메터 dataBaseName에 인수로 앞으로 사용할(혹은 미리 만들어둔) DB의 이름 문자열을 넘겨야합니다. 이때 문자열에 사용할 수 있는 케릭터는 해당 시스템의 파일 시스템의 제약을 따른다고 스펙 문서에 나온 것으로 보아 아마 여기서 정하는 이름으로 파일명을 만드는게 아닐까 생각됩니다.(우리가 쓰는 에뮬에서는 어떨지 아래에서 확인해보겠습니다.) recordSize에는 새로 DB를 생성할때 해당 DB의 하나의 레코드 사이즈(바이트단위)를 인수로 넘깁니다. 만약 기존에 존재하는 DB라면 여기에 무슨 값이 있던 무시되도록 되어있다고 하네요. create에는 DB를 새로 생성할 것인지의 여부를 TRUE,FALSE로 넘겨야 합니다. mode에는 DIR_PRIVATE_ACCESS, DIR_SHARED_ACCESS, DIR_SYSTEM_ACCESS 중 하나가 전달 되어야 합니다. 파일 시스템에서도 보게 되겠지만, 위피에서는 파일의 접근 권한을 개인영역(private), 공유영역(share), 시스템영역(system)으로 구분하고 각 영역의 접근 권한이 설정될 수 있도록 되어있습니다. 일단 우리가 작성하는 어플의 경우 시스템은 절대 못 건들테니(^^) 개인영역이나 공유영역을 사용해야 합니다. 특별히 어플들 사이에 공유해야하는 DB가 아니라면 사실 개인영역(private)을 사용하는 것이 위피 시스템이 관리하는데도 안정적이지 않을까 하는 생각이 드는군요. /* 간혹 위피 매니저등의 버전에 따라서는 해당 어플이 삭제될때 그 어플이 생성한 데이터를 제대로 삭제 못해주는 경우도 있는데, 특히 공유 영역은 왠지 그럴 가능성이 더 많을 것 같다는 생각이 들거든요. 특별히 여러가지 어플이 공유해야하는 데이터가 아니라면 그쪽 영역은 안써주는게 예의(^^)가 아닐까 싶습니다. 뭐 그냥 제 생각입니다.^^ */
DB의 open이 성공하면 0보다 큰 정수인 dbID 가 반환됩니다. (0보다 작은 음수값의 경우 Clet 에서는 공통적으로 에러를 의미합니다. 자세한 에러 상황은 스펙 문서등을 참고해주세요.) 다 사용한 DB는 MC_dbCloseDataBase 로 닫습니다. 또한 더 이상 사용하지 않는 DB의 경우 MC_dbDeleteDataBase로 삭제할 수 있습니다. 만약 사용중인(open되어 close되지 않은) DB인 경우 삭제되지 않습니다. 삭제시에는 dataBaseName으로 해당 DB의 이름과 mode에 위에서 open시에 사용했던 정확한 영역을 넘겨줘야 합니다. /* 즉 이런 파라메터의 형태로 봐서도 Clet 의 DB API는 단순히 파일 시스템을 이용해서 좀 더 기능을 제공하는 것~ 이라고 할 수 있겠죠^^ */
MC_dbGetAccessMode 로는 dataBaseName에 해당하는 DB의 접근 모드 값을 알 수 있습니다. 다만 현재 어플의 권한에 따라서 접근할 수 있는 한계가 있으니 그 한계내에서 찾아보는 거라고 추측할 수 있겠네요. 스펙 문서에서는 share영역의 경우 CP 레벨 이상인 경우 라고 정의하고 있습니다. /* CP = Content Provider 의 의미일껍니다.(맞나^^) 대개 이통사에 납품하는 어플의 경우 CP레벨 이상이 나오죠. 위피 컴파일시에 정하도록 되어 있습니다. 그 이상의 권한의 경우 이통사등과 미리 협의해서 허가를 받아야 합니다. */ 접근할 수 있는 영역 기준으로 모든 DB 이름을 알고자 하는 경우 MC_dbListDataBases를 사용합니다. 이때 인수로 넘기는 buf는 할당된 공간이어야 하며, len은 그 공간의 길이를 의미합니다. 만약 DB이름들을 전부 적으려고 할때 공간이 부족하다던가 하면 반환되는 값이 음수값이 됩니다. 만약 제대로 반환에 성공한다면 0이상의 값이 되며, 그 값이 DB의 전체 갯수를 의미합니다. buf 안에 기록되어져 돌아오는 형태는 각 DB명마다 '\0'(=NULL=0)값으로 구별되며 마지막 DB명 후에는 NULL(=0)값이 두번 연속으로 기록되어 반환됩니다.
뭔가 글만으로도 어느정도 이해가 어렵지 않은(^^) API 들입니다만, 그래도 프로그램이라는건 직접 코드를 돌려보면서 익히는 것이 정도(正道)겠죠. 위의 API 들을 써서 한번 간단한 어디까지나 테스트용 어플을 만들어 보겠습니다.
- #define DB1_NAME "TestDataBase2008" // DB 이름을 바꾸기 쉽게 빼두었습니다.
#define DB2_NAME "ShareDB_0324"
void bufPrint(M_Char *buf) // DB list를 출력하기 위해 만든 함수
{
M_Int32 idx;
for(idx=0;idx<256;idx++) {
if (buf[idx]==0) {
MC_knlPrintk("\n");
if (buf[idx+1]==0 && idx<255) break;
} else {
MC_knlPrintk("%c",buf[idx]);
}
} // 대충 만든거니깐 그냥 결과 값만 보세요^^
}
void startClet(int argc, char* args[])
{
M_Int32 dbID1,dbID2,retVal;
M_Char buf[256];
retVal = MC_dbOpenDataBase(DB1_NAME, 128, TRUE, DIR_PRIVATE_ACCESS);
if (retVal < 0) {
MC_knlPrintk("* DB1 open error[%d]\n",retVal);
}
else {
dbID1 = retVal;
MC_dbCloseDataBase(dbID1);
}
retVal = MC_dbOpenDataBase(DB2_NAME, 10, TRUE, DIR_SHARED_ACCESS);
if (retVal < 0) {
MC_knlPrintk("* DB2 open error[%d]\n",retVal);
}
else {
dbID2 = retVal;
MC_dbCloseDataBase(dbID2);
}
retVal = MC_dbGetAccessMode(DB2_NAME);
MC_knlPrintk("DB2 Access mode = [%d]\n",retVal);
memset(buf,0,256);
retVal = MC_dbListDataBases(buf, 256);
MC_knlPrintk("DBLIST cnt[%d]\n",retVal);
bufPrint(buf);
retVal = MC_dbDeleteDataBase(DB1_NAME, DIR_PRIVATE_ACCESS);
MC_knlPrintk("DB1 delete = [%d]\n",retVal);
memset(buf,0,256);
retVal = MC_dbListDataBases(buf, 256);
MC_knlPrintk("DBLIST cnt[%d]\n",retVal);
bufPrint(buf);
}
어디까지나 각 API 들의 간단한 동작을 확인하기 위한 코드니깐, 일단 왜 만들고 쓰지도 않고 지운다던가 하는건 그러려니 해주시고요. 일단 대략 이렇게 코드를 넣어서 컴파일해서 에뮬에서 실행한 후 디버그 결과를 보니 전 이렇게 나왔습니다.
제대로 된게 없군요(ㅠㅠ) 에러 코드를 하나씩 보도록 하겠습니다. 위에서 나온 에러 값들을 API의 에러코드가 정의된 헤더 파일에서 찾아보겠습니다. /* 현재 사용하는 SDK의 경우 MCerror.h 에 존재하는 내용입니다. 이런 헤더 이름은 각 플랫폼 SDK 마다 다를 수 있습니다. */
- #define M_E_ERROR -1 // 기타에러
- #define M_E_NOENT -12 // 내용 없음
- #define M_E_ACCESS -24 // 접근에러
일단 첫번째 DB의 생성의 에러는 뭔가 정말 애매한 '기타에러'군요...사실 이런 에러는 무슨 에러인지 알아내기 쉽지 않습니다만, 일단 현재 SDK의 동작을 살펴보던중 제가 첫번째 DB 이름으로 입력한 문자열이 너무 길어서 그렇다는 걸 발견했습니다.(~.~) 고로 "TestDB2008" 정도로 줄여보기로 하겠습니다.
두번째 DB의 에러는 '접근에러'네요...흠..share 영역을 사용하려고 한 것인데, 스펙 문서에서 CP 레벨 이상만 된다고 했는데, 어디서 이걸 정해줘야 하는 걸까요. 딱 생각나는게 있으신가요? (^^) 바로 컴파일후 에뮬레이터가 인식할 수 있는 형태의 파일로 변경할때 사용하던 WIPI Bin Maker 에 권한 관련 설정이 존재합니다.
제가 계속 테스트 소스 생성에 사용하는 mif 파일을 load해보니 Access Level이 위와 같이 Public 을 가르키고 있었네요. 바로 옆인 CP 로 옮겨서 다시 Make 한번 해주면, 이후 계속해서 CP 레벨로 인식하게 됩니다. /* System 레벨에 대한 테스트는 이 강좌에서는 하지 않겠습니다. 쓸 일이 거의 없는 부분일테니깐요. */
DB2 의 접근 모드를 얻기 위해 사용한 MC_dbGetAccessMode 에서는 '내용없음' 이 반환되었군요. 당연히 DB2 가 아직 생성되지 못했으니 그런거라고 봐야겠죠.
그럼 위의 문제들을 수정해서 다시 실행해보죠.
일단 DB 생성 에러는 전부 사라졌네요. DB2의 접근 모드로 리턴된 값은 2 , 헤더에서 찾아보면(MCdb.h) DIR_SHARED_ACCESS 에 해당하는 값이라는 걸 알 수 있습니다.
DB1,DB2가 전부 생성되었으므로 DB list 를 얻어오니 2개에 이름도 정확히 출력되네요. DB1 을 삭제하고 나서 다시 DB list를 출력하니 정확히 1개의 DB와 이름이 출력되네요.
사실 옵션들을 조금씩 더 바꿔보면서 여러가지 변화를 보는 것도 좋겠지만, 역시나 지면...아니 시간 관계상(^^) 생략하도록 하겠습니다. 만약 DB API 에 관심이 생기시거나 이거 써먹을 때가 있겠는데 싶은 분들은 파라메터도 좀 바꿔보시고 이런저런 에러도 만들어 보시길 바라겠습니다. 더불어 위에서 DB 이름 글자수 제한을 오버해서 에러를 만나는 것과 같은 상황은 사실 Clet 플랫폼마다 다를 수 있는 부분이니 만큼 가급적 이름등은 컴쪽 공통룰(?)인 8자 영문,숫자~와 같은 룰을 애용하시길 바라겠습니다. /* 주말마다 작성하는 강좌이다 보니 상세하게 확인하는건 한계가 있습니다. 이 점은 양해 바라겠습니다. 저도 일단 현업 개발자이다 보니 해야할 일이 좀 많은지라 ㅠㅠ */
[그냥 좀 더~ 라는 의미의 보너스~]
그럼 DB는 과연 어떤 형태로 생성되는 걸까요. 뭐 이런것도 플랫폼마다 분명 다르겠고, 우리가 사용하는건 에뮬레이터이니깐 폰에서는 또 다르겠지만, 일단 우리가 사용하는 SDK에서는 SDK 설치 디렉토리 기준으로 "SDK설치폴더\Bin\wipi\sys\DBMS" 에 DB파일들이 위치하고 있습니다. 파일 확장자의 의미등은 한번 유추해보시기 바랍니다.^^ 예를 들어 현 SDK에서 DB이름 길이 제한이 왜 걸리는지는 DBMS.DAT 파일의 저장 형태를 보다보면 이해가 될듯하네요.^^ 뭐 그냥 호기심으로 살펴보는 정도로 문제될 건 없겠죠^^~ 이런 것은 각자 궁금하면 살펴보게 되는 뭐 그런 영역 같으니깐 자세히 다루지는 않겠습니다.
Database Record API
위에서 Database 를 어떻게 관리할 수 있는지 간단하게 살펴보았습니다. 이제 그럼 실제 데이터를 기록해봐야 겠네요. 위의 내용에서 Clet 의 DB는 레코드(record)단위로 데이터를 다룬다고 이야기 했습니다. 이 레코드 데이터의 길이는 DB가 생성될때 정해지는 부분입니다. 한번 길이가 정해지면 그 길이 이하의 데이터만이 하나의 레코드가 될 수 있는 것이죠. 즉 미리 다룰 데이터의 최대 길이로 DB를 생성해야 겠죠.
그럼 어떤 API들이 있는가 보도록 하겠습니다.
- M_Int32 MC_dbInsertRecord(M_Int32 dbId, M_Byte *buf, M_Int32 len);
M_Int32 MC_dbSelectRecord(M_Int32 dbId, M_Int32 recId, M_Byte *buf, M_Int32 len);
M_Int32 MC_dbUpdateRecord(M_Int32 dbId, M_Int32 recId, M_Byte *buf, M_Int32 len);
M_Int32 MC_dbDeleteRecord(M_Int32 dbId, M_Int32 recId);
M_Int32 MC_dbListRecords(M_Int32 dbID, M_Int32 *buf, M_Int32 len);
M_Int32 MC_dbGetNumberOfRecords(M_Int32 dbId);
M_Int32 MC_dbGetRecordSize(M_Int32 dbId);
일단 파라메터들을 보면 dbId 는 위에서 DB open등을 통해 반환받은 DB의 인식 ID 이고, recId 는 레코드를 구별하는 ID 값이라는 것을 생각해볼 수 있습니다. DB를 생성할때와 마찬가지로 MC_dbInsertRecord 가 성공하면 recId 를 반환하는 것이겠죠.(어느정도 API들의 동작 방식은 규칙성이 있다고 볼 수 있겠죠.) 모든 레코드에 기록하는 데이터는 M_Byte (= unsigned char) 형이네요. 즉 뭐든 기록이 가능한 데이터 형식이라고 볼 수 있겠습니다. 물론 len 에 해당하는 데이터 길이는 DB open 하던때에 정해준 길이 이하로 해야겠죠. 정해진 길이보다 크다면 에러가 반환 될껍니다. /* 어떠한 데이터 형식이라고 해도 실제 기기상의 메모리의 입장에서 생각하면 정보 처리의 단위는 바이트형으로 볼 수 있게 됩니다. 예를 들어 M_Int32 형의 데이터는 실제 메모리상에서는 4바이트의 공간을 가지는 형태이고 이걸 M_Byte 형으로 타입케스팅(typecasting) 하게되면 4바이트의 데이터로 접근하고 다룰 수도 있게 되는 것이죠. 아래 예에서도 이런 타입 캐스팅을 사용하고 있으니 아직 이런 개념을 잘 모르신다면 책이나 검색등을 통해서 살펴보시면서 진행하시면 되겠습니다. C 언어에서는 이러한 타입을 변경하여 데이터를 다루는 것을 일정한 규칙내에서 꽤나 자연스럽게 할 수 있고, 그 응용 범위를 알아 갈 수록 좀 더 잼난 프로그래밍도 가능해진다고 생각되네요. */
MC_dbInsertRecord, MC_dbSelectRecord, MC_dbUpdateRecord, MC_dbDeleteRecord는 각각 SQL에서 사용되는 Insert, Select, Update, Delete 와 유사하기 때문에 API 명도 그렇게 만든 것 같네요. 각각 새로운 데이터를 입력(Insert)하고, 원하는 데이터를 골라내고(Select), 데이터중 변경이 필요한 내용은 수정하여 다시 쓰고(Update), 필요없는 데이터는 찾아서 지우는(Delete) 기능을 하는 API 들입니다. 어떤 레코드들이 있는지 알기 위한 함수가 MC_dbListRecords 이며, 해당 DB의 레코드 사이즈를 알고자 하는 경우 MC_dbGetRecordSize 를 사용하며, 해당 DB의 레코드 총 갯수를 알고자 하는 경우 MC_dbGetNumberOfRecords 를 사용합니다.
그럼 간단한(^^) 예제를 통해서 어떻게 사용하는건지 살펴보겠습니다.
- #define DB_NAME "TestDB2008" // DB 이름
#define DB_RECORD_SIZE 64 // DB의 레코드 사이즈
typedef struct _MYDATA {
M_Int32 data1;
M_Int32 data2;
M_Char dataString[40];
} MYDATA; // 기록할 데이터 구조체
M_Int32 dbID,recID1;
MYDATA myData;
M_Int32 recList[4]; // 실제 list를 얻을때는 레코드 사이즈를 얻어 그 사이즈 만큼 할당해서 쓰는게 좋겠죠.
void startClet(int argc, char* args[])
{
}
void handleCletEvent(int type, int param1, int param2)
{
M_Int32 retVal,idx;
M_Byte *buf;
switch(type)
{
case MV_KEY_PRESS_EVENT:
MC_knlPrintk("[EVENT-KEYPRESS] [%c]\n",param1);
switch(param1)
{
case MC_KEY_1: // DB OPEN
retVal = MC_dbOpenDataBase(DB_NAME, DB_RECORD_SIZE, TRUE, DIR_PRIVATE_ACCESS);
if (retVal < 0)
{
MC_knlPrintk("- DB open error[%d]\n",retVal);
}
else
{
dbID = retVal;
MC_knlPrintk("- DB open success[dbID=%d]\n",dbID);
}
break;
case MC_KEY_2: // Insert Record 1
myData.data1 = 1;
myData.data2 = 1000;
strcpy(myData.dataString,"첫번째 DATA");
buf = (M_Byte*)&myData; // 구조체 데이터를 M_Byte 형으로 타입케스팅해서 기록합니다.(주소값을 사용하는거죠)
retVal = MC_dbInsertRecord(dbID,buf,sizeof(MYDATA));
if (retVal < 0)
{
MC_knlPrintk("- Record Insert Error [%d]\n",retVal);
}
else
{
recID1 = retVal;
MC_knlPrintk("- Record Insert Success [%d]\n",retVal);
}
break;
case MC_KEY_3: // Insert Record 2
myData.data1 = 2;
myData.data2 = 0x1234;
strcpy(myData.dataString,"두번째 레코드예요~");
buf = (M_Byte*)&myData;
retVal = MC_dbInsertRecord(dbID,buf,sizeof(MYDATA));
if (retVal < 0)
{
MC_knlPrintk("- Record Insert Error [%d]\n",retVal);
}
else
{
MC_knlPrintk("- Record Insert Success [%d]\n",retVal);
}
break;
case MC_KEY_4: // Select Record 1
memset(&myData,0,sizeof(MYDATA));
buf = (M_Byte*)&myData;
retVal = MC_dbSelectRecord(dbID,recID1,buf,DB_RECORD_SIZE); // select시의 사이즈는 record size !
if (retVal < 0)
{
MC_knlPrintk("- Record1 Select Error [%d]\n",retVal);
}
else
{
MC_knlPrintk("- Record1 Select Success [%d]\n",retVal);
MC_knlPrintk("- Data [%d][%d][%s]\n",myData.data1,myData.data2,myData.dataString);
}
break;
case MC_KEY_5: // Update Record 1
myData.data1 = 3;
myData.data2 = 3000;
strcpy(myData.dataString,"첫번째 데이터를 변경함");
buf = (M_Byte*)&myData;
retVal = MC_dbUpdateRecord(dbID,recID1,buf,sizeof(MYDATA));
if (retVal < 0)
{
MC_knlPrintk("- Record Update Error [%d]\n",retVal);
}
else
{
MC_knlPrintk("- Record Update Success [%d]\n",retVal);
}
break;
case MC_KEY_6: // list records
retVal = MC_dbGetRecordSize(dbID);
MC_knlPrintk("- DB Record's Size [%d]\n",retVal);
retVal = MC_dbGetNumberOfRecords(dbID);
MC_knlPrintk("- DB Record's Count [%d]\n",retVal);
retVal = MC_dbListRecords(dbID,recList,4);
if (retVal < 0)
{
MC_knlPrintk("- MC_dbListRecords Error [%d]\n",retVal);
}
else
{
for(idx=0;idx<retVal;idx++)
MC_knlPrintk("- RecordID [%d]\n",recList[idx]);
}
break;
case MC_KEY_7: // delete record 2 - list를 본 후에만 가능 (예를 들기 위해 이렇게 한거예요.)
retVal = MC_dbDeleteRecord(dbID, recList[1]);
if (retVal < 0)
{
MC_knlPrintk("- Record2 Delete Error [%d]\n",retVal);
}
else
{
MC_knlPrintk("- Record2 Delete Success [%d]\n",retVal);
}
break;
case MC_KEY_9: // DB CLOSE
MC_dbCloseDataBase(dbID);
MC_knlPrintk("- DB Close\n");
break;
}
break;
}
}
이번 예제 코드도 어디까지나 순전히 사용하는 법을 간단히 알아보기 위해 만든 코드입니다. 실제로 사용하게 될때는 좀 더 효율적으로 다듬어서 사용하셔야 합니다. 예를 들면 위에 주석으로 적어 둔 것 같이, 레코드의 리스트를 얻어오는 경우 레코드의 총 갯수 부터 얻어오고 그 갯수를 기준으로 메모리를 할당해서 리스트를 가져오는 것이 효율적이겠죠. 일단 그럼 위 예제 코드를 테스트해 본 결과를 보도록 하겠습니다.
/* 실행 후 키를 누른 순서는 디버그 출력에서도 보이듯이 1->2->3->4->5->4->6 이었습니다. */
1번키를 눌러서 DB를 open했습니다. create를 TRUE로 했습니다만, 그렇다고 매번 새로 만드는 것이 아닙니다. DB가 현재 존재하지 않을 경우 새로 만드는다는 의미이니깐요.(아래 결과를 보면 이해가 되실껍니다.) 2번과 3번키로 각각 데이터를 입력하였습니다. 이때 입력할 데이터를 예제에서는 구조체 데이터로 만들고 그것을 파라메터로 넣을때 M_Byte*형으로 타입캐스팅한 변수로 바꿔서 넣었는데, 사실 파라메터의 해당 자리에 직접 타입캐스팅을 해서 넣어도 무관합니다. 위에서도 언급했지만, 실제 메모리상에서의 데이터는 바이트의 배열이라고도 생각할 수 있으므로 저런 방법으로의 타입 캐스팅이 가능하고, 이러한 방식으로 아래에서 보게될 파일의 기록/읽기등도 동일하게 가능합니다.
4번키는 2번키로 입력한 데이터 레코드 ID를 이용해서 select 한 결과를 보여줍니다. 입력한 값이 제대로 나오고 있죠.
5번키는 위의 레코드 ID의 데이터를 update합니다. 그것을 그 다음 다시 4번키로 select해보니 변경된 값이 나오네요. 제대로 동작하는 것이죠.
6번키로 현재 DB의 레코드 사이즈와 레코드 총 갯수, 그리고 각 레코드의 ID 값을 얻어낼 수 있습니다.
일단 여기까지 테스트한 후 에뮬레이터를 종료합니다. /* DB close 를 생략했습니다만, 기록이 제대로 된다는 것을 아래 결과로 알 수 있습니다. */
코드를 수정하지 말고 다시 에뮬레이터를 실행합니다. 그래서 다시 아래와 같이 테스트해 보았습니다.
/* 키 입력 순서는 6->1->6->7->6->7->9 입니다. */
일단 바로 6번키를 눌러보니 에러값이 나오는군요. DB API때 처럼 에러 코드 값을 찾아보니 'M_E_BADFD (잘못된 식별자)' 라는 에러네요. 현재 에뮬레이터가 실행된 후 아직 DB를 열지 않았으므로 dbID 변수값에 아무런 값이 없을테니(혹은 엄한 값이 있겠죠) 위와 같은 에러가 나오는건 당연할 것 같습니다.
1번으로 DB를 열고, 다시 6번을 눌러보니 전 테스트 마지막에서 보았던 것과 같은 레코드 결과가 나오는군요. 위에서 DB Close 없이 어플을 그냥 종료했음에도 데이터는 잘 기록되어 있음을 알 수 있습니다. 7번키로 6번과정에서 얻은 레코드 2번째 ID를 지워보겠습니다. 그후 다시 6번키를 누르면 레코드 한개가 제대로 삭제되어서 총 갯수 1개에 레코드 ID도 1개만 나옴을 알 수 있네요. 이 상태에서 다시 7번을 누르니 에러 코드가 출력됩니다. 'M_E_BADRECID (잘못된 레코드 식별자)' 라는 에러네요. 즉 이미 아까 레코드ID 488을 지웠으므로 그 값을 다시 지우려고 하니 이런 에러가 발생한 것이겠죠. 마지막으로 9번키로 DB를 닫았습니다.
대략 이런 방식으로 Clet 이 지원하는 DB API 를 활용 할 수 있습니다. 가만히 보고 있으면 직접 파일에 접근해서 데이터를 처리해줘야 하는 것을 좀 편하게(?) 만들어둔 API 라는 것을 알 수 있습니다. 아마 지금 이런 API를 처음 보시는 분들도 어느정도 계속 공부하시다보면 어느새인가 이런 API보다 더 멋진 데이터 관리 API 를 만드실 수 있으리라 생각합니다.^^ /* 예를 들어 현재 DB API는 자신이 원하는 데이터를 찾기가 생각보다 어렵습니다. 이유는 각 데이터 단위(즉 레코드)의 구별자가 단순히 레코드 ID 라는 것이고 이 레코드 ID는 우리가 지정하는 값이 아니니 특별한 의미가 있는 것도 아니죠. 위의 420 이니 488이니 하는 값은 사실 현 SDK에서는 DB 데이터 파일의 해당 레코드의 시작 번지를 의미하는 값입니다.(위 DB API 에서 덤으로 이야기한 그 폴더의 파일을 보시면 아실껍니다.) 488-420 = 68...우리가 지정한 데이터 사이즈는 64..나머지 4바이트, 실제로 데이터를 보면 레코드의 시작 주소로 부터 4바이트는 다음 레코드가 있는 주소를 나타내는 값이네요. 뭐 이런 의미는 플랫폼마다 다르겠지만, 아무튼 결국 데이터를 검색하는데 유용한 정보는 아니겠죠. 따라서 좀 더 유용하게 사용하려면 아무래도 직접 파일 구조나 접근할 방법을 설계해야 할 것입니다. */
Clet의 DB API 설계자는 아마 위에서 제가 잡담처럼 적어둔 문제를 약간이라도 해결(^^)할 수 있는 방법을 제시하기 위해 DB API 에 조금은 유별한 함수를 하나 두었습니다. DB API의 마지막 내용으로 이 함수의 사용법을 한번 살펴보도록 하겠습니다.
- M_Int32 MC_dbSortRecords(M_Int32 dbID, M_Int32* buf, M_Int32 len,
M_Int32 (*compare)(const void *, const void *), M_Int32 (*filer)(const void *));
MC_dbSortRecords 는 MC_dbListRecords 와 첫 3개의 파라메터는 같은 구조를 가지는 것으로 보아 아마 뭔가 그후에 나오는 파라메터를 이용해서 레코드 ID 리스트의 순서를 정렬(sort)해서 보여줍니다. 그럼 정렬의 기준을 어떻게 정하느냐는 파라메터 compare 로 결정합니다. 파라메터의 형태를 보니 함수 포인터를 넘기라는거네요. 함수 포인터가 어떤 것인지 모르신다면, 5강에서 보았던 픽셀 연산 함수를 한번 생각해보시면 되겠습니다. 그때도 각 픽셀의 연산을 위한 함수를 직접 지정할 수 있었고, 그 원형대로 함수를 작성해주면 되었었죠.
C에서는 포인터가 여러가지 개념으로 사용 될 수 있습니다. 코드도 사실 실행시에는 메모리상에 위치하는 것이고, 그렇게 생각하면 각각의 함수의 시작점도 주소화 할 수 있을 것입니다. 이러한 생각에서 나온 것이 함수 포인터라고 생각하면 될 것 같습니다. /* 더 자세한 내용은 당연히 C 책등을 찾아보시길 강력히 권해드립니다. 포인터는 C의 핵심이라고 해도 과언이 아닐 정도로 중요한 부분이니깐요. */
즉 compare에 넘기는 함수는 파라메터로 const void * 를 두개 가지며, M_Int32 값을 반환하는 함수로군요. 뭔가 떠오르시는게 있습니까? ^^ 사실 C 를 어느정도 해 오신 분들이라면 이러한 비교형 함수 만들기에 대해서 감을 가지고 계실 것 같습니다.
strcmp 나 memcmp 와 같은 대상 비교형의 함수의 경우 그 리턴값이 >0 , <0 , ==0 으로 나눠진다는 것을 알고 계신가요. 즉 대상 A,B 가 있을때 A>B 라면 반환되는 값은 0보다 크고(>0) , A<B 라면 0보다 작고(<0), A==B 라면 0을 반환(==0)하는 방식입니다. 위의 compare 파라메터에 들어가는 함수도 마찬가지 식으로 비교한 값을 반환해주면, 그걸 기준으로 레코드 ID를 정렬한다는 겁니다.
그럼 filer 파라메터에 함수는 어떤 값을 반환해야 하는 것일까요. 스펠링이 틀려서 의미가 와닿지 않겠습니다만, 사실 filter 입니다^^. 뭐 스펙문서부터 틀리니 아마 SDK 개발자들도 그냥 틀린 문자 그대로 쓴거겠죠...뭐 그러려니 하시고...(이런건 애교로 봐줘야죠^^) 아무튼 즉, 필터입니다. 정렬할 레코드 ID 리스트에 해당 레코드를 넣을 것인지를 결정하는 함수 인 것이죠. 이 함수를 통해 0보다 큰 값을 반환하면 그 레코드는 레코드 리스트에 포함되고 0 을 반환하면 리스트에 포함되지 않는 형식입니다.
마지막으로 위의 두 함수 파라메터를 NULL 로 입력하면, compare의 경우는 레코드의 데이터를 무조건 binary compare (즉 memcmp 와 유사하다고 생각되네요.)하여 각 바이트 비교로 올림차순 정렬하며, filer의 경우는 NULL 이 입력되면 모든 레코드에 대해서 비교를 한다는 의미가 됩니다.
길게 떠들었습니다만(^^) 아무튼 이런건 역시 코드로 보는게 편하겠죠. 예제 코드를 만들어 보겠습니다.
- #define DB_NAME "TestDB2008"
#define DB_RECORD_SIZE 64
typedef struct _MYDATA {
M_Int32 data1;
M_Int32 data2;
M_Char dataString[40];
} MYDATA;
M_Int32 dbID;
MYDATA myData;
M_Int32 recList[10];
M_Int32 retVal;
void insertData(M_Int32 data1, M_Int32 data2 , M_Char *string)
{
myData.data1 = data1;
myData.data2 = data2;
strcpy(myData.dataString,string);
retVal = MC_dbInsertRecord(dbID,(M_Byte*)&myData,sizeof(MYDATA));
if (retVal < 0)
{
MC_knlPrintk("- Record Insert Error [%d]\n",retVal);
}
else
{
MC_knlPrintk("- Record Insert Success [%d] - [%d][%d][%s]\n",retVal,data1,data2,string);
}
}
M_Int32 MySort1(const void *dat1 , const void *dat2)
{
MYDATA *myData1 = (MYDATA*)dat1;
MYDATA *myData2 = (MYDATA*)dat2;
// data2 값으로 정렬합니다.
if (myData1->data2 > myData2->data2)
return 1;
else if (myData1->data2 < myData2->data2)
return -1;
else
return 0;
}
M_Int32 MySort2(const void *dat1 , const void *dat2)
{
MYDATA *myData1 = (MYDATA*)dat1;
MYDATA *myData2 = (MYDATA*)dat2;
// string 값을 앞에서부터 2글자만 비교합니다.
return (M_Int32)strncmp(myData1->dataString, myData2->dataString, 2);
}
M_Int32 MyFilter(const void *dat)
{
MYDATA *myData = (MYDATA*)dat;
// 이 필터는 string값 앞의 두 문자가 "00"인 경우 무시하는 필터입니다.
if (strncmp(myData->dataString,"00",2)==0) return 0;
else return 1;
}
void handleCletEvent(int type, int param1, int param2)
{
M_Int32 idx;
switch(type)
{
case MV_KEY_PRESS_EVENT:
MC_knlPrintk("[EVENT-KEYPRESS] [%c]\n",param1);
switch(param1)
{
case MC_KEY_1: // DB OPEN
retVal = MC_dbOpenDataBase(DB_NAME, DB_RECORD_SIZE, TRUE, DIR_PRIVATE_ACCESS);
if (retVal < 0)
{
MC_knlPrintk("- DB open error[%d]\n",retVal);
}
else
{
dbID = retVal;
MC_knlPrintk("- DB open success[dbID=%d]\n",dbID);
}
break;
case MC_KEY_2: // Insert Records
insertData(1,1000,"00테스트 데이터1");
insertData(2,1290,"14테스트 데이터2");
insertData(3,1100,"04테스트 데이터3");
insertData(5,1250,"09테스트 데이터4");
insertData(4,1300,"12테스트 데이터5");
insertData(2,1150,"00테스트 데이터6");
break;
case MC_KEY_3: // normal list & info
retVal = MC_dbGetRecordSize(dbID);
MC_knlPrintk("- DB Record's Size [%d]\n",retVal);
retVal = MC_dbGetNumberOfRecords(dbID);
MC_knlPrintk("- DB Record's Count [%d]\n",retVal);
retVal = MC_dbListRecords(dbID,recList,10);
if (retVal < 0)
{
MC_knlPrintk("- MC_dbListRecords Error [%d]\n",retVal);
}
else
{
for(idx=0;idx<retVal;idx++)
MC_knlPrintk("- RecordID [%d]\n",recList[idx]);
}
break;
case MC_KEY_4: // sort - NULL,NULL
retVal = MC_dbSortRecords(dbID,recList,10,NULL,NULL);
if (retVal < 0)
{
MC_knlPrintk("- MC_dbSortRecords Error [%d]\n",retVal);
}
else
{
for(idx=0;idx<retVal;idx++)
{
MC_dbSelectRecord(dbID, recList[idx],(M_Byte*)&myData,DB_RECORD_SIZE);
MC_knlPrintk("- RecordID[%d],[%d][%d][%s]\n",recList[idx],myData.data1,myData.data2,myData.dataString);
}
}
break;
case MC_KEY_5: // sort - MySort1 ,NULL
retVal = MC_dbSortRecords(dbID,recList,10,MySort1,NULL);
if (retVal < 0)
{
MC_knlPrintk("- MC_dbSortRecords Error [%d]\n",retVal);
}
else
{
for(idx=0;idx<retVal;idx++)
{
MC_dbSelectRecord(dbID, recList[idx],(M_Byte*)&myData,DB_RECORD_SIZE);
MC_knlPrintk("- RecordID[%d],[%d][%d][%s]\n",recList[idx],myData.data1,myData.data2,myData.dataString);
}
}
break;
case MC_KEY_6: // sort - MySort2 ,MyFilter
retVal = MC_dbSortRecords(dbID,recList,10,MySort2,MyFilter);
if (retVal < 0)
{
MC_knlPrintk("- MC_dbSortRecords Error [%d]\n",retVal);
}
else
{
for(idx=0;idx<retVal;idx++)
{
MC_dbSelectRecord(dbID, recList[idx],(M_Byte*)&myData,DB_RECORD_SIZE);
MC_knlPrintk("- RecordID[%d],[%d][%d][%s]\n",recList[idx],myData.data1,myData.data2,myData.dataString);
}
}
break;
case MC_KEY_9: // DB CLOSE
MC_dbCloseDataBase(dbID);
MC_knlPrintk("- DB Close\n");
break;
}
break;
}
}
코드가 좀 더 길어지고 함수가 늘었습니다만, 일단 동작은 간단하게 일어나는 구조입니다. 아마 이해하시는데 무리는 없을 것이라고 생각합니다.
그럼 실행 결과를 보도록 하죠.
/* 키 입력은 1->2->3->4->5->6->9 입니다. */
일단 1번키로 DB를 열고 2번키로 총 6개의 레코드를 입력하였습니다. 그리고 3번키로 단순히 레코드 리스트를 출력하니 입력한 순서대로 출력되네요.
일단 sort 함수에 비교나 필터 파라메터는 NULL로 해서 결과를 얻어본 값이 4번키를 눌렀을때의 결과입니다.
여기서 주의깊게 보셔야 할 부분은 ① 라고 표시한 부분입니다. 일단 비교 파라메터가 NULL 인 경우 데이터를 단순히 바이너리값으로 비교해서 올림차순으로 정렬한다고 했습니다. 대략 첫번째(data1)값을 보면 일단 올림차순이 된게 맞는 것 같은데, 표시된 부분의 두번째 값(data2)을 보니 1290 이 1150 보다 큰데 어째서 더 낮은 수인 것처럼 위로 올라간 것일까요?
저 결과를 보시고 당연히 그렇지라고 하신 분들은 이미 엔디안(endian) 개념을 아시는 분들일껍니다. 즉, 숫자가 메모리상 혹은 파일상에 기록될때 어떤 형식을 사용하느냐에 따라서 위와 같은 결과를 가져올 수 있는 것인데요. 우리가 사용하는 Intel 계열의 pc나 현재 폰의 메모리나 데이터 기록 방식은 little-endian 방식입니다. 이 little-endian 방식은 데이터값을 기록하는 구조가 아래와 같습니다.
- 0x12345678 -> 메모리: ...... 78 56 34 12 ........... (little-endian)
이런 방식으로 위의 값을 보면 1290 과 1150 은 각각 0x50A , 0x47E 이므로 메모리상에서는 동일한 비교 대상위치로 볼때 "0A 05" 와 "7E 04" 로 보일 것입니다. 따라서 바이트순으로 비교하는 관점에서 보면 "0A 05"가 더 작은 값으로 비교가 되는 것이죠. (0x0A < 0x7E) 즉, 이러한 결과로 위와 같은 결과가 나온다는 것을 알 수 있습니다. /* 엔디안에 관련된 자세한 설명등은 책이나 검색을 통해서 참고해주시기 바랍니다. 메모리 내용을 다루는데 있어서 간단한 이론이지만, 자주 햇갈릴 수 있는 부분이기 때문에, 잘 알아두셔야 합니다. */
5번 키를 누르면 compare용 함수로 data2값을 기준으로 내림차순 하게한 MySort1 함수를 사용하게 됩니다. 필터는 NULL 로 했으니 모든 레코드를 가지고 비교하겠죠.
6번 키를 누르면 dataString값중 앞의 2자리만을 비교해서 내림차순 정렬하게 됩니다. 이때 필터는 앞의 두자리가 "00"이라면 포함하지 않도록 설정한 것입니다.
위와 같은 방법으로 레코드 데이터를 직접 원하는 순서로 얻을 수가 있습니다. 그렇지만, 뭐 이런 API가 있다고 해서 모든 경우에서 편리하지는 않겠죠. 확실히 DB API는 데이터를 다루는게 간단히 접근할 수 있는 방법은 제시하고 있지만, 세세하게 제어하는데는 아쉬운 부분이 많습니다. (물론 간단히 어플 작성등을 하는 경우에는 이런 API도 꽤 개발 시간을 줄여주니 도움이 되죠.)
따라서 자신이 원하는 파일 구조로 데이터를 저장하기 위해 직접 파일을 제어하는 방법을 익힐 필요가 있습니다.
이것이 파일 시스템 API 를 살펴봐야할 이유가 될지는 모르겠지만요^^ 아무튼 DB API는 여기까지 보고 파일로 넘어가겠습니다.^^
FILE API
C언어등에서 직접 fopen 이나 fread , fwrite 등을 사용해 보셨다면, 이번 파트의 내용은 그냥 주르륵 후딱~ 보시면서 넘어가셔도 무관할 것 같습니다.
기본적인 파일 API 들은 C 의 기본 파일 API 와 상당히 유사하며 사용법도 거의 같습니다. 특별히 어려운 API는 없으므로 한번에 쭉 보도록 하죠^^
- M_Int32 MC_fsOpen(M_Char* name, M_Int32 flag, M_Int32 aMode);
M_Int32 MC_fsRead(M_Int32 fd, M_Byte* buf, M_Int32 len);
M_Int32 MC_fsWrite(M_Int32 fd, M_Byte* buf, M_Int32 len);
M_Int32 MC_fsClose(M_Int32 fd);
M_Int32 MC_fsSeek(M_Int32 fd, M_Int32 pos, M_Int32 where);
M_Int32 MC_fsFileAttribute(M_Char* name, MC_FileInfo* fa, M_Int32 aMode);
M_Int32 MC_fsRemove(M_Char* name, M_Int32 aMode);
M_Int32 MC_fsRename(M_Char* oldname, M_Char* newname, M_Int32 aMode);
위의 4개 API는 파일에서 가장 기본이 되는 열고(open),읽고(read),쓰고(write),닫고(close)하는 함수들입니다. 더 설명할 것이 없을 만큼 직관적이지 않습니까^^
MC_fsOpen은 name으로 지정된 파일을 열어서 fd값을 반환합니다. /* File Descriptor = 시스템으로 부터 할당받는 파일이나 소켓을 대표하는 정수값 */
이때 name의 문자 길이는 30자 이내로 해야한다고 스펙 문서에서 규정하고 있습니다. 사실 이런저런 이유로 길지 않게 해주는게 좋을 것 같기는 합니다. flag 에는 파일을 열때 어떻게 열것인지를 알려줍니다. 아래와 같은 값들이 사용됩니다.
- MC_FILE_OPEN_RDONLY // 읽기만 가능
MC_FILE_OPEN_WRONLY // 쓰기만 가능
MC_FILE_OPEN_WRTRUNC // 열기만 가능하고 파일이 존재하면 파일크기가 0이 된다.
MC_FILE_OPEN_RDWR // 읽기와 쓰기가 모두 가능
aMode의 값은 DB API와 마찬가지로 접근(access)영역을 알려주는 것입니다. 아래와 같이 구별됩니다.(DB의 open에 넣던 것과 같은 의미입니다.)
- MC_DIR_PRIVATE_ACCESS // private 디렉토리에 접근
MC_DIR_SHARED_ACCESS // shared 디렉토리에 접근
MC_DIR_SYSTEM_ACCESS // system 디렉토리에 접근
MC_fsOpen에서 반환되는 값이 0보다 작으면 에러값입니다. 파일을 열때는 여러가지 이유로 에러가 발생하는 경우들이 존재할 수 있으므로 항상 반환되는 값을 체크하도록 하는게 좋습니다.
여기서 구한 fd값을 가지고 MC_fsRead , MC_fsWrite 를 이용해서 데이터를 읽고 씁니다. 다 사용한 파일은 꼭 MC_fsClose를 이용해서 닫아야 합니다. 제대로 닫지 않을 경우 재 오픈이 불가능해지거나 하는 에러 사항이 발생합니다.(뭐 이런건 시스템마다 조금씩 다릅니다만, 쓰고 닫는다라는 습관을 들이는게 중요하겠죠.)
파일 시스템을 사용할 경우 꽤나 유용하게 사용하는 기능을 제공하는 함수가 MC_fsSeek 함수입니다. 이 함수를 통해 해당 파일의 원하는 위치를 읽고 쓸 수가 있게 됩니다.
where 파라메터의 인수로 아래와 같은 값을 입력할 수 있습니다.
- MC_FILE_SEEK_SET // 파일의 처음을 기준으로 파일포인터의 위치를 설정
MC_FILE_SEEK_CUR // 파일의 current position을 기준으로 파일포인터의 위치를 설정
MC_FILE_SEEK_END // 파일의 끝을 기준으로 파일포인터의 위치를 설정
where의 값을 기준으로 pos 값만큼 파일 포인터가 이동하며 이후 MC_fsRead, MC_fsWrite등을 통해 그 위치부터 읽고 쓸 수 있게 됩니다. 이런 파일 제어 방식은 실제로 상당히 많은 곳에서 사용되며, 파일 구조를 설계하는데 있어서 이런 방식을 같이 고려해두면 효율적으로 원하는 내용을 찾고 읽을 수 있는 전략을 만들 수 있게 됩니다.
MC_fsFileAttribute 함수를 통해서 특정 파일의 파일 정보를 구할 수 있습니다. name 파일을 aMode 영역안에서 찾아서 파일이 존재할 경우 fa 구조체에 내용을 전달합니다.
MC_FileInfo 구조체는 아래와 같은 구조를 가지고 있습니다.
- struct _fileInfo{
M_Int32 attrib; // 파일의 특성을 표시한 bit mask들
M_Uint32 creationTime; // 파일이 생성된 시간을 1970년 1월1일 이후 초단위로 나타낸다.
M_Uint32 size; // 파일의 크기
};
위 구조체의 내용중 attrib 에서 현재 확인할 수 있는 값은 MC_FILE_IS_DIR 이며 이 값이 비트 플래그 방식으로 설정되어 있으면, name 은 디렉토리라는 것을 의미합니다. /* 비트 플래그(bit-flag) 방식이란 특정 속성등을 표기할때 메모리를 효율적으로 사용하기 위해 대개 하나의 비트에 값을 대응해놓고 그 비트를 비교함으로서 해당 속성의 여부를 판단하는 방법입니다. 플랫폼마다 위와 같은 비트 값은 다르게 설정할 수 있으므로 스펙에 명기된 것 같은 정의된 이름을 사용해서 & 등을 통해 비교하는 형태로 사용해야 합니다. */ 파일 정보 구조체에서 자주 사용하게 되는 값은 size 값 정도일듯합니다. (파일 사이즈 구할 일이 종종 생기거든요.)
파일을 지워야 할때는 MC_fsRemove, 파일명을 바꿔줘야 할때는 MC_fsRename 을 사용합니다. 뭐 특별한 설명은 필요 없는 함수 같군요.^^
위의 API들에 대해서는 특별히 예제를 작성하지 않아도 아마 충분히 스스로 만들어서 해보실 수 있으리라 생각합니다.
더불어 저번 시간에 이미 한번 예제에서 은근 슬쩍 다루기도 했었죠.(기억하시는분?^^)
- // 다음시간에 볼 파일 관련 API를 사용합니다.
fID = MC_fsOpen("result.bmp",MC_FILE_OPEN_WRONLY,MC_DIR_PRIVATE_ACCESS);
buf = (M_Byte*)MC_GETDPTR(bufID);
MC_fsWrite(fID,buf,len);
MC_fsClose(fID);
MC_knlFree(bufID);
사실 위에서 본 파일 API 들은 어느 환경에 가도 유사하게 지원되는 기본 함수에 속하는 부분이고, 특별히 사용법이 어렵다기 보다는 미묘한 실수나 에러 방지를 해주지 않아서 프로그램이 오동작하는 경우정도만 발생시키기 때문에 겁내지 마시고 그냥 하나씩 해보면서 익히는게 가장 좋은 방법이라고 생각합니다. 오히려 DB API 보다 직관적으로 쉽게 보이지 않으신지..^^
그럼 이제 남아있는 파일 API 들도 한방에 살펴보도록 하겠습니다.(오늘은 진도가 팍팍 잘 나가는군요 ~.~)
- M_Int32 MC_fsSetMode(M_Char* fileName, M_Int32 fmode, M_Int32 aMode);
- M_Int32 MC_fsMkDir(M_Char* dirName, M_Int32 aMode);
M_Int32 MC_fsRmDir(M_Char* dirName, M_Int32 aMode); - M_Int32 MC_fsGetCounts(M_Char* dirName, M_Int32 aMode);
M_Int32 MC_fsList(M_Char *name, M_Char* buf, M_Int32 bufSize, M_Int32 aMode);
M_Int32 MC_fsIsExist(M_Char* fileName, M_Int32 aMode);
M_Int32 MC_fsTell(M_Int32 fd); - M_Int32 MC_fsTotalSpace(void);
M_Int32 MC_fsAvailable(void);
MC_fsSetMode는 fileName의 속성을 fmode로 변경합니다. 속성값은 아래와 같습니다. /* 잘 사용되지는 않는 함수 같군요^^ */
- MH_FILEMODE_RDONLY // 읽기 전용모드이면 세팅된다.
MH_FILEMODE_RDWR // 읽기/쓰기 모드이면 세팅된다.
MH_FILEMODE_WRONLY // 쓰기 전용모드이면 세팅된다.
// 우리가 사용하는 SDK에서는 아래같이 재정의 되어있습니다.
#define MC_FILEMODE_RDONLY MH_FILEMODE_RDONLY
#define MC_FILEMODE_WRONLY MH_FILEMODE_WRONLY
#define MC_FILEMODE_RDWR MH_FILEMODE_RDWR
디렉토리를 생성하고자 하는 경우 MC_fsMkDir , 디렉토리를 삭제하고자 하는 경우 MC_fsRmDir 을 사용합니다. 디렉토리명에 디렉토리명 구분자는 '/'을 사용합니다.(리눅스/유닉스가 사용하는 방식이죠. 윈도우/DOS에서는 '\'을 사용합니다.) 생성시에는 이미 디렉토리가 있는 경우등에 에러가 발생할 수 있고, 삭제시에는 아직 해당 디렉토리에 파일이나 서브 디렉토리가 남아 있다면 에러가 발생합니다. 역시 특별한 경우를 제외하고 폰에서는 잘 사용하지 않는 함수이기도 합니다. /* 시스템에 따라서는 디렉토리나 파일이나 비슷한 구조로 접근하는 녀석도 있다보니 무작정 잘 디렉토리별로 분리한다고 해서 좋다고 하기도 뭐할지도 모르겠습니다. 관련 내용은 아래 추가적으로 왕창 적어둔 잡담에서^^(뭐 사실 무시해도 되는 내용일듯도 합니다만^^) */
해당 디렉토리의 모든 파일과 서브디렉토리 이름을 가져오려면 일단 MC_fsGetCounts 함수를 통해서 갯수를 얻고 그후 MC_fsList를 통해서 buf에 실제 파일명, 디렉토리명을 반환 받아야 합니다. 이때 파일과 디렉토리명이 넘어오는 형식은 위에서 DB명을 가져올때와 마찬가지로 '\0'(=NULL=0)으로 종결되는 문자열의 연속이며, 마지막 문자열은 NULL이 두번 입력되는 형태입니다.
특정 경로상의 파일이 존재하는지에 대한 확인은 MC_fsIsExist를 통해서 할 수 있습니다. 이때도 경로명의 디렉토리 구분자는 '/' 이어야 합니다.
MC_fsTell 은 현재 파일 포인터가 위치하고 있는 위치를 알려줍니다. 고전적인 방식이지만, seek 과 tell 의 조합으로 파일 사이즈를 구하는 방법이 있고 실제 Clet 에서도 가능한 방법입니다. (어떻게 하면 파일 사이즈를 구할 수 있을지에 대해서 한번 생각해보는 것도 좋겠네요.^^)
현재 기기의 파일 시스템 전체 사이즈는 MC_fsTotalSpace로 얻을 수 있습니다. 단위는 바이트(byte)입니다. 그리고 사용 가능한 파일 시스템의 여유 공간을 얻는 함수는 MC_fsAvailable 입니다. /* 다만 이 함수에서 주의할 점이 있는데, 폰에서는 MC_fsAvailable로 얻어지는 값이 정확히 그만큼 다 비어있다고 보기 어렵다는 것입니다. 이러한 문제는 여러가지 이유에서 발생하는 것으로 생각되는데, 일단 모든 파일 API 들은 blocking형 API 라고 정의된 것으로 보아 아마 API가 호출되는 시점에서는 그 값이 참일지 모르겠습니다만, 그후 시스템도 여러가지 이유로 파일 시스템을 활용하고(임시 파일이라던가 뭐 내부적으로 뭔가 작업을 하고 있다던가..등등) 따라서 우리가 이 함수를 불러서 남은 용량이 100KB 가 나왔다고 해서 100KB를 쓸 수 있겠구나 하면 흔히 이상한 현상을 볼 수 있게 될 가능성이 높다는 것입니다. 실제로 예와 같이 100KB 의 용량이 남아서 추가 다운로드등을 98KB정도 했다고 하면, 폰과 시점에 따라서 기록이 잘 되는 경우도 있겠지만, 어떤 경우는 파일 기록이 용량 부족이라고 실패하며, 어떤 경우는 잘 기록 된것 같지만, 어플을 재실행하거나 했을때 파일이 다 기록이 안되서 깨져있거나 읽어지지 않거나 하는 일이 발생하기도 합니다. 대략 경험적으로 알아야 하는 부분입니다만, 위의 함수에서 얻어지는 남은 용량에서 어느정도 여유를 빼시고 나서의 용량이 실제 파일 시스템에서 사용할 수 있는 용량이라고 보는게 좋을 것 같습니다. 뭐 어디까지나 그냥 개인적인 잡담입니다만^^ */
예제 하나 없이 파일 API 를 훅~ 넘어 갈까 했으나 뭔가 좀 아쉽잖아요. 간단히 테스트로 계속 사용하던 폴더에는 뭐가 있는지 그정도만 출력해보도록 하겠습니다. /* 전 지금까지 예제 코드를 전부 하나의 프로젝트로 동일하게 사용해 왔습니다. ^^ 솔직히 아무리 익숙하다고 해도 새로 프로젝트 만드는거 은근히 귀찮습니다.^^ */
- void bufPrint(M_Char *buf) // DB list를 출력예제에서 재활용+개조^^
{
M_Int32 idx,fidx;
M_Char filename[128];
MC_FileInfo fi;
fidx=0;
for(idx=0;idx<256;idx++) {
if (buf[idx]==0) {
filename[fidx] = 0;
fidx=0;
MC_fsFileAttribute(filename, &fi, MC_DIR_PRIVATE_ACCESS);
MC_knlPrintk(" [attr:0x%x][date:%d][size:%d]\n",fi.attrib,fi.creationTime,fi.size);
if (buf[idx+1]==0 && idx<255) break;
} else {
MC_knlPrintk("%c",buf[idx]);
filename[fidx++] = buf[idx];
}
} // 대충 만든거니깐...그대로 쓰시면 안되요..ㅎㅎ
}
void startClet(int argc, char* args[])
{
M_Int32 retVal;
M_Char buf[512];
retVal = MC_fsTotalSpace();
MC_knlPrintk("[FILE] Total = [%dbytes][%dkb][%dmb]\n",retVal,retVal>>10,retVal>>20);
retVal = MC_fsAvailable();
MC_knlPrintk("[FILE] Available = [%dbytes][%dkb][%dmb]\n",retVal,retVal>>10,retVal>>20);
retVal = MC_fsMkDir("tempDir",MC_DIR_PRIVATE_ACCESS);
MC_knlPrintk("[FILE] mkdir result[%d]\n",retVal);
retVal = MC_fsGetCounts("/", MC_DIR_PRIVATE_ACCESS);
MC_knlPrintk("[FILE] '/' count = [%d]\n",retVal);
MC_fsList("/", buf, 512, MC_DIR_PRIVATE_ACCESS);
MC_knlPrintk("[FILE LIST : '/']\n");
bufPrint(buf);
}
자 이제 실행 결과가 어떻게 나오는지 보도록 하죠. /* 주의할 점은 코드상에 MC_fsMkDir 이 들어가 있으므로 처음 실행되는 시점에서만 디렉토리 만들기가 성공한다는 것입니다. 그후는 에러 코드 -5 , M_E_EXIST (해당 리소스가 이미 존재함) 이 반환됩니다. 당연한 결과겠죠. 이미 만들어진 폴더를 또 만들려고 하는 것이니깐요^^ */
흠 제 메인 하드는 아직 100G 이상 남아 있습니다만, 뭐 파일 시스템 용량은 저 정도로 출력되는군요. 에뮬이니깐 뭐 그러려니 해야하는 부분이겠죠.
'/'은 루트 디렉토리를 가르킵니다. 이 루트는 access Mode가 MC_DIR_PRIVATE_ACCESS 에 해당하는 영역의 루트니깐 결국 어플리케이션이 설치된 곳을 기준으로 하는 루트 디렉토리겠죠. 다만 폰과 플랫폼에 따라서는 이 부분에 조금 차이가 있는데, 에뮬레이터에서는 루트 디렉토리를 출력하니 위와 같이 현재 우리가 만든 어플의 에뮬형 파일인 mod과 taf 파일도 보이고 실행을 위해 풀어진(이런식으로 동작한다는거죠^^) test.dll 파일도 보이는군요. (제 프로젝트 소스 결과물이 test.dll 입니다. 아마 실습해보는 분들마다 이런건 다 다른 결과가 나오겠죠.) 그러나 폰에서는 대개 이러한 우리가 만든 프로그램의 바이너리 파일이 담긴 파일 (SKT폰이라면 daf , KTF나 LGT라면 jar겠죠.)이 보이지 않습니다. 당연히 그게 안전하겠죠. 에뮬레이터니깐 저런 결과를 낸다고 생각하고 넘어가시면 좋겠습니다.
파일 정보 구조체 값을 각각 구해보았는데, tempDir의 경우 디렉토리라서 attr값이 0x1 로 나오는군요. 현 SDK의 디렉토리 구분 비트 플래그는 0x1 로 정의되어 있습니다. 그 값이 넘어 온것이죠. 즉 이녀석은 폴더다~ 그런거죠. date 부분은 아쉽게도 전부 이상하게 나오는군요. 에뮬이라서 그런걸테니 뭐 그러려니 하셔야 겠습니다. 파일 사이즈는 제대로 구해지는 것 같네요. 그나마 다행이랄까요^^
사실 컴퓨터에서 파일을 다루는거나 폰에서 파일을 다루는 것은 기본적으로는 거의 유사합니다. 따라서 이미 여러 프로그램 언어등을 통해서 파일 처리하는 법을 배우신 분들이라면 별로 어렵지 않게 익힐 수 있는 부분이라고 생각되고, 사실 Clet API 전체에서도 가장 무난하게 통과할 수 있는 파트가 아닐까 생각되네요. 덕분에 저도 이번 강좌는 정말 간만에 부담없이 팍팍 써내려가고 있는 느낌입니다만^^
그냥 잡담 : 안 읽으셔도 됩니다.^^
사실 이번에 다룬 내용은 크게 어려운 내용이 없었으리라 생각됩니다. 기본적인 FILE API 등을 활용해서 위에서 본 것과 같은 DB API 보다 훨씬 더 멋진 형태의 데이터 관리 환경을 만들 수도 있을 것이고, 그러한 부분은 각자의 상상의 나래를 펴야할 부분이겠죠.
여기서는 쉽게 생각하는 파일 시스템에 대해서 핸드폰에서의 파일 시스템이라는 관점으로 간단히(!) 관련은 있지만 좀 별개(?)의 이야기를 해볼까 합니다. 고로 읽지 않으셔도 무관한 부분이라는겁니다. 과제나 프로젝트 등으로 Clet을 공부하고 계시는 분들은 시간 없으면 그냥 패스해도 되는 내용입니다.^^ (사실 거의 개인 잡담이라고 보셔도 될듯한 내용입니다. ㅎㅎ)
흔히 주변의 개발자 분들에게 이번 파트 관련해서 많이 듣는 부분이 "Clet 에서는 파일 몇개나 쓸 수 있어요?" 와 같은 경험적 체험(^^)에 근거한 수치를 요구하는 질문이 주를 이룹니다만, 사실 저도 그동안 몇년 폰 시스템을 봐오면서 이 부분에 대한 정답은 결과적으로 시스템에 따른다라고 밖에는 대답할 수가 없다는 것을 말씀드리고 싶습니다.
다른 관점으로 생각해봐야 할 부분은 우리가 흔히 파일 시스템이라고 하면 프로그램 경험이 많은 사람일수록 컴퓨터에서의 파일 시스템을 다루던 경험을 떠올리게 되는데, 이는 폰의 파일 시스템에 적용함에 있어서 몇몇 부분에 무리가 있는 부분이 존재할 수 있다는 것을 이야기하려고 합니다. 전부 다 설명하는건 떠들어야할 주변(^^) 이야기가 너무 많은지라 위의 질문과 연관되는 내용만 이야기 해볼까 합니다.
우리가 폰에서 사용하는 파일 시스템이 동작하는 환경은, 곰곰히 생각해보면 컴퓨터의 HDD같은 데이터 저장 매체의 형태와는 조금 다른 flash-rom 이라는 것을 고려해야하고 더불어 흔히 쓰는 Window의 NTFS나 FAT32와 같이 검증되고 편리한(?) 파일 정보 테이블을 사용하지 않고 뭔가 공간을 적게 사용하면서 간단하게 구현된 FAT(File Allocation Table)을 사용할 것이라는 부분에 중점을 두고 생각해봐야 합니다. 즉 이런 경우가 있을 수 있죠.(실제 유사 사례를 타사 겜에서 본 적이 있습니다.) 어떤 게임에서는 기본 리소스 패키지(그게 jar(압축) 이던 daf(비압축?)같은 형태던)에서 파일을 읽으려니 seek도 안되고 순간 메모리는 더 먹고...그래서 앗싸리 빠른 로딩과 부분 로딩의 편의를 위해 추가 다운로드나 리소스에서 끄집어 내서(^^) 파일로 써두자 하고 나름 멋지게 폴더도 세세하게 구분하고(~.~) 그래서 파일을 수백개 정도(뭐 양이야 기기마다 체감 한계가 좀 다릅니다만, Clet 에서는 꽤 가능하기는 합니다.)만들었다고 하죠. 그래놓고 겜을 돌려 봅니다. 오호 요즘 폰이야 빠르니 이거 편리하고 좋구만요. 앞으로 이렇게 해야겠다. 하고 개발자는 계속 파일을 늘려갑니다.(~.~) 그러다 어플을 재설치 하려고 어플 삭제를 누르니...왠걸 폰이 바보가 되었나~ 반응을 안하는 것이죠...흠..내 폰이 꼬진건가 다른 폰에서 해보자~ 하고 다른 최신 폰에서 해보니 삭제 누르니 뭐 1-2초후에 삭제 완료라고 나옵니다. 흠..내 폰이 버그폰이구만... 이러고 패스해야할 문제일까요^^ (또 어떤 2-3년전 폰에서는(clet되는) 파일 갯수가 많으면 삭제 했는데도 어플만 지우고 리소스는 못 지우는 황당한 사태도 발생하기도 하죠.~.~)
자 이런 부분은 어떤 부분에서 문제가 되는 걸까요? 사실 정답은 폰 개발자에게 물어보는게 가장 정확하겠지만(뭐 그 개발자가 그 모델의 시스템을 기억해준다는 가정하에^^) 대략 유추를 해볼 수는 있겠죠. 일단 흔히 모바일 기기...아니 좀 영역을 넓혀서 하드웨어에 소프트웨어를 올려서 동작시키고자 할때 흔히 이야기하는 임베디드(embeded)동네를 한번 떠올려 보겠습니다.(개인적으로 임베디드도 재미난 분야라고 생각합니다.^^) 만약 여러분이 flash-rom에 FS(file-system)을 올리려고 한다면, 어떤 구조로 만드는게 효율적일까요? 아 윈도우의 FAT32나 NTFS 규격이 공개되어 있으니(정식 공개던가요? 기억이 가물) 그걸 옮기면 되겠군요...그러면 좋겠지만, 실제 현장에서는 비용적인 부분을 꽤나 고려해야합니다. 바로 뭔가 멋지구리한 시스템을 올리기에 사용할 수 있는 메모리 공간(flash-rom등)이 적다는 것이죠. 많이 쓸 수 있으면 좋겠지만, 생산 단가등을 낮추기 위해서는 최대한 안쓰는게 최고겠죠.(실제 업계에서 임베디드 리눅스가 아쉬운 부분도 이런 부분이기도 합니다만 뭐 요즘은 많이 개선되었더군요)
자 그래서 머리속에서 떠오르는데로 나름 간단한 구조를 생각해봤습니다. (문법적인건 무시합니다. 의미 전달용입니다.) /* 설마 이게 제대로 돌아가리라 생각하는 분은 없으시겠죠^^ */
- struct FILESYSTEM {
-
int filecount; // 전체 파일+디렉토리 갯수
-
struct FILE {
-
int fileID; // 인식 ID
-
char filename[32]; // 이름
-
int fileProperty; // 파일이냐 디렉토리냐등의 정보
-
long fileDate;
-
int fileSize;
-
long fileOffsetAddr; // 실제 메모리상의 파일 데이터 시작 주소
-
int parentID; // 상위 디렉토리 ID = 상위 fileID
-
} // FILE 구조체가 가능한 계속 연결되서 연속 생성
-
// 각 디렉토리간의 써칭을 빠르게 하기 위해 별도 오프셋 테이블을 가져도 좋을듯함 (뭐 이정도 단서만 달아두면 그다음이야^^)
- }
뭐 그냥 이글을 쓰면서 생각나는데로 얼렁뚱땅 적어본거니깐 뭐 유심히 보실 것도 없습니다. 디렉토리까지 고려해도 간단히 하면 저런 형태가 되지 않을까 생각해봤습니다.(물론 제대로 하려면 훨씬 더 필요할 껍니다. 당연한거죠^^)
뭐 아무튼 이렇게 만들어서 오 간단한걸~ 이정도면 뭐 모바일 기기용도로는 충분하겠어~ 하고 탑재를 했습니다.
그런데 이렇게 만들어 놓고 사용을 해보니 파일이나 디렉토리가 별로 없을때는 충분히 메모리상에 올려서 찾던지, 혹은 seek를 신나게 하던 해서 원하는 위치로 가서 파일명으로 비교해서 찾을 만 했습니다. 그렇다면, 파일이 많아졌다고 생각해보죠. 만약 우리가 만든 어플에서만 001.dat 파일부터 200.dat 파일까지 200개의 파일을 만들었다고 생각해 보겠습니다. 그럼 저런 유사한 파일 시스템 정보 테이블에도 기록이 되겠죠. 그러고 나서 어플에서 우리가 사용하는 시점에 fopen("199.dat") /* 문법 무시 */ 라고 했다고 합시다. 자 그럼 시스템은 어떻게 해당 파일이 존재하는 정확한 메모리 영역(flash-rom이니깐^^)의 시작 주소를 찾을 수 있을까요. 뭐 일단 FS의 개발자가 어느정도 신경을 써놔서 내 디렉토리를 찾아가는 것까지는 빠르게 해 두었다고 하고, 그 다음은 파일 이름으로 찾아야 할텐데, 뭐 간단히 구현했다면 그냥 해당 디렉토리에 해당하는 리스트 처음부터 뒤지겠죠(^^). 문자열 비교는 상당히 비싼 연산입니다. 최악의 경우 글자 단위 전부를 봐야할 수도 있으니깐요. 암튼 운이 나빠서(ㅎㅎ) 001.dat 파일부터 순서대로 리스트화 된 바람에 199.dat는 그야말로 199번째 비교에서 찾아냈습니다. 뭐 요즘 cpu 야 충분히 빠르니 이정도야 금방 되겠죠.(^^) 그런데 이 어플이 하나의 메뉴를 띄우기 위해 200개 파일중 한 시점에서 랜덤한 순서의 50개의 파일을 읽어야 한다고 할 경우 문제가 발생할 수 있을 것 같습니다.(실제 겜등을 제작해보면 나올 수 있는 상황이죠. 리소스 재활용이라고 해서 많이 쪼개둘수록 조합해서 읽어내야 하니깐요.) 자 그럼 위와 같은 검색을 50번 해야겠죠. 뭐 FS 만든 사람이 천재라(--;) 001.dat ~ 050.dat 를 순서대로 읽는 것을 예측하고 마지막 FAT 접근 포인터를 기억해서 거기서부터 검색했다면 linear search 라고 해도 뭐 빠르게 찾을 수 있을지도 모르겠습니다만....(그러나 가정에서 랜덤한 순서라고 했으니 이것도 안 통하겠군요--;) 설마 이런 멋진 예측 시스템이 있을꺼라고 생각하지는 않으시겠죠^^
더 멋진 상황은 쓰기의 경우에서 발생합니다. flash-rom은 특성상 읽기는 빠르게 나오지만 쓰기는 상대적으로 느립니다. 뭐 우리가 대략 사용하는 사이즈정도에서는 그정도도 무시할 수준이 될지 모르겠습니다만, 극단적으로 작은 파일 사이즈(몇KB)임에도 쓰기 속도가 심하게는 100배 넘게 차이는 폰들도 본적이 있습니다.(물론 그래봐야 1000ms이내려나요) 자 이런 경우 쉽게 생각하기로 flash-rom이 느려서 파일 쓰기가 느린건가봐. 좋은 롬 좀 쓰지~ 가 될 수도 있겠습니다만, 실상 FS의 로직상의 문제일 가능성도 있습니다. 일단 빈 영역을 알맞은 위치에 찾는 것도 꽤나 쉽지 않은 문제이니깐요.(흔히 말하는 단편화 현상과도 관련있는 부분입니다. 이런 문제 때문에 위에서 이야기한 MC_fsAvailable 에서 제대로 현실적인(실제로) 남은 용량을 못 주는 부분도 있으리라 생각합니다.^^) 이렇다면 뭔가 유저 크레이티브(^^)한 데이터를 좀 많이 생성해야 해서 파일로 팍팍 써야한다면 문제가 있을 수도 있게 되는 것이죠. 더불어 FS 에 따라서는 쓰기와 삭제가 내부적으로는 유사 로직을 가지고 있을 수 있는 경우도 있다고 생각해 볼 수 있습니다. (이런 케이스가 리소스 허벌나게 많은 어플 삭제시 맛가는 증상과 관련이 있을지도 모르죠.) 삭제라는 것을 그냥 간단히 FAT와 같은 정보 테이블에서만 지운다라고 할 수도 있지만, 어떤 시스템에서는 제대로 메모리 상태를 구분하기 위해 해당 영역이 지워졌다고 표시하는 시스템도 있을 수 있습니다. 표시라는건 결국 기록을 의미하죠. 뭐 결국 느림보가 되는건 마찬가지 일지도 모른다는 의미입니다. /* 대개 폰에서 아무런 반응을 안 먹는 경우 그냥 베터리 제거전까지 돌되는 폰도 있지만, 알아서 몇초안에 리셋되서 재시작하는 폰도 보셧을 껍니다. 대개 하드웨어 상의 무한루프 방지(뭐 간단한 예인거고, 실제 용도는 여러가지입니다만 이해를 쉽게 하기위해)를 위한 watchdog 이라는 시스템을 구현하고 있고, 이녀석이 동작해서 리셋을 작동하게 하는 경우가 많습니다. 뭐 이런 시스템이 있음에도 폰이 돌이 되는건...결국 이것마저도 못할 상태던가, 구현을 안했던가, watchdog 갱신은 이뤄지는데 하드웨어 예외처리를 거의 안했던가 하는 경우겠죠. 사실 안했을 가능성이 많다고 봅니다. 예외처리 안된 폰 많이 봤어요. 저도 신나게 죽여봤으니깐요^^ */
잠깐 이야기의 방향을 돌려서 혹시 파일을 꽤 많이 생성했는데, 어느 순간부터 fopen("파일") 이라고 열려고 하는데 반응이 너무 느리다~ 라는 경험을 해보신 적이 있으셨다면, 위와 마찬가지로 FS의 로직 문제일 가능성이 높습니다. 주절주절 언급한 내용입니다만, 결론적으로 과연 파일명을 기준으로 파일을 찾아가는 방법에서 문자열 비교를 어떻게 구현해두었을까요. 설마 그런 시스템에 linear search 같은 구닥다리 무식(--;)한 방법을 쓰겠어요~ 라고 하신다면, 그럴 확율이 높다는 겁니다. 안 그래도 부족한 메모리(물론 요즘이야 여유 넘치기도 하지만요)에 binary search같은걸 구현하려고 시스템이 부팅할때 파일명을 이쁘게 정렬시켜 둔다거나(binary search는 속도상 빠른 검색 방법이지만, 기본 데이터가 정렬되어 있어야 한다는 기본 전제 조건이 요구됩니다.)하는 일은 결국 메모리를 사용하는거죠. 뭐 메모리를 여유롭게 장착한 시스템에서야 개발자가 여유롭게(^^) 그렇게 만들어 두었을지도 모르겠습니다만...과거에 보았던 위피 초기폰들은 거의 제 예상대로 linear 방식이 꽤 많은 것 같았습니다. 파일 갯수가 많아질 수록 파일의 open시간이 느려졌거든요.
실제 제가 작업에 참여하고 상용화 하는 게임에서는 따로 파일의 패키징 방법을 구현해서 사용하고 합니다만, 최근에는 거의 툴의 작업에 공을 들여서 내부 파일 써칭은 가급적 바로 index로 날라가도록(fly~ ??^^) 하는 방식을 선호하고 있습니다. 대략 몇백개까지는 안되던 파일을 파일명으로 로딩하던 시절이 있었는데(하나의 패키지 파일안에 파일명과 오프셋테이블, 데이터가 기록된 형태죠) 첨에는 뭐 몇개 되겠어 하고 linear하게 search했다가 겜 화면 뜨는 시간 보고 좌절했습니다. 바로 패키징하는 어플 좀 손 보고 검색 방식을 binary로 바꿨습니다. 정말 뻥 좀 포함해서 뚝딱~하고 뜨더군요. ^^ 뭐 실제 폰 시스템이라고 크게 다르지는 않을꺼라고 생각하는 것도 이런 경험과 과거 임베디드 삽질 경험에서 나오는 것입니다만...뭐 어디까지나 제 개인적인 생각일뿐입니다.
그래서 하고자 하는 이야기는 무엇이냐...현재까지의 모바일 기기에서의 파일 시스템 경험으로 가장 쓰기 좋은(혹은 합리적이라고 생각하는) 방법은 각자 노하우로 각자 어플에 맞게 구현된 자체 패키지 형태를 사용하는 것이 좋을 것이라는 것입니다. 일단 그렇게 되면 파일 시스템상의 파일 갯수도 줄어 들고, 플래쉬 메모리상에서의 seek야 거의 후딱이고, 원하는 위치를 찾는 방법도 자신이 사용하고자 하는 형태에 맞춰서 최적화가 가능하므로 현재로서는 이런 방식을 추천해드리고 싶습니다. 물론 이렇게 하기 위해서는 리소스를 묶을 어플리케이션(흔히 말해 툴~)이 필요해집니다. 더불어 KTF나 LGT에서는 그냥 제공(?)되던 좋은 jar 압축도 버려야 하고요. 결국 압축 알고리즘도 공부하게 되죠^^. 역시 뭔가 시작해버리면 끝없이 공부할게 연속해서 생기는 것이 세상의 이치인가 봅니다.~.~ 아무튼, 폰이나 기타 모바일 기기등에서 어플리케이션을 만들어 가면서 파일 시스템등에 고민+갈등을 겪으셨던 분이 1%라도 그냥 공감하는 해주시면 그걸로 이 주절주절글은 의미가 있는 거 아닐까 생각되네요. 어찌되었건~ 고생했던 기억(=추억)도 나누면 좋지 않을까하는 생각입니다.^^
/* 최근의 폰들은 시스템의 성능이 더욱 올라가고 있으므로 뭐 이제 이런 내용도 별 의미없는 내용이 될 날이 조만간 올꺼라고 생각합니다. 그치만 폰 동네가 좀 그렇죠. 과거폰이라고 무작정 안할 수도 없기도 하고...아무튼 혹시 이런 문제로 고민하셨던 분들에게 그냥 같이 고생해본 개발자로서 떠들어 본 잡담글 이었습니다.^^ 뭐 뭔가 이상한 내용이 있더라도 뭐 그러려니 해주세요. 향후 폰들은 lcd refresh나 더 빨라졌으면 좋겠어요. 이제는 이쪽이 더 문제인듯한 ㅎㅎ */
정리
이번 시간에는 데이터를 읽고 저장하고 관리하는 부분인 DB API 와 FILE API 를 알아보았습니다.
위에서도 언급했지만, 아마 이번 API 부분은 어느정도 친숙한 분들도 많을 것 같고, 그리 내용도 어렵지 않고 사용시에 약간만 주의해주면 큰 문제 없는 부분이기에 본 내용에서도 간단간단~ 하게 넘어간 부분도 있지만, 주요한 기능은 살펴보았다고 생각합니다.
다음 시간에는 에뮬에서는 별로 고생을 안하지만(정말?) 폰에서는 유별나게 엄청 고생하게되는 Network 동네를 방문해보도록 하겠습니다.(^^)
분명 쉽지 않은 파트일테니 뭐 이번 시간은 어려운 동네로 가기위한 잠깐의 휴식이었다고 생각해도 될듯 하네요. 뭐 그렇다고 너무 겁내실 필요는 없구요.
학교의 과제나 프로젝트등을 만들기 위해 Clet 을 공부하는 분들이라면 Network 파트는 패스해도 괜찮을 듯 싶습니다. 뭐 욕심을 부리시겠다면 말리지는 않겠구요.^^
그럼 다음 내용에서 뵙도록 하겠습니다. 이상한 주절주절 글까지 포함해서 긴글 읽으시느라 고생 많으셨습니다. 이번 한 주도 화이팅~하세요.
본 내용은 크리에이티브 커먼즈 코리아 저작자표시-비영리-동일조건변경허락 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
분문의 오류나 오타등의 문의는 문의 게시판에 해주시기 바랍니다. 본 내용은 지속적으로 업데이트 될 수 있습니다.^^
Email : juno@evermore.pe.kr