앞선 포스팅에서는 Internal/External 저장소에 사용자의 데이터를 저장하는 방법에 대해 살펴 보았습니다.
오늘은 데이터베이스를 활용하여 원하는 데이터를 저장하는 방법에 대해 알아보겠습니다.
Intenrnal/External 저장소에 저장하는 것 보다 데이터베이스를 이용하여 저장할 때는, 동일한 형태의 데이터를 저장할 수 있다는 점에서 더 편리합니다.
회원정보를 저장할 경우가 대표적인 예인데, 이 경우 회원의 이름, 성별, 전화번호, 주소 등등이 각각 DB의 속성(Attribute) 가 될 것이고,
실제 들어가는 데이터 (김태희, 여, 010-0000-0000, 경기도.. ) 들이 값(Value) 이 될 것입니다.
1. SQLiteOpenHelper 를 이용한 DB 생성
SQLiteOpenHelper Class 는 Database 를 생성하고 해당 Database 의 Version 을 관리합니다.
해당 Class 를 사용하기 위해서 Database가 처음 생성될 때 불리는 onCreate(..) 와 Database 가 Upgrade 될 경우에 불리는 onUpgrade(..) callback method 를 구현해 주어야 하며, 필요에 따라 onOpen(..) 함수와 onDowngrade(..) 함수도 구현하여 사용할 수 있습니다.
저는 SQLiteOpenHelper를 상속 받은 MySQLiteOpenHelper 를 생성해서 간단하게 onCreate/onUpgrade 만 구현해 주었습니다.
public class MySQLiteOpenHelper extends SQLiteOpenHelper {
private final String TAG = "MySQLiteOpenHelper";
public MySQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
String sql = "create table student (_id integer primary key autoincrement, name text, age integer, address text)";
sqLiteDatabase.execSQL(sql);
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
String sql="drop table if exists student";
sqLiteDatabase.execSQL(sql);
onCreate(sqLiteDatabase);
}
}
onCreate 에서는 Database를 생성해 주었고, 각 Field 는 name, age, address 로 구성될 수 있도록 하였습니다.
"create table student (_id integer primary key autoincrement, name text, age integer, address text)";
위 Query 로 DB생성 후 실제 만들어진 DB 를 열어보면 다음과 같이 data 가 저장됩니다.
그리고 onUpgrade 함수에는 현재 생성되어있는 Database 를 지우고 onCreate 를 통해 다시 생성될 수 있도록만 구현을 해 두었습니다.
2. DB에 데이터 삽입 및 삭제
DB에 데이터 삽입/삭제를 위해 MyDBHandler 클래스를 생성하였습니다.
MyDBHandler 클래스에서는 DBActivity 로 부터 사용자 정보를 받아 MySQLiteOpenHelper 로 전달 합니다.
데이터를 저장 할 때 ContentValues 를 이용했는데, ContentValues 는 Data 를 Key와 Value의 Set 으로 저장할 수 있습니다.
그래서 Database 에 값을 줄때 "name, 홍길동" ,"age, 24" 등과 같이 Key 와 Value 의 형태로 쉽게 전달 가능합니다.
public class MyDBHandler {
private final String TAG = "MyDBHandler";
SQLiteOpenHelper mHelper = null;
SQLiteDatabase mDB = null;
public MyDBHandler(Context context, String name) {
mHelper = new MySQLiteOpenHelper(context, name, null, 1);
}
public static MyDBHandler open(Context context, String name) {
return new MyDBHandler(context, name);
}
public Cursor select()
{
mDB = mHelper.getReadableDatabase();
Cursor c = mDB.query("student", null, null, null, null, null, null);
return c;
}
public void insert(String name, int age, String address) {
Log.d(TAG, "insert");
mDB = mHelper.getWritableDatabase();
ContentValues value = new ContentValues();
value.put("name", name);
value.put("age", age);
value.put("address", address);
mDB.insert("student", null, value);
}
public void delete(String name)
{
Log.d(TAG, "delete");
mDB = mHelper.getWritableDatabase();
mDB.delete("student", "name=?", new String[]{name});
}
public void close() {
mHelper.close();
}
}
select 함수는 Database 의 전체 내용을 return 해주고, insert/delete 함수는 각각 Database에 데이터를 추가/삭제 시 호출 됩니다.
delete 의 경우 name 을 인자로 받아서 이름이 동일 하면 해당 row 를 모두 삭제하도록 하였습니다.
3. 동작 확인
기존에 작성하였던 MainActivity 에 Database 버튼을 하나 추가한다음에, 이 버튼을 눌렀을 때 Activity 를 전환하여 Database 의 내용을 뿌려주도록 하였습니다.
가장 상단에는 Database 에 내용을 추가할 수 있도록 Editbox 를 3개 두었고, 각각 이름/나이/주소 정보를 저장할 수 있도록 하였습니다.
Update Database 를 누르게 되면 Editbox 에 추가한 내용이 Database 에 업데이트 되고 리스트의 내용이 업데이트 됩니다.
리스트의 각 항목을 롱클릭 하게 되면 해당 Field 가 삭제 됩니다. ^^
여기까지 Database 를 이용한 아주 간단한 사용자 데이터 저장에 대해서 살펴보았습니다.
모든 소스는 https://github.com/bettercho/MyGithub/tree/master/storeuserdata 를 참고하시면 됩니다.
부모 프로세스가 fork() 함수를 사용하여 자식 프로세스를 생성하였을 때, fork() 함수가 리턴되는 시점부터 2개의 프로세스가 동작하게 됩니다.
부모 프로세스가 자식 프로세스의 종료 상태를 얻기 위해서는 wait() 함수를 사용합니다.
다른 말로 표현하면 wait() 함수를 사용하여 자식 프로세스가 종료 될 때까지 기다릴 수 있습니다.
wait() 함수는 아래와 같이 동작합니다.
1. 자식 프로세스가 동작 중이면 호출 차단이 차단되기 때문에 상태를 얻어올 때까지 대기
2. wait() 함수 호출자가 시그널을 받을 때까지 대기
3. 자식 프로세스가 종료된 상태라면 즉시 호출이 반환되어 상태를 얻음, 이 때 wait() 함수는 자식 프로세스의 프로세스 ID를 반환
4. 자식 프로세스가 없다면 호출이 즉시 반환되며, 에러값을 반환
#include <sys/wait.h>
pid_t wait(int *statloc);
성공 : 프로세스 ID 반환
오류 : -1
자식 프로세스가 정상 종료되었을 때와 비정상 종료 되었을 때, wait() 함수의 반환값과 statloc 인자값이 다르게 채워집니다.
wait 함수 반환 값
statloc 값
자식 프로세스가 정상적으로 종료
프로세스 ID
- WIFEXITED(statloc) 매크로가 true를 반환
- 하위 8비트를 참조하여 자식 프로세스가 exit, _exit, _Exit에 넘겨준 인자값을 얻을 수 있음, WEXITSTATUS(statloc)
자식 프로세스가 비정상적으로 종료
프로세스 ID
- WIFSIGNALED(statloc) 매크로가 true를 반환
- 비정상 종료 이유를 WTERMSIG(statloc) 매크로를 사용하여 구할 수 있음
wait 함수 오류
-1
- ECHILD : 호출자의 자식 프로세스가 없는 경우
- EINTR : 시스템 콜이 인터럽트 되었을 때
예제를 통하여 살펴 보도록 하겠습니다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>
int main() {
pid_t childPid;
int status,i;
childPid = fork();
if(childPid > 0) { // 부모 프로세스
pid_t waitPid;
printf("부모 PID : %ld, pid : %d %d \n",(long)getpid(), childPid, errno);
for(i=0;i<5;i++) {
sleep(1);
}
waitPid = wait(&status);
if(waitPid == -1) {
printf("에러 넘버 : %d \n",errno);
perror("wait 함수 오류 반환");
}
else {
if(WIFEXITED(status)) {
printf("wait : 자식 프로세스 정상 종료 %d\n",WEXITSTATUS(status));
}
else if(WIFSIGNALED(status)) {
printf("wait : 자식 프로세스 비정상 종료 %d\n",WTERMSIG(status));
}
}
printf("부모 종료 %d %d\n",waitPid,WTERMSIG(status));
}
else if(childPid == 0){ // 자식 프로세스
printf("자식 PID : %ld \n",(long)getpid());
printf("자식 종료\n");
exit(0);
}
else { // fork 실패
perror("fork Fail! \n");
return -1;
}
return 0;
}
fork를 한 후에, 부모 프로세스는 5초간 sleep을 하였기 때문에 wait 함수를 호출하기 전에 자식 프로세스는 종료된 상태입니다.
따라서 wait 함수를 호출하면 자식 프로세스의 상태를 즉시 반환 받을 수 있습니다.
실행 결과를 보겠습니다.
자식 프로세스는 정상 종료 되었으며 WEXISTSTATUS(status) 매크로를 사용하여 자식 프로세스가 반환한 0값을 얻어왔습니다.
부모 PID : 34989, pid : 34999 0
자식 PID : 34999
자식종료
wait : 자식프로세스정상종료 0
부모종료 34999 0
이번에는 자식 프로세스에 sleep 함수로 지연을 발생시켜서 부모 프로세스가 wait() 함수를 호출하였을 때 자식 프로세스가 종료될 때까지 대기하도록 만들어 보겠습니다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>
int main() {
pid_t childPid;
int status,i;
childPid = fork();
if(childPid > 0) { // 부모 프로세스
pid_t waitPid;
printf("부모 PID : %ld, pid : %d %d \n",(long)getpid(), childPid, errno);
waitPid = wait(&status);
if(waitPid == -1) {
printf("에러 넘버 : %d \n",errno);
perror("wait 함수 오류 반환");
}
else {
if(WIFEXITED(status)) {
printf("wait : 자식 프로세스 정상 종료 %d\n",WEXITSTATUS(status));
}
else if(WIFSIGNALED(status)) {
printf("wait : 자식 프로세스 비정상 종료 %d\n",WTERMSIG(status));
}
}
printf("부모 종료 %d %d\n",waitPid,WTERMSIG(status));
}
else if(childPid == 0){ // 자식 코드
printf("자식 PID : %ld \n",(long)getpid());
for(i=0;i<5;i++) {
sleep(1);
}
printf("자식 종료\n");
exit(0);
}
else { // fork 실패
perror("fork Fail! \n");
return -1;
}
return 0;
}
자식 프로세스가 5초 동안 동작하는 동안 부모 프로세스는 wait() 함수를 호출하여 자식 프로세스의 종료 상태를 얻기 위해 대기합니다.
실행 결과를 살펴 보도록 하겠습니다.
기대했던 것과 다르게 wait함수는 -1를 반환하였습니다. 에러의 원인을 찾기 위해 errno 출력과 perror 함수를 이용하였습니다.
부모 PID : 35650, pid : 35660 0
자식 PID : 35660
자식종료
에러넘버 : 4
wait 함수오류반환: Interrupted system call
부모종료 -1 126
"Interrupted system call" 에러 문구가 wait 함수가 성공하지 못한 이유를 알려주고 있습니다.
Advanced Programming in the UNIX Environment 도서에 10.5 절에 나와 있듯이 유닉스 계열의 시스템에서는 '느린' 시스템 호출에 의해 차단되어 있는 도중에 신호를 잡으며 그 시스템호출이 가로채입니다. 이런 경우 시스템 호출 함수는 errno를 EINTR로 설정하고 오류를 반환합니다.
wait() 함수는 시스템 콜 함수이며 '느린' 시스템 콜 함수에 속합니다.
이 문제를 해결하기 위해서는 오류 반환을 검사하여 오류가 발생한 시스템 콜 함수를 다시 호출하는 방법을 사용합니다.
"Interrupted system call" 오류가 발생하더라도 wait함수를 재실행할 수 있도록 아래와 같이 코드를 수정합니다.
while문을 사용하여 wait 함수가 -1 값을 반환하고 errno 값이 EINTR일 경우에 wait함수를 재실행하도록 코드를 추가하였습니다.