[개념 콕] 안드로이드 Content Provider

내일배움캠프 수료생이 개발에 꼭 필요한 핵심 개념만 콕 집어 드립니다.
Jun 07, 2024
[개념 콕] 안드로이드 Content Provider
✍🏼
안드로이드 입문자 여러분, 정보가 너무 많고 배워야 할 것도 산더미라 어디서부터 시작해야 할지 막막하신가요? 내일배움캠프 수료생들이 4개월 동안 배운 엄선된 안드로이드 핵심 개념을 직접 정리해서 알려 드립니다. 공부하다 막히거나 헷갈리는 개념이 있다면 개념 콕으로 정리해보세요.
 

ContentProvider란?

ContentProivider는 Activity, BroadcastReceiver, Service와 함께 안드로이드 애플리케이션을 구성하는 4대 구성요소 중 하나로 다른 애플리케이션의 데이터에 접근이 필요할 때 사용하게 되는 컴포넌트입니다.
각 앱은 하나의 프로세스로 실행되며 자신의 프로세스에서 사용하는 데이터는 자신만 접근할 수 있도록 설계되어 있습니다. 하지만 사진첩에 있는 사진들을 가져오거나 연락처에 있는 연락처 정보들을 가져오는 경우가 있는데요. 이 때 사진 앱에는 ContentProvider가 구현되어 있어 데이터를 다른 앱에서 사용할 수 있도록 통로를 제공합니다.
그리고 ContentProvider는 앱의 보안을 위해 생겨난 안드로이드 기본 구성 요소이기 때문에 안드로이드 시스템에서 관리하며 Manifest 파일에 명시해줘야 시스템에서 알 수 있습니다.
이미지 출처: Android
이미지 출처: Android
 
일반적으로 다음과 같이 두 가지 경우에서 주로 ContentProvider를 사용하게 됩니다.
  • 내 애플리케이션에서 다른 애플리케이션의 ContentProvider에 액세스 하기 위해 코드 구현
  • 내 애플리케이션에 ContentProvider를 생성하여 다른 애플리케이션과 데이터 공유
 
ContentProvider가 생성된 다른 애플리케이션의 데이터를 접근하기 위해서는 ContentResolver 객체를 사용하여 ContentProvider와 서버-클라이언트 구조로 통신을 주고 받아야 합니다. 즉, ContentResolver 객체가 ContentProvider에 데이터를 요청하게 되고 ContentProvider는 요청된 작업을 실행하고 결과를 반환하는 구조입니다.
이미지 출처: Android
이미지 출처: Android
ContentProvider에서 공유할 수 있는 데이터는 데이터베이스, 파일, SharedPreference 3가지가 있습니다. 하지만 일반적으로는 ContentProvider는 CRUD동작을 기본으로 하고 있기에 데이터베이스가 주로 사용됩니다.
❓ CRUD : Create(생성), Read(읽기), Update(수정), Delete(삭제)를 뜻하는, 데이터 처리의 4가지 기본 원칙 👉🏼 CRUD가 무엇인지 더 알고 싶다면?
 

ContentProvider 구현 방법

 
Manifest에 다음과 같이 등록을 해줍니다.
<provider android:authorities="com.example.contentprovider" android:name=".TestContentProvider" android:exported="true" </provider>
여기서 exported에 true값을 주는 이유는 안드로이드 4.2 이상 버전에서는 보안상의 이유로 ContentProvider를 공개하지 않는 것이 기본값이 되었기 때문입니다. 따라서 ContentProvider를 사용하기 위해서는 이 exported에 true를 줘야 합니다.
 
이제 본격적으로 ContentProvider를 구현해보겠습니다.

Content URI 설계

companion object{ const val AUTHORITY = "com.example.contentprovider" const val BASE_PATH = "person" val CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH) const val PERSONS = 1 const val PERSON_ID = 2 val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply { addURI(AUTHORITY, BASE_PATH, PERSONS) addURI(AUTHORITY, BASE_PATH + "/#", PERSON_ID) } }
ContentProvider를 만들기 위해서는 고유한 값을 가진 Content URI를 만들어야 합니다. Content URI는 Provider에서 데이터를 식별하는 URI로 Provider의 권한과 테이블 또는 파일을 가리키는 이름이 포함됩니다. 또한 ID 부분은 테이블 내 개별적인 행을 가리키게 됩니다. 이 Content URI가 ContentProvider의 모든 메소드에 필수 인자로 들어가고 이를 통해 액세스할 테이블, 행 또는 파일을 결정할 수 있습니다.
 
여기서 잠깐, URI 구조에 대해 알아보고 갑시다.
URI 구조: content://com.example.contentprovider/person/1
  • content:// : ContentProvider에 제어되는 데이터라는 의미로 항상 content://로 시작
  • Authority : com.example.contentprovider를 가리키며, ContentProvider를 구분하는 고유의 값으로 사용
  • Base Path : 테이블 또는 파일을 가리키는 이름으로 해당 URI에서는 person 테이블을 지칭
  • ID : 마지막 숫자로 테이블 내 행(레코드)을 지칭
 
 
아래는 데이터베이스의 CRUD에 해당하는 query(읽기), insert(추가), delete(삭제), update(수정)의 메소드를 구현한 코드입니다.
 

query 메소드 구현

override fun query( uri: Uri, projcetion: Array<out String>?, selection: String?, selctionArgs: Array<out String>?, sortOrder: String? ): Cursor? { var cursor: Cursor? = null when(uriMatcher.match(uri)){ PERSONS -> { cursor = database.query(TABLE_NAME, projcetion, selection, selctionArgs, null, null, sortOrder) } else -> throw IllegalArgumentException("알 수 없는 URI : $uri") } cursor.setNotificationUri(context?.contentResolver, uri) return cursor }
 
 

insert 메소드 구현

override fun insert(uri: Uri, values: ContentValues?): Uri? { val id = database.insert(TABLE_NAME, null, values) if(id > 0){ val uri = ContentUris.withAppendedId(CONTENT_URI, id) context?.contentResolver?.notifyChange(uri, null) return uri } throw SQLException("추가 실패 URI : $uri") }
 

delete 메소드 구현

override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int { var count = 0 when(uriMatcher.match(uri)){ PERSONS -> count = database.delete(TABLE_NAME, selection, selectionArgs) else -> throw IllegalArgumentException("알 수 없는 URI : $uri") } context?.contentResolver?.notifyChange(uri, null) return count }
 

update 메소드 구현

override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int { var count = 0 when(uriMatcher.match(uri)){ PERSONS -> count = database.update(TABLE_NAME, values, selection, selectionArgs) else -> throw IllegalArgumentException("알 수 없는 URI : $uri") } context?.contentResolver?.notifyChange(uri, null) return count }
 
만든 김에 한 가지만 더 만들어봅시다.
 

getType 메소드 구현

override fun getType(uri: Uri): String? { when(uriMatcher.match(uri)){ PERSONS -> return "vnd.android.cursor.dir/persons" else -> throw IllegalArgumentException("알 수 없는 URI : $uri") } }
getType 메소드는 Content Uri이 반환하는 데이터 유형(MIME)을 알고 싶을 때 사용하는 메소드입니다. 텍스트, HTML 또는 JPEG과 같은 보편적인 유형의 데이터라면 getType이 해당 데이터에 대한 표준 MIME 유형을 반환해야 하고 데이터 테이블 또는 테이블의 행을 가리킨다면 Android 공급업체별 MIME 형식으로 반환해야 합니다. 반환되는 유형별 MIME 형식은 공식문서를 참고하시면 됩니다.
 
이렇게 만들어진 전체 코드는 다음과 같습니다.
class PersonProvider : ContentProvider() { private lateinit var database : SQLiteDatabase override fun onCreate(): Boolean { if(context == null) return false database = DatabaseHelper.getInstance(context!!).writableDatabase return true } override fun query( uri: Uri, projcetion: Array<out String>?, selection: String?, selctionArgs: Array<out String>?, sortOrder: String? ): Cursor? { var cursor: Cursor? = null when(uriMatcher.match(uri)){ PERSONS -> { cursor = database.query(TABLE_NAME, projcetion, selection, selctionArgs, null, null, sortOrder) } else -> throw IllegalArgumentException("알 수 없는 URI : $uri") } cursor.setNotificationUri(context?.contentResolver, uri) return cursor } override fun getType(uri: Uri): String? { when(uriMatcher.match(uri)){ PERSONS -> return "vnd.android.cursor.dir/persons" else -> throw IllegalArgumentException("알 수 없는 URI : $uri") } } override fun insert(uri: Uri, values: ContentValues?): Uri? { val id = database.insert(TABLE_NAME, null, values) if(id > 0){ val uri = ContentUris.withAppendedId(CONTENT_URI, id) context?.contentResolver?.notifyChange(uri, null) return uri } throw SQLException("추가 실패 URI : $uri") } override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int { var count = 0 when(uriMatcher.match(uri)){ PERSONS -> count = database.delete(TABLE_NAME, selection, selectionArgs) else -> throw IllegalArgumentException("알 수 없는 URI : $uri") } context?.contentResolver?.notifyChange(uri, null) return count } override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int { var count = 0 when(uriMatcher.match(uri)){ PERSONS -> count = database.update(TABLE_NAME, values, selection, selectionArgs) else -> throw IllegalArgumentException("알 수 없는 URI : $uri") } context?.contentResolver?.notifyChange(uri, null) return count } companion object{ const val AUTHORITY = "com.example.contentprovider" const val BASE_PATH = "person" val CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH) const val PERSONS = 1 const val PERSON_ID = 2 val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply { addURI(AUTHORITY, BASE_PATH, PERSONS) addURI(AUTHORITY, BASE_PATH + "/#", PERSON_ID) } } }
 
사실 ContentProvider라는 개념은 데이터베이스에 대한 이해가 우선적으로 있어야 하기 때문에, 데이터베이스를 배운 전공자 분들이 아니고서는 제대로 이해하기 어려운 부분이 많습니다. 만약 이 내용이 어렵다면 “아, 이게 애플리케이션을 구성하는 4대 구성요소 중 하나구나” 정도로 이해해주세요.
 
 

내일배움캠프는 개발에 필요한 핵심만 배웁니다

지금까지 꼭 필요한 안드로이드 지식에 대해 알아보았습니다. 내일배움캠프에서는 전문가들이 선별한 핵심 안드로이드 개발 지식으로 개발 공부도, 취업도 보다 효율적으로 할 수 있는데요. 국내 유수의 IT기업 출신 튜터님들과 실습 위주의 독보적인 커리큘럼으로 개발자 취업을 체계적으로 준비해보세요. 내일배움캠프 4개월, 여러분 인생의 가장 큰 터닝 포인트입니다.
 
 
참고 문헌
 
 
CREDIT
글 | 김기원 내일배움캠프 수료생 편집 | 정효재 팀스파르타 에디터
Share article
Subscribe to our newsletter

내일배움캠프 블로그