이 편에 이어 이번엔 콘텐트 프로바이더에 대해 이야기해보자
콘텐트 프로바이더는 여러 앱 간에 데이터를 공유할 필요가 있을 때 사용한다.
데이터 소스가 굳이 DB일 필요는 없다, 파일이든 네트워크를 통해서 가져오는 값이든 상관없이 모두 콘텐트 프로바이더의 데이터 소스가 될 수 있다. 다만, API가 DB를 데이터 소스로 사용하는 것에 더 맞게 디자인되어 있다.
ContentProvider에 접근하는 것은 ContentResolver를 통해서만 가능하다
ContentResolver은 Context의 getContentResolver() 메서드로 구할 수 있다 (코틀린이라면 contentResolver면 충분)
ContentResolver은 일종의 프락시인데, 해당하는 Uri의 ContentProvider를 찾는 역할을 한다.
즉, Resolver이 추상 클래스고 실제 구현체는 ContextImpl
의 내부 클래스인 ApplicationContentResolver
이다.
개발자 가이드에서는 로컬 프로세스에서만 데이터가 쓰인다면 콘텐트 프로바이더를 사용하지 말 것을 권장한다.
그럼에도 실제로 쓸 때 고민이 되는 경우가 많은데, 이의 장단점을 살피고 결론을 알아보자
ContentProvider의 메서드 시그니처를 따라야 하므로 여러 DB를 사용하더라도 API의 일관성을 유지할 수 있다. 이런 일관성은 다른 프로젝트에 적응할 때도 장점이 될 수 있다.
CursorLoader, AsyncQueryHandler와 같은 클래스들이 콘텐트 프로바이더의 Uri가 전달되어야만 동작한다.
1개의 앱에서도 프로세스가 분리될 수 있는 경우에 장점이 된다.
예를 들어, 서비스에서 메모리 사용이 많아서 서비스를 프로세스로 분리하고 각 프로세스에서 DB 헬퍼를 사용한다고 가정하자. 각 프로세스에서 DB 헬퍼를 싱글톤으로 만들어도 프로세스마다 1개씩 있기 때문에 DB 락 문제가 언제든 발생할 수 있다. 이 때 앱 프로세스에 콘텐트 프로바이더를 두고, 서비스 프로세스에서 ContentResolver를 통해 콘텐트 프로바이더에 접근하면 유일한 DB 헬퍼를 유지할 수 있다.
위 예시가 지금은 이해가 가지 않아도 추후에 설명을 보면 더 이해가 잘 간다.
DB에 직접 접근하는 것에 비해 코드가 복잡하다
프락시인 ContentResolver를 거쳐야 하기 때문에 직접 DB를 접근하는 것에 비해서 속도가 느리다.
groupBy, having, limit 같은 파라미터를 ContentResolver의 메서드에 전달 할 수 없다. 필요한 경우 Uri나 쿼리 파라미터에 억지로 끼워넣어서 전달해야 한다.
ContentResolver를 통하므로 별도의 공개 메서드를 만들어도 접근할 수 없다.
로컬 프로세스에서 콘텐트 프로바이더는 꼭 필요할 때만 쓰는게 좋다.
DB에 직접 접근하는 코드에서도 메서드 시그너처를 ContentProvider
와 유사하게 만들면, 이후에 수월하게 변경할 수 있다.
프로세스가 분리되거나 다른 앱에서 DB에 접근해야 한다면, 필요한 부분만 콘텐트 프로바이더를 제공해서 DB 접근(내부용) + 콘텐트 프로바이더(외부용)
로 구성하는 것도 가능한 방법이다.
락 문제가 방지되긴 하는데 이는 콘텐트 프로바이더를 썼기 때문이 아니라 콘텐트 프로바이더를 만드는 일반적인 패턴때문이다.
public class ExampleProvider extends ContentProvider {
static class DatabaseHelper extends SQLiteOpenHelper {
...
@Override
public boolean onCreate() {
mOpenHelper = new DatabaseHelper(getContent());
return true;
}
....
}
}
위 코드에서 보면 onCreate()에서 DatabaseHelper를 하나 생성해놓고 이것을 사용하고 있다. onCreate()는 처음 ContentProvider를 사용할 때 단 한 번만 실행된다.
ContentProvider는 단말에서 오직 하나만 존재하기 때문에 DatabaseHelper도 하나뿐이다. 따라서 내부적으로 명령어가 직렬화(serialized)되면서 DB 락 문제가 없는 것이다.
ContentProvider의 onCreate() 메서드는 Application의 onCreate() 메서드 이전에 실행된다.
따라서 Application의 onCreate()가 먼저 실행된다고 가정하면 안된다. Application에서 생성한 인스턴스를 ContentProvider의 onCreate()에서도 쓰고 싶겠지만 순서가 맞지 않다.
ContentProvider의 onCreate()는 메인 스레드에서 실행하고 다른 메서드는 일반적으로 별도 스레드에서 실행하므로, ContentProvider의 메서드 간에는 스레드 안정성에 주의해야한다. 즉, 멤버 변수를 함부로 쓰면 안된다.
로컬에서도 ContentProvider는 백그라운드 스레드에서 호출하도록 권장되고, 외부 프로세스에서의 접근은 바이더 스레드를 거쳐 실행한다
DB 명령 실행 후에 getContent().getContentResolver().notifyChange(uri, null)
을 호출한다.
ContentResolver에는 registerContentObserver() 메서드가 있어서 데이터가 변경되면 알 수 있게 ContentObserver를 등록할 수 있다.
그러므로 notifyChange는 ContentObserver에 알리는 것이다. 이 옵저버에는 변경된 데이터가 어떤 데이터인지는 알리지 않고 변경되었다는 것만 알린다.
또한, 데이터 변경 콜백을 받으면 다시 조회하는 로직을 주로 사용한다. 다시 조회할 때는 또다시 쿼리 명령을 전달할 필요가 없다.
query() 메서드의 리턴 결과인 Curesor에는 requery()
가 있어서 새로 조회할 수 있다. CursorAdapter에서는 내부에 콘텐트 옵저버가 등록되어 있어서 notifyChange()가 불리면 requeyr()가 실행된다.
ContentProvider는 여러 명령어를 한꺼번에 실행할 수 있는 applyBatch()
를 제공한다.
근데 이 메서드를 보면 속도 향상을 위해 트랜잭션을 쓰는 걸로 혼동할 수 있지만 실제로는 ContentProviderOperation
목록을 한꺼번에 전송하고 순차 실행하는 것에 지나지 않는다.
물론 콘텐트 프로바이더가 다른 프로세스에서 실행된다면 바인더를 거쳐야해서 하나씩 명령어를 주고받는 것보다 한꺼번에 보내는게 실행 속도 면에서 훨 낫다.
하지만 트랜잭션을 쓰는 것처럼 속도향상이 큰 것은 아니다.
트랜잭션을 쓰고 싶다면 ContentProvider의 applyBatch를 오버라이드하는 방법을 쓸 수는 있다
@Override
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) {
SQLiteDatabse db = mOpenHelper.getWritableDatabase();
db.beginTrasactoin();
try {
ContentProviderResult[] result = super.applyBatch(operations);
db.setTrasactionSuccessful();
return result;
} finally {
db.endTrasaction();
}
}