데이터베이스 동시 접근 – Sqlite

2023. 1. 13. 19:23모바일프로그래밍/안드로이드

728x90

SQLiteOpenHelper 구현이 되어있다고 다음과 같이 가정해보자.

public class DatabaseHelper extends SQLiteOpenHelper {... }
 

자 그럼 여기서 서로 다른 쓰레드에서 다음과 같이 데이터베이스 데이터를 접근하는 코드를 만들어 본다.

// 쓰레드 1
Context context =getApplicationContext();
DatabaseHelperhelper = new DatabaseHelper(context);
SQLiteDatabasedatabase = helper.getWritableDatabase();
database.insert(…);
database.close();

// 쓰레드 2
Context context =getApplicationContext();
DatabaseHelperhelper = new DatabaseHelper(context);
SQLiteDatabasedatabase = helper.getWritableDatabase();
database.insert(…);
database.close();
 

Logcat은 아래와 같은 메시지를 뿌릴테고, 동시에 둘 중에 하나는 데이터베이스를 변경하지 못하는 상황이 발생되었을 것이다.

android.database.sqlite.SQLiteDatabaseLockedException:database is locked (code 5)

왜냐하면, 새로운 SQLiteOpenHelper 오브젝트를 생성 할 때 마다 사실, 새로운 데이터베이스 연결 이 생기기 때문에이런 일이 발생한다. 만약 동시에 다른 실제 명시적인 연결에서 데이터베이스 쓰기를 시도한다면, 실패 할 것이다.

다중 쓰레드 상에서 데이터베이스를 사용하기 위해서 확실히 하나의데이터베이스 연결만 사용해야만 한다.

 

 

그럼, 다음과 같이 단일 SQLiteOpenHelper 오브젝트를 리턴 하고 인스턴스를 가지고 있는 싱글톤 DatabaseManager 클래스를 만들어 보도록 하자.

public class DatabaseManager {

	private staticDatabaseManager instance;
	private staticSQLiteOpenHelper mDatabaseHelper;

	public staticsynchronized void initializeInstance(SQLiteOpenHelper helper) {
		if(instance == null) {
			instance = new DatabaseManager();
			mDatabaseHelper = helper;
		}
	}

	public staticsynchronized DatabaseManager getInstance() {
		if(instance == null) {
			throw new IllegalStateException(DatabaseManager.class.getSimpleName() +
			" is not initialized, call initialize(..) method first.");
		}

		returninstance;
	}


	publicsynchronized SQLiteDatabase getDatabase() {
		return mDatabaseHelper.getWritableDatabase();
	}

}
 

분리된 쓰레드에서 데이터베이스로 데이터를 쓰는 코드를 만드는 것은 다음과 같은 형식이 될 것이다.

// In your application class
DatabaseManager.initializeInstance(new DatabaseHelper());

// Thread 1
DatabaseManager manager =DatabaseManager.getInstance();
SQLiteDatabase database = manager.getDatabase()
database.insert(…);
database.close();

// Thread 2
DatabaseManager manager =DatabaseManager.getInstance();
SQLiteDatabase database = manager.getDatabase()
database.insert(…);
database.close();
 

이것도 다음 같은 충돌 상황을 만들어 낼 것이다.

java.lang.IllegalStateException: attempt to re-open analready-closed object: SQLiteDatabase
 
 

우리가 오직 하나의 데이터베이스 연결을 사용하기 전에, getDatabase() 메서드는 쓰레드1  쓰레드2 에대해서 똑같은 SQLiteDatabase 오브젝트를 리턴 해야 한다.

쓰레드2가쓰고 있을 지 모르는 데이터베이스를 쓰레드1 이 닫아 버리는 일이 벌어진 것이고, IllegalStateException 충돌 상황이 발생 한 것이다.

 

그래서, 데이터베이스가 이제 더 이상 사용하고 있지 않은지그리고 닫아도 되는 지 확실히 알 수 있어야 한다.

 

stackoveflow에서 말하는 어떤 이는 SQLiteDatabase를 절대 닫지 말아야 한다고 권고했는 데, logcat 은 당신에게 아래와 같은 존경(?)의 메시지를 보낼 것이다. 이는 좋은 생각이 아니라고 생각한다.

Leak found
Caused by: java.lang.IllegalStateException:SQLiteDatabase created and never closed
 

실제 예제

이에 대한 가능한 솔루션 하나는 데이터베이스 연결이 열렸는지/닫혔는지추적하기 위한 카운터를 만들어 주는 것이다.

public class DatabaseManager {

	privateAtomicInteger mOpenCounter = new AtomicInteger();
	private staticDatabaseManager instance;
	private staticSQLiteOpenHelper mDatabaseHelper;
	privateSQLiteDatabase mDatabase;

	public staticsynchronized void initializeInstance(SQLiteOpenHelper helper) {
		if(instance == null) {
			instance = new DatabaseManager();
			mDatabaseHelper = helper;
		}
	}

	public staticsynchronized DatabaseManager getInstance() {
		if(instance == null) {
			throw new IllegalStateException(DatabaseManager.class.getSimpleName() +
			" is not initialized, call initializeInstance(..) methodfirst.");
		}
		returninstance;
	}

	publicsynchronized SQLiteDatabase openDatabase() {
		if(mOpenCounter.incrementAndGet() == 1) {
			//Opening new database
			mDatabase = mDatabaseHelper.getWritableDatabase();
		}
		returnmDatabase;
	}

	publicsynchronized void closeDatabase() {
		if(mOpenCounter.decrementAndGet() == 0) {
			//Closing database
			mDatabase.close();
		}
	}
}
 

그리고 다음과 같이 쓰면 된다.

SQLiteDatabase database = DatabaseManager.getInstance().openDatabase();
database.insert(...);
// database.close(); Don't close it directly!
DatabaseManager.getInstance().closeDatabase(); // correctway
 

데이터베이스가 필요할 때마다 매번 DatabaseManager 클래스의 openDatabase() 메서드를 호출 해야만 한다.

 

이 메서드 내에 얼만큼 데이터베이스가 열려졌는지 카운터를 만들어 놓는다.

 

만약 카운터가 1과 같다면, 새로운 데이터베이스 연결을만들어도 된다는 뜻이고, 아니라면 데이터베이스 연결이 이미 열려진 상태라는 것을 알 수 있다.

 

closeDatabase() 메서드도 마찬가지이다. 이 메서드를 매번 호출해서 카운트를 감소시키고, 0이 되는 때에 데이터베이스 연결을 닫아야만 한다.

 

이렇게 되면 안전하게 데이터베이스를 사용할 준비가 된 것이다 – 쓰레드에안전하다라는 말이다.

 

 

참고사이트

1. http://www.dmytrodanylyk.com/concurrent-database-access/

 

728x90