※ DB 락 문제

: SQLite의 가장 큰 문제는 DB락이다. DB 작업은 메인 스레드보다는 백그라운드 스레드에서 사용되는 게 권장되 스레드간에 DB 작업 시 락을 잡는 시점이 겹치면서 문제가 발생한다.


- 락 상태

: DB에 쓸 때는 exclusive(배타) lock을 잡고 읽을 때는 shared(공유) lock를 잡는다. 배타 락은 다른 락을 허용하지 않고 공유 락은 다른 공유락을 허용한다. 락 상태는 아래와 같이 있다.

1. UNLOCKED : 읽기와 쓰기가 안 되는 기본 상태이다.


2. SHARED : 읽기만 된다. 다른 스레드에서 동시에 공유 락을 가져도 된다. 단, 다른 스레드에서 쓰기는 못 한다. 쓰기를 위해서는 모든 공유 락이 해제될 때까지 기다려야 한다.


3. RESERVED : 쓰기를 예약한다. 예약 락은 하나만 있을 수 있고 여러 공유 락과 같이 있을 수 있다. 또한 새로운 공유 락도 허용된다.


4. PENDING : 쓰기를 예약하되 최대한 빨리 쓰려고 한다. 따라서 새로운 공유 락이 허용 안 된다.


5. EXCLUSIVE : 파일에 쓴다. 다른 락과 같이 있을 수 없으며 하나만 허용된다. 배타 락을 가질 시 다른 작업이 불가능해 배타 락에서의 작업을 최소화 해야 한다.


: DB 락이 발생하는 원인은 CREATE, UPDATE, DELETE에서 쓰기를 하면 배타 락을 잡기 때문이다. 특히, CUD에서 쓰기를 한꺼번에 하는 트랜잭션을 할 때 배타 락을 오래 잡는다.


- 트랜잭션 동작 방식

1. deferred : 락을 뒤로 미룬다. 트랜잭션을 시작할 때 락을 잡지 않다가 읽기 작업이 있으면 공유 락을 잡고 쓰기 작업이 있으면 예약 락을 잡는다. 즉, 락이 뒤로 미뤄지기 때문에 다른 스레드에서 DB 작업이 가능하다


2. immediate : 트랜잭션을 시작할 때 예약 락이 잡힌다. 따라서 다른 스레드에서 읽기는 할 수 있으나 예약 락은 1개만 가능하기 때문에 다른 immediate 트랜잭션을 시작할 수 없다.


3. exclusive : 트랜잭션을 시작할 때 배타 락이 잡힌다. 따라서 트랜잭션 처음부터 끝까지 다른 스레드에서 DB 작업을 할 수 없다.


: 안드로이드에서는 immediate와 exclusive 동작 방식만 지원하고 exclusive 동작 방식보다는 immediate 동작 방식을 권장한다. 기본적으로 안드로이드는 exclusive 동작 방식을 한다. 따라서 db.beginTransaction() 보다는 immediate 동작 방식인 db.beginTransactionNonExclusive()를 사용하면 좋다. 이렇게 하면 DB 락 문제를 조금은 회피 가능하다. 


- DB 락 예

1. 여러 스레드에서 1개의 SQLiteDatabase 인스턴스만 가지고 쓰기를 한다. => DB 락 X

2. 여러 스레드에서 각각의 SQLiteDatabase 인스턴스를 가지고 쓰기를 한다. => DB 락 O

3. 1개의 스레드에서 1개의 SQLiteDatabase 인스턴스를 가지고 계속 쓰기를 하고 여러 스레드에서 각각 SQLiteDatabase 인스턴스를 가지고 읽기만 한다. => DB락 O

4. 여러 스레드에서 각각 SQLiteDatabase 인스턴스를 가지고 읽기만 한다. => DB 락 X

5. 1개의 SQLiteDatabase 인스턴스를 가지고 쓰기 트랜잭션과 읽기를 동시에 한다. => DB 락 X

* 많은 데이터를 처리할 때 트랜잭션으로 감싸면 더 속도가 빨라진다.


: 위 예를 볼 때 SQLiteDatabase 인스턴스를 1개만 가지고 여러 스레드에서 작업하면 DB락이 발생하지 않는다. 따라서 DB 락 문제를 해결하려면 SQLiteDatabase 인스턴스를 1개만 유지하면 된다. 하지만 SQLiteDatabase 인스턴스를 1개만 사용하면 속도가 많이 느리다. 따라서 만약 여러 스레드에서 읽기만 한다면 읽기 전용 DB와 쓰기 전용 DB를 만드는 게 좋다. 읽기의 경우 SQLiteDatabase 인스턴스를 각각 생성해도 DB 락이 발생하지 않기 때문이다.



※ SQLiteOpenHelper 클래스

: SQLiteOpenHelper 클래스는 DB 생성과 DB 버전 관리를 알아서 해주므로 SQLiteDatabse 말고 반드시 SQLiteOpenHelper 클래스를 사용하자.

public class DatabaseHelper extends SQLiteOpenHelper{
	private static final String DATABASE_NAME = "example.db";
	private static final int DATABASE_VERSION = 2;
	
	DatabaseHelper(Context context){
		super(context, DATABASE_NAME, null, DATABASE_VERSION);
	}
	
	@Override
	public void onCreate(SQLiteDatabase db){
		db.execSQL(......);
	}
	
	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
		for(int i=oldVersion+1; i<=newVersion; i++){
			processUpgrade(db, i);
		}
	}
}

DB마다 별도의 DB 헬퍼가 필요하다. 생성자의 DATABASE_NAME이 DB 파일명이다. 생성자의 세 번째 인자에 null을 넣으면 쿼리 결과로 SQLiteCursor 가 반환된다. 대부분 SQLiteCursor을 사용하므로 null 값을 보통 넣는다. 참고로 DB는 SQLiteOpenHelper 생성자에서 생성되지 않는다. getReadableDatabase()나 getWritableDatase 호출할 때 DB 생성/열기가 된다. 즉, onCreate/onOpen/onUpdate가 호출된다. 만약 SQLiteOpenHelper가 이미 생성되었다면 다시 생성하지 않는다. DB 업데이트는 위 소스와 같이 한다. 기존 버전에서부터 각 버전마다의 변경 내용을 적용해야 한다. 추가로 onCreate와 onUpdate는 순차적으로 실행되지 않는다. 둘 중 하나만 호출된다. 따라서 onUpdate에 최근 수정 사항을 코딩했다고 onCreate에 안 하면 안 된다. 처음 앱을 설치했을 때는 onCreate만 호출되기 때문이다. onCreate도 최근 수정 사항을 반영하여 코딩해야 한다.

onCreate와 onUpdate 메서드는 트랜잭션으로 이미 감싸져 있다. 따라서 별도로 트랜잭션을 고려할 필요가 없다.


메모리 DB라는 게 있다. SQLiteOpenHelper 생성자에서 DAATABASE_NAME 대신 null 값을 넣으면 된다. 메모리 DB는 기존의 파일 DB 보다 속도가 훨씬 빠른 대신 휘발성이다. 캐시 용도로 사용하는 게 좋다. 휘발성이기 때문에 버전에 신경쓸 필요는 없다. 


앞 부분에서 DB 락 문제가 생기지 않으려면 하나의 인스턴스만 생성해야 한다고 했다. 따라서 DB 헬퍼는 싱글턴 패턴을 만들어서 사용해야 한다.

private static DatabaseHelper instance;

public static synchronized DatabaseHelper getInstance(Context context){
	if(instance == null){
		instance = new DatabaseHelper(context.getApplicationContext());
	}
	return instance;
}

private DatabaseHelper(Context context){
	....
}

참고로 생성자 인자로 getApplicationContext()를 전달했다. getApplicationContext()는 앱이 살아 있는 동안 제거되지 않기 때문이다. 

close() 메서드는 거의 호출할 필요가 없다. close 메서드를 호출하면 SQLiteDatabase 인스턴스는 null이 된다. 만약 다른 스레드에서 접근할 시 close 때문에 문제가 될 수 있다. close()는 거의 호출 하지 않는 게 좋다. 

DB 기능을 변경할 때는 onConfigure()와 onOpen()을 사용하면 된다. onConfigure()는 SQLiteDatabase 생성/열기 이후, onCreate()와 onUpdate() 전에 호출되며 WAL이나 외래키 지원 기능을 활성화할 수 있다. onOpen()은 onCreate()와 onUpdate() 이후에 호출 되며 DB 연결 설정을 변경할 때 사용한다. 


* 로컬 프로세스에만 데이터가 쓰이면 콘텐트 프로바이더를 사용 안 하는 걸 권장한다. DB에 직접 접근하는 코드에서도 콘텐트 프로바이더를 사용 안 하는 게 좋다. 콘텐트 프로바이더는 외부에 접근하거나 프로세스가 분리될 때 사용하는 게 좋다.

+ Recent posts