android/Jetpack

Room DB 와 RoomTrackingLiveData

android by kotlin 2022. 11. 3. 14:04

Room 의 구성요소인 Dao 에서 getAll() 과 같은 메서드에서는 리턴타입을 LiveData 형태로하면 연결된 테이블의 데이터가 변경되면 옵저버들에게 통지가 가도록 만들 수 있습니다. 그런데 생각을 해보다 좀 이상한 부분이 있었습니다. LiveData 는 관찰 가능한 데이터 홀더 클래스로 값이 변경될 때 setValue 또는 postValue 메서드를 호출하여 옵저버들의 onChanged 메서드를 호출하여 통지를 해주는 방식으로 동작합니다. 그렇게 하려면 데이터소스에서 데이터를 읽어온 후 LiveData 에 값을 설정하는 메서드에서 setValue 를 호출해줘야지만 옵저버들에게 통지가 가게됩니다. 즉 아래와 같이 Activity 또는 Fragment 에서 먼저 LiveData 옵저버를 등록한 후 서버에서 데이터를 받아오는 메서드를 호출하여 LiveData 의 데이터가 갱신되도록 하여 통지를 받는 방식으로 구현됩니다.

 

//ViewModel class
private val _data = MutableLiveData<List<Data>>()
val data: LiveData<List<Data>> get() = _data
	
fun loadData() {
	
	viewModelScope.launch {
		val response = RemoteRepository.load()
		_data.value = response
	}
}

 

//Activity or Fragment 
fun initObservers() {
	viewModel.data.observe(this) { data ->
		//View Update
	}
	viewModel.loadData()
}

 

하지만 Room 에서 LiveData 를 사용할 때는 데이터가 변경되었을 때(Table 이 변경되었을 때) 명시적으로 setValue 를 호출하는 곳이 보이지 않았습니다. 그래서 도대체 어떻게 테이블이 변경되었을 때 LiveData 에서 관찰중인 옵저버들에게 통지를 해줄 수 있는지 도저히 이해가 가지 않았습니다. 그래서 생성된 DaoImpl 클래스를 확인해보니 반환타입을 LiveData 로 하였을 때 그냥 LiveData 를 반환하는 것이  아닌 createLiveData 메서드를 호출하여 LiveData 를 상속받은 RoomTrackingLiveData 라는 클래스의 객체를 반환하는 것을 확인하였습니다. RoomTrackingLiveData 클래스는 Entity Table 내의 데이터 변경을 관찰하여 데이터가 변경되었을 때 변경된 리스트를 읽어와서 postValue 메서드를 호출하는 방식으로 동작하였습니다. 아래의 코드는 RoomTrackingLiveData 의 동작 방식을 간략히 발췌한 것입니다.

 

//Dao Impl 클래스

@Override
public LiveData<List<UserEntity>> getAll() {
	final String _sql = "SELECT * FROM userentity";
	final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
	return __db.getInvalidationTracker().createLiveData(new String[]{"userentity"}, false, new Callable<List<UserEntity>>() {

//관찰중인 테이블이 변경되었을 때 call 메서드가 호출되어 변경된 데이터를 모두 읽어옵니다.
		@Override
		public List<UserEntity> call() throws Exception {
			final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
			try {
				final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid");
				final int _cursorIndexOfUserName = CursorUtil.getColumnIndexOrThrow(_cursor, "userName");
				final int _cursorIndexOfAge = CursorUtil.getColumnIndexOrThrow(_cursor, "age");
				final List<UserEntity> _result = new ArrayList<UserEntity>(_cursor.getCount());
				while(_cursor.moveToNext()) {
					final UserEntity _item;
					final String _tmpUid;
					if (_cursor.isNull(_cursorIndexOfUid)) {
						_tmpUid = null;
					} else {
						_tmpUid = _cursor.getString(_cursorIndexOfUid);
					}
					final String _tmpUserName;
					if (_cursor.isNull(_cursorIndexOfUserName)) {
						_tmpUserName = null;
					} else {
						_tmpUserName = _cursor.getString(_cursorIndexOfUserName);
					}
					final int _tmpAge;
					_tmpAge = _cursor.getInt(_cursorIndexOfAge);
					_item = new UserEntity(_tmpUid,_tmpUserName,_tmpAge);
					_result.add(_item);
				}
				return _result;
			} finally {
				_cursor.close();
			}
		}
	
		@Override
		protected void finalize() {
			_statement.release();
		}
	});
}

 

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public <T> LiveData<T> createLiveData(String[] tableNames, boolean inTransaction,
	Callable<T> computeFunction) {
	
//LiveData 를 상속받은 RoomTrackingLiveData 객체를 생성하여 반환해줍니다.
        return mInvalidationLiveDataContainer.create(
			validateAndResolveTableNames(tableNames), inTransaction, computeFunction);
}

 

//RoomTrackingLiveData 클래스

@SuppressLint("RestrictedApi")
RoomTrackingLiveData(
		RoomDatabase database,
		InvalidationLiveDataContainer container,
		boolean inTransaction,
		Callable<T> computeFunction,
		String[] tableNames) {
	mDatabase = database;
	mInTransaction = inTransaction;
	mComputeFunction = computeFunction;
	mContainer = container;
	
	mObserver = new InvalidationTracker.Observer(tableNames) {
		@Override
		public void onInvalidated(@NonNull Set<String> tables) {
			ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
		}
	};
}

@SuppressWarnings("WeakerAccess")
final Runnable mInvalidationRunnable = new Runnable() {
	@MainThread
	@Override
	public void run() {
		boolean isActive = hasActiveObservers();
		if (mInvalid.compareAndSet(false, true)) {
			if (isActive) {
				getQueryExecutor().execute(mRefreshRunnable);
			}
		}
	}
};

final Runnable mRefreshRunnable = new Runnable() {
	@WorkerThread
	@Override
	public void run() {
		if (mRegisteredObserver.compareAndSet(false, true)) {
			mDatabase.getInvalidationTracker().addWeakObserver(mObserver);
		}
		boolean computed;
		do {
			computed = false;
			// compute can happen only in 1 thread but no reason to lock others.
			if (mComputing.compareAndSet(false, true)) {
				// as long as it is invalid, keep computing.
				try {
					T value = null;
					while (mInvalid.compareAndSet(true, false)) {
						computed = true;
						try {
//public LiveData<List<UserEntity>> getAll() 메서드에서 등록한 Callable 객체의 call 메서드를 호출합니다.
							value = mComputeFunction.call();
						} catch (Exception e) {
							throw new RuntimeException("Exception while computing database"
								+ " live data.", e);
						}
					}
					if (computed) {
//call 메서드에서 가져온 변경된 데이터들을 postValue 를 통해 LiveData 에 바인딩하여 옵저버들에게 통지가 가도록 합니다.
						postValue(value);
					}
				} finally {
					// release compute lock
					mComputing.set(false);
				}
			}
                
		} while (computed && mInvalid.get());
	}
};