(원문: http://developer.android.com/guide/topics/providers/content-provider-basics.html)

(위치: Develop > API Guides > App Components > Content Providers 
> Content Provider Basics)

(전체 번역 글 목록으로 이동하기)


컨텐트 프로바이더 기초
(Content Provider Basics)


컨텐트 프로바이더는 데이터 저장소에 접근하는 방법을 제공합니다. 프로바이더는 저장된 데이터에 기반하여 서비스를 제공하는 앱의 일부이며, 주로 다른 앱에 의해 사용됩니다. 다른 앱은 프로바이더 클라이언트 객체를 이용하여 프로바이더 앱에 액세스할 수 있습니다. 프로바이더와 프로바이더 클라이언트는 일관되고 표준적인 인터페이스를 제공하는데, 이것은 내부적으로 프로세스간 통신(IPC)과 데이터 보안(secure data access) 관련 기능을 구현하고 있습니다. 

본 문서에서 다룰 내용은 대략 아래와 같습니다:

  • 컨텐트 프로바이더는 어떻게 동작하는가.
  • 컨텐트 프로바이더로부터 데이터를 가져오는 API.
  • 컨텐트 프로바이더에 데이터를 삽입, 갱신, 삭제하는 API.
  • 그 외의 유용한 API들


개요

컨텐트 프로바이더는 다른 앱에 데이터를 제공하는데, 그 데이터는 관계형 데이터베이스에서의 테이블들과 유사하게 하나 또는 그 이상의 테이블로써 제공됩니다. 하나의 행(row)은 프로바이더가 제공하는 어떤 데이터 타입에 대한 객체를 나타내고, 그 행의 각 열(column)은 해당 객체가 가지고 있는 각 데이터를 나타냅니다. 

예를 들어, 안드로이드 플랫폼에 내장된 프로바이더 중에 사용자 딕셔너리가 있는데, 이것은 사용자가 저장하고 싶어하는 비표준의(non-standard) 단어들을 저장합니다. 아래의 표1은 이 프로바이더의 테이블에 데이터들이 어떤 식으로 저장되는지를 보여줍니다:

표 1: 사용자 딕셔너리 테이블 샘플

 단어(word)

 앱ID
(app id)

 빈도
(frequency)

 지역(locale)

 _ID

 mapreduce

 user1

 100

 en_US

 1

 precompiler

 user14

 200

 fr_FR

 2

 applet

 user2

 225

 fr_CA

 3

 const

 user1

 255

 pt_BR

 4

 int

 user5

 100

 en_UK

 5

위의 표1에서, 각 행(row)은 표준 딕셔너리에 없을법한 단어에 대한 정보를 나타냅니다. 그리고 각 열(column)은 처음 입력됐을때의 지역정보와 같은, 해당 단어에 대한 정보를 나타냅니다. 각 열의 헤더는 프로바이더에 저장된, 열의 이름입니다. 어떤 행의 지역정보를 얻기 위해서는, 그 행의 locale 값을 찾으면 됩니다. 그리고 _ID는 프로바이더에서 자동으로 생성하는 "기본키(primary key)"를 의미합니다.

메모: 프로바이더에서 기본키인 _ID를 반드시 가지고 있어야 하는 것은 아닙니다. 하지만 프로바이더를 통해 데이터들을 ListView에 바인드해야 한다면, _ID를 가질 필요가 있습니다. 이와 관련해서는 쿼리 결과 보여주기 섹션에서 자세히 설명하겠습니다. 


프로바이더에 액세스하기

앱은 컨텐트 프로바이더로부터 데이터를 얻기 위해 컨텐트 리졸버(ContentResolver) 객체를 사용합니다. 이 객체는 ContentProvider클래스의 서브클래스에 대한 객체의 메소드들과 이름이 같은 메소드들을 갖고 있습니다. 그 메소드들에는 저장소에 대한 "CRUD" (create, read, update, delete) 함수들이 포함됩니다. 

클라이언트 앱에 있는 ContentResolver 객체와 데이터를 제공하는 앱에 있는 ContentProvider 객체는 프로세스간 통신(IPC)을 합니다. 그리고 ContentProvider는 실제 데이터 저장소와 외부에 드러난 데이터 테이블 사이에서 추상 레이어 역할도 합니다. 

메모: 프로바이더에 액세스하기 위해서는, 보통 내앱의 매니페스트 파일에 해당 권한들이 선언되어 있어야 합니다. 자세한 내용은 아래의 컨텐트 프로바이더 관련 권한들 섹션에서 학습하실 수 있습니다.

예를 들어, 사용자 사전 프로바이더에 있는 단어 및 장소 목록을 얻기 위해서는 ContentResolver.query() 메소드를 호출하는데, 이것은 사용자 사전 프로바이더에 정의되어 있는 ContentProvider.query() 메소드를 호출합니다. 아래 예제 코드는 ContentResolver.query() 메소드를 호출하는 것을 보여줍니다:

// Queries the user dictionary and returns results
mCursor
= getContentResolver().query(
   
UserDictionary.Words.CONTENT_URI,   // The content URI of the words table
    mProjection
,                        // The columns to return for each row
    mSelectionClause                    
// Selection criteria
    mSelectionArgs
,                     // Selection criteria
    mSortOrder
);                        // The sort order for the returned rows

아래의 표2는 query(Uri, projection, selection, selectionArgs, sortOrder) 메소드의 각 인자가 SQL의 SELECT 구문과 어떻게 대응되는지를 보여줍니다.

표2: query() 메소드와 SQL 쿼리의 대응관계

 query()의 인자

 SELECT의 구문

 비고

 Uri

 FROM table_name

 Uri는 프로바이더의 table_name 테이블과 대응됩니다.

 projection

 col,col,col,...

 projection은 select하여 가져올 데이터의 컬럼들과 대응됩니다.

 selection

 WHERE col = value

 selection은 where 조건절과 대응됩니다.

 selectionArgs

 where절의 value가 ?일때, ?를 대치하는 값. SQL 바인딩 참고

 

 sortOrder

 ORDER BY col,col,...

 sortOrder는 정렬 기준으로서 order by 절과 대응됩니다.


컨텐트 URI

컨텐트 URI는 프로바이더의 데이터를 가리키는 URI입니다. 컨텐트 URI는 프로바이더 자체를 나타내는 이름(uri의 authority부분. scheme 뒷부분)과 그 안의 테이블을 가리키는 이름(uri의 path부분. authority 뒷부분)을 포함하고 있습니다. 프로바이더의 테이블에 액세스하기 위해 클라이언트용 메소드를 호출할 때, 그 테이블에 대한 컨텐트 URI는 그 메소드의 인자 중 하나가 됩니다. 

위의 예제 코드에서, CONTENT_URI는 사용자 사전의 "words" 테이블을 가리키는 URI입니다. ContentResolver 객체는 그 URI의 authority부분을, 시스템이 알고 있는 프로바이더들의 authority들과 비교하여 맞는 것을 찾아서, 그 프로바이더로 쿼리 인자들을 보냅니다. 

ContentProvider는 액세스할 테이블을 알기 위해 URI의 path부분을 봅니다. 프로바이더는 보통 그 path부분과 일치하는 이름의 테이블을 가지고 있습니다.

위의 예제 코드에서 "words" 테이블에 해당하는 전체 URI는 아래와 같습니다:

content://user_dictionary/words

위에서, user_dictionary가 있는 부분이 URI의 authority부분이며 프로바이더를 가리키고, words가 있는 부분이 URI의 path부분이며 테이블을 가리킵니다. 그리고 content://가 있는 부분은 URI의 scheme부분이며 이 문자열이 컨텐트 URI임을 알려주는 것입니다. 

대부분의 프로바이더들은 URI의 끝에 ID값을 넣음으로써 해당 테이블의 ID에 해당하는 데이터에 액세스하는 것을 허용합니다. 예를 들어, 사용자 사전에서 _ID가 4인 데이터를 얻기 위해 사용하는 컨텐트 URI는 아래와 같이 구할 수 있습니다:

Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);

원하는 데이터들을 얻거나, 업데이트하거나, 삭제할 때에도 ID값을 사용할 수 있습니다. 

메모: UriUri.Builder 클래스는 문자열로부터 Uri 객체를 만들어내는 편리한 메소드들을 가지고 있습니다. ContentUris 클래스는 URI에 ID값을 추가해주는 메소드를 가지고 있습니다. 위의 코드는 사용자 사전의 컨텐트 URI에 ID값인 4를 추가하기 위해 withAppendedId() 메소드를 사용하는 것을 보여줍니다.


프로바이더로부터 데이터 가져오기

이번 섹션에서는 사용자 사전 프로바이더를 예로 들어 프로바이더에서 데이터를 얻는 방법을 설명합니다. 

설명을 더 명확하게 하기 위해서 이번 섹션의 예제 코드들은 ContentResolver.query() 메소드를 "UI 쓰레드"에서 호출합니다. 하지만 실제 코드에서는 쿼리들이 별도의 쓰레드에서 비동기적으로 처리되도록 해야합니다. 그러기 위한 한가지 방법으로 CursorLoader 클래스를 사용할 수 있으며, 그에 대한 자세한 내용은 로더 문서에서 학습하실 수 있습니다. 또한, 예제 코드들은 전체 코드의 일부분일 뿐이며 앱 전체의 코드를 보여주지 않습니다.

프로바이더에서 데이터를 얻기 위해서는, 아래의 기본적인 절차를 따르면 됩니다.

1. 프로바이더에 대한 읽기 권한을 요청합니다.

2. 프로바이더에 쿼리를 보내는 코드를 작성합니다.


읽기 권한 요청하기

프로바이더로부터 데이터를 얻기 위해서는, 내 앱에 프로바이더에 대한 "읽기 권한(read access permission)"이 있어야 합니다. 이 권한에 대한 요청은 런타임에 할 수 없고, 매니페스트 파일에 <uses-permission> 요소를 추가하여 여기에 프로바이더가 정의한 정확한 권한 이름을 지정해줘야 합니다. 매니페스트 파일에 <uses-permission> 요소를 추가하면, 내 앱이 해당 권한을 "요청하는" 것과 같습니다. 이 경우 사용자는 앱을 설치하기 전에 권한 요청에 대한 사실을 확인할 수 있고, 앱을 설치한다면 해당 요청을 암묵적으로 승인한 것과 같다고 할 수 있습니다. 

프로바이더에 대한 읽기 권한의 정확한 이름을 찾기 위해서는 프로바이더 문서를 살펴봐야 하며, 이는 다른 권한들에 대해서도 마찬가지입니다. 

프로바이더에 액세스 하기 위한 권한의 역할에 대하여, 더 자세한 내용은 컨텐트 프로바이더의 권한들 섹션에서 학습하실 수 있습니다. 

사용자 사전 프로바이더는 자신의 매니페스트 파일에 android.permission.READ_USER_DICTIONARY 권한을 정의하고 있으며, 사용자 사전 프로바이더의 데이터를 읽기 원하는 앱은 이 권한을 요청해야 합니다. 


쿼리 만들기

프로바이더에서 데이터를 읽기 위해 다음으로 할 일은 쿼리를 만드는 것입니다. 아래 예제 코드에서는, 사용자 사전 프로바이더에 액세스 하기 위한 몇가지 변수들을 정의하고 있습니다:

// A "projection" defines the columns that will be returned for each row
String[] mProjection =
{
   
UserDictionary.Words._ID,    // Contract class constant for the _ID column name
   
UserDictionary.Words.WORD,   // Contract class constant for the word column name
   
UserDictionary.Words.LOCALE  // Contract class constant for the locale column name
};

// Defines a string to contain the selection clause
String mSelectionClause = null;

// Initializes an array to contain selection arguments
String[] mSelectionArgs = {""};

아래의 예제에서는 사용자 사전 프로바이더를 이용하여 ContentResolver.query()를 사용하는 방법을 보여줍니다. 프로바이더에 요청되는 쿼리는 SQL 쿼리와 거의 비슷하며, 리턴 받을 컬럼들(projection)과 조건절에 들어갈 값들(selection criteria), 그리고 정렬 기준들(sort order)을 포함합니다. 

조건절에 대한 표현은 selection 구문(clause)과 selection 인자들(arguments)로 분리될 수 있습니다. selection 구문은 논리적 표현(and, or 등), 참/거짓 표현, 컬럼 이름과 컬럼 값의 조합으로 구성됩니다(위 예제에서는 mSelectionClause 변수). 만약 selection 구문에 실제 값 대신에 물음표(?)가 들어가 있다면, selection 인자들에서 대응되는 실제 값을 가져오게 됩니다(위 예제에서는 mSelectionArgs). 

아래의 예제에서, 만약 사용자가 입력한 단어가 없다면, selection 구문은 null이 되고 프로바이더에 있는 모든 단어들이 리턴됩니다. 하지만 사용자가 입력한 단어가 있다면, selection 구문은 UserDictionary.Words.WORD + " = ?" 가 되고 selection 인자에는 사용자가 입력한 단어가 들어갑니다.

/*
 * This defines a one-element String array to contain the selection argument.
 */

String[] mSelectionArgs = {""};

// Gets a word from the UI
mSearchString
= mSearchWord.getText().toString();

// Remember to insert code here to check for invalid or malicious input.

// If the word is the empty string, gets everything
if (TextUtils.isEmpty(mSearchString)) {
   
// Setting the selection clause to null will return all words
    mSelectionClause
= null;
    mSelectionArgs
[0] = "";

} else {
   
// Constructs a selection clause that matches the word that the user entered.
    mSelectionClause
= UserDictionary.Words.WORD + " = ?";

   
// Moves the user's input string to the selection arguments.
    mSelectionArgs
[0] = mSearchString;

}

// Does a query against the table and returns a Cursor object
mCursor
= getContentResolver().query(
   
UserDictionary.Words.CONTENT_URI,  // The content URI of the words table
    mProjection
,                       // The columns to return for each row
    mSelectionClause                  
// Either null, or the word the user entered
    mSelectionArgs
,                    // Either empty, or the string the user entered
    mSortOrder
);                       // The sort order for the returned rows

// Some providers return null if an error occurs, others throw an exception
if (null == mCursor) {
   
/*
     * Insert code here to handle the error. Be sure not to use the cursor! You may want to
     * call android.util.Log.e() to log this error.
     *
     */

// If the Cursor is empty, the provider found no matches
} else if (mCursor.getCount() < 1) {

   
/*
     * Insert code here to notify the user that the search was unsuccessful. This isn't necessarily
     * an error. You may want to offer the user the option to insert a new row, or re-type the
     * search term.
     */


} else {
   
// Insert code here to do something with the results

}

위의 예제는 아래 SQL 구문을 실행하는 것과 비슷합니다.

SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;

예제 코드와 SQL 구문의 차이점 중 하나로, SQL 구문에서는 해당 클래스의 상수 이름(클래스에 정의된 이름) 대신에 실제 words 테이블의 컬럼 이름이 들어갑니다. 


악의적인 입력으로부터 보호하기

컨텐트 프로바이더에 의해 관리되는 데이터가 SQL 데이터베이스에 있고, SQL 구문이 외부의 신뢰할 수 없는 데이터를 포함하게 된다면, 이는 SQL 인젝션 공격을 받을 수 있습니다. 

아래 selection 구문을 예로 들어 보겠습니다:

// Constructs a selection clause by concatenating the user's input to the column name
String mSelectionClause =  "var = " + mUserInput;

이와 같이 하면, 사용자가 SQL 구문에 악의적인 SQL을 추가할 수 있게 됩니다. 예를 들면, 사용자가 입력한 mUserInput"nothing; DROP TABLE *;" 일 경우, selection 구문은 var = nothing; DROP TABLE *; 이 됩니다. selection 구문이 SQL 구문에 들어가기 때문에, 이 SQL 구문으로 인해 프로바이더가 (SQL 인젝션 공격에 대한 대비를 하고 있지 않다면) SQLite 데이터베이스의 모든 테이블들을 지워버릴지도 모릅니다. 

이 문제를 피하기 위해서는, selection 구문에 실제 값으로 대치될 수 있는 물음표(?)를 사용하고, selection 인자들을 따로 배열로 입력하면 됩니다. 이렇게 하면, 사용자가 입력한 내용이 SQL 구문으로 바로 인식되지 않고 데이터로서 쿼리에 바인드 됩니다. 그러면 사용자가 입력한 내용이 요구되는 데이터 타입에 부합하는지 확인되기 때문에, 악의적인 목적으로 SQL 구문을 변형할 수 없습니다. 따라서 사용자가 입력한 내용을 바로 SQL 구문에 포함시키는 대신에, 아래와 같이 분리하는 것이 좋습니다.

selection 구문: 

// Constructs a selection clause with a replaceable parameter
String mSelectionClause =  "var = ?";

selection 인자들:

// Defines an array to contain the selection arguments
String[] selectionArgs = {""};

selection 인자들은 아래와 같이 값을 추가합니다:

// Sets the selection argument to the user's input
selectionArgs
[0] = mUserInput;

물음표(?)를 사용한 selection 구문과 selection 인자들을 배열로 입력하는 방식은 상당히 권장되는 방식이며, 심지어 프로바이더가 SQL 데이터베이스에 기반하는 경우가 아니더라도 마찬가지입니다. 


쿼리 결과 보여주기

ContentResolver.query() 메소드는 쿼리의 selection 조건에 부합하는 결과들에 대하여 쿼리의 projection에서 지정한 컬럼 값들을 담고있는 Cursor 객체를 리턴해 주며, Cursor 객체를 통해 결과값들에 액세스할 수 있습니다. Cursor의 메소드들을 이용하여, 여러 행의 결과들에 대하여 순차적으로 각 행을 읽을 수 있고, 각 컬럼의 데이터 타입을 확인하여, 그 컬럼들의 데이터를 얻을 수 있습니다. 몇몇 Cursor 구현 객체들은, 프로바이더의 데이터가 변경되거나, Cursor 객체가 변경될 때 옵저버 객체들의 메소드가 호출되도록 트리거가 걸려있거나, 또는 둘 다일때 해당 Cursor 객체가 자동적으로 업데이트 되도록 구현되어 있습니다. 

메모: 프로바이더는 쿼리를 만드는 객체의 특성에 기반하여 특정 컬럼에 대한 액세스를 제한할 수도 있습니다. 예를 들어, 주소록(Contacts) 프로바이더도 몇가지 컬럼들에 대한 액세스를 제한하고 있어서 그 컬럼들의 값들은 리턴해 주지 않습니다.

만약 selection 조건에 부합하는 결과가 없다면, 프로바이더가 리턴해준 Cursor의 Cursor.getCount()는 0이 됩니다. 

만약 에러가 발생한다면, 쿼리에 대한 결과는 해당 프로바이더가 어떻게 처리하느냐에 달려 있습니다. 이러한 경우 보통은 null을 리턴하거나 Exception을 발생시킵니다. 

Cursor 객체는 결과 행(row)들의 "리스트"이기 때문에, 그 결과들을 보여주기 위해서는 ListView에 SimpleCursorAdapter를 연결하여 사용하는 것이 좋은 방법 중 하나입니다.

다음의 예제 코드는 이전 예제 코드에 이어지는 내용입니다. 여기에서, 쿼리 결과로 얻은 Cursor 객체를 포함하는 SimpleCursorAdapter 객체를 만들고, 이것을 ListView에 연결하는 것을 보여줍니다:

// Defines a list of columns to retrieve from the Cursor and load into an output row
String[] mWordListColumns =
{
   
UserDictionary.Words.WORD,   // Contract class constant containing the word column name
   
UserDictionary.Words.LOCALE  // Contract class constant containing the locale column name
};

// Defines a list of View IDs that will receive the Cursor columns for each row
int[] mWordListItems = { R.id.dictWord, R.id.locale};

// Creates a new SimpleCursorAdapter
mCursorAdapter
= new SimpleCursorAdapter(
    getApplicationContext
(),               // The application's Context object
    R
.layout.wordlistrow,                  // A layout in XML for one row in the ListView
    mCursor
,                               // The result from the query
    mWordListColumns
,                      // A string array of column names in the cursor
    mWordListItems
,                        // An integer array of view IDs in the row layout
   
0);                                    // Flags (usually none are needed)

// Sets the adapter for the ListView
mWordList
.setAdapter(mCursorAdapter);

메모: (SimpleCursorAdapter에서) Cursor를 이용하여 ListView를 보여주기 위해서는, Cursor가 _ID 컬럼을 가지고 있어야 합니다. 이 때문에 실제로 ListView에서 _ID의 값을 사용하지 않음에도 불구하고, 위의 예제에서 쿼리를 만들때 projection에 _ID가 포함되어 있는 것입니다. 그리고 이 때문에 대부분의 프로바이더들이 관리하는 테이블들은 _ID 컬럼을 가지고 있습니다. 


쿼리 결과에서 데이터 구하기

쿼리 결과를 단순하게 보여주는데에 그치지 않고, 다른 작업들을 할 수 있습니다. 예를 들어, 사용자 사전에 있는 단어들의 스펠링을 확인하여 다른 프로바이더에 필요한 정보를 요청할 수도 있는 것입니다. 그러기 위해서는 아래 예제 코드에서 보이는 바와 같이 Cursor의 결과들을 순차적으로 읽을 수 있어야 합니다:

// Determine the column index of the column named "word"
int index = mCursor.getColumnIndex(UserDictionary.Words.WORD);

/*
 * Only executes if the cursor is valid. The User Dictionary Provider returns null if
 * an internal error occurs. Other providers may throw an Exception instead of returning null.
 */


if (mCursor != null) {
   
/*
     * Moves to the next row in the cursor. Before the first movement in the cursor, the
     * "row pointer" is -1, and if you try to retrieve data at that position you will get an
     * exception.
     */

   
while (mCursor.moveToNext()) {

       
// Gets the value from the column.
        newWord
= mCursor.getString(index);

       
// Insert code here to process the retrieved word.

       
...

       
// end of while loop
   
}
} else {

   
// Insert code here to report an error if the cursor is null or the provider threw an exception.
}

Cursor 구현 객체는 여러 타입의 결과값들을 얻을 수 있도록 하기 위하여 몇개의 "get" 메소드를 가지고 있습니다. 위의 코드에 있는 getString() 메소드도 그 중 하나입니다. Cursor 객체는 getType() 메소드도 가지고 있는데, 이것은 해당 컬럼 값의 데이터 타입이 무엇인지를 리턴해 줍니다. 


컨텐트 프로바이더 관련 권한들

프로바이더 역할을 하는 앱은, 다른 앱이 데이터에 액세스하기 위해 꼭 가지고 있어야 하는 권한(permission)들을 지정해 둘 수 있습니다. 이러한 권한들은, 앱이 무슨 데이터에 액세스하려고 하는지 사용자가 알도록 해줍니다. 프로바이더의 요구사항에 기반하여, 다른 앱들은 필요한 권한들을 매니페스트 파일에 추가해야하며, 이것들은 사용자들이 앱을 설치할 때 확인할 수 있는 것입니다. 

만약 프로바이더 앱이 아무 권한도 지정하지 않았다면, 다른 앱들은 프로바이더의 데이터에 액세스할 수 없습니다. 하지만 프로바이더 앱 내의 컴포넌트들은 지정된 권한이 없더라도 항상 읽기와 쓰기 권한을 가지고 있습니다. 

위에서 언급했듯이, 사용자 사전 프로바이더에서 데이터를 읽기 위해서는 android.permission.READ_USER_DICTIONARY 권한이 필요합니다. 그리고 데이터를 삽입, 갱신, 삭제하기 위해서는 android.permission.WRITE_USER_DICTIONARY 권한이 필요합니다. 

클라이언트 앱이 프로바이더에 액세스하기 위한 권한을 얻기 위해서는, 매니페스트 파일에 <uses-permission> 요소를 추가함으로써 권한을 요청해야 합니다. 그러면 안드로이드 패키지 매니저가 앱을 설치할 때 사용자에게 그 앱이 요청하고 있는 권한들을 보여주고 승인하도록 합니다. 여기서 사용자가 그것을 승인하면 패키지 매니저는 설치를 계속하게 되고, 승인하지 않으면 설치를 중단합니다. 

아래의 <user-permission>은 사용자 사전 프로바이더에 읽기 권한을 요청하는 코드입니다.

<uses-permission android:name="android.permission.READ_USER_DICTIONARY">


데이터의 삽입, 갱신, 삭제

프로바이더로부터 데이터를 얻을 때와 마찬가지 방식으로, 클라이언트가 ContentProvider의 데이터를 변경할 수도 있습니다. ContentProvider에 대응되는 ContentResolver의 메소드를 호출하면 됩니다. 프로바이더와 클라이언트는 보안적으로 안전하게 연결되며, 프로세스간 통신(IPC, inter-process communication)을 합니다. 


데이터 삽입(inserting)

프로바이더에 데이터를 삽입하기 위해서는, ContentResolver.insert() 메소드를 호출합니다. 이 메소드는 새 데이터(new row)를 삽입하고, 삽입된 데이터에 대한 컨텐트 URI를 리턴 받습니다. 아래 예제 코드는 사용자 사전 프로바이더에 새 단어를 삽입하는 방법을 보여줍니다: 

// Defines a new Uri object that receives the result of the insertion
Uri mNewUri;

...

// Defines an object to contain the new values to insert
ContentValues mNewValues = new ContentValues();

/*
 * Sets the values of each column and inserts the word. The arguments to the "put"
 * method are "column name" and "value"
 */

mNewValues
.put(UserDictionary.Words.APP_ID, "example.user");
mNewValues
.put(UserDictionary.Words.LOCALE, "en_US");
mNewValues
.put(UserDictionary.Words.WORD, "insert");
mNewValues
.put(UserDictionary.Words.FREQUENCY, "100");

mNewUri
= getContentResolver().insert(
   
UserDictionary.Word.CONTENT_URI,   // the user dictionary content URI
    mNewValues                          
// the values to insert
);

새 데이터는 하나의 ContentValues 객체에 들어가며, 이는 하나의 결과를 갖는 커서와 그 형태가 비슷합니다. ContentValues 객체의 컬럼들의 데이터 타입은 모두 같을 필요가 없으며, 어떤 컬럼에 값을 지정하고 싶지 않다면 ContentValues.putNull() 메소드를 호출함으로써 해당 컬럼을 null로 셋팅할 수 있습니다. 

위의 예제 코드에서 _ID 컬럼은 보이지 않는데, _ID 컬럼은 자동적으로 관리되기 때문입니다. 프로바이더는 새로운 데이터가 삽입될 때 유니크한 값을 생성하여 _ID 컬럼에 추가해 주며, 보통은 이 값을 테이블의 기본키(primary key)로 사용합니다. 

mNewUri는 새로 추가된 데이터를 가리키는 컨텐트 URI로서, 아래와 같은 형태가 됩니다:

content://user_dictionary/words/<id_value>

위의 <id_value>는 추가된 데이터의 _ID 값입니다. 대부분의 프로바이더들은 위와 같은 형태의 URI들을 컨텐트 URI로 인식하고, 요청된 작업을 수행합니다. 

위와 같이 리턴받은 URI에서 _ID 값을 얻기 위해서, ContentUris.parseId() 메소드를 사용할 수 있습니다. 


데이터 갱신(updating)

데이터(row)를 업데이트하기 위해서는, 데이터를 삽입할 때처럼 갱신할 값들을 담고 있는 ContentValues 객체를 만들고, 쿼리에 담을 selection 조건을 만들고, ContentResolver.update() 메소드를 호출하면 됩니다. 만약 어떤 컬럼의 값을 비우고(clear) 싶다면, ContentValues의 값을 null로 셋팅하면 됩니다. 

아래 예제 코드는 locale이 "en_"으로 시작하는 데이터들을 찾아서 그 데이터들의 locale을 null로 갱신하는 것을 보여줍니다. 리턴받는 값은 업데이트된 데이터(row)들의 갯수를 의미합니다:

// Defines an object to contain the updated values
ContentValues mUpdateValues = new ContentValues();

// Defines selection criteria for the rows you want to update
String mSelectionClause = UserDictionary.Words.LOCALE +  "LIKE ?";
String[] mSelectionArgs = {"en_%"};

// Defines a variable to contain the number of updated rows
int mRowsUpdated = 0;

...

/*
 * Sets the updated value and updates the selected words.
 */

mUpdateValues
.putNull(UserDictionary.Words.LOCALE);

mRowsUpdated
= getContentResolver().update(
   
UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI
    mUpdateValues                      
// the columns to update
    mSelectionClause                    
// the column to select on
    mSelectionArgs                      
// the value to compare to
);

ContentResolver.update()를 호출할 때에는 사용자가 입력한 내용에 대한 유효성 검사가 선행되어야 합니다. 이에 대한 자세한 내용은 악의적인 입력으로부터 보호하기 섹션에서 학습하실 수 있습니다. 


데이터 삭제(deleting)

데이터들을 삭제하는 것은 데이터를 얻을 때와 방식이 비슷합니다. 지우길 원하는 데이터들에 대한 selection 조건을 지정한 쿼리를 요청하면 되며, 그러면 삭제된 데이터의 갯수(the number of deleted rows)가 리턴됩니다. 아래 예제 코드에서는 app id가 "user"인 데이터들을 삭제하는 방법을 보여주며, 삭제된 데이터의 수를 리턴받아 mRowsDeleted에 담고 있습니다.

// Defines selection criteria for the rows you want to delete
String mSelectionClause = UserDictionary.Words.APP_ID + " LIKE ?";
String[] mSelectionArgs = {"user"};

// Defines a variable to contain the number of rows deleted
int mRowsDeleted = 0;

...

// Deletes the words that match the selection criteria
mRowsDeleted
= getContentResolver().delete(
   
UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI
    mSelectionClause                    
// the column to select on
    mSelectionArgs                      
// the value to compare to
);

ContentResolver.delete()를 호출할 때에도 사용자가 입력한 내용에 대한 유효성 검사가 선행되어야 합니다. 이에 대한 자세한 내용은 악의적인 입력으로부터 보호하기 섹션에서 학습하실 수 있습니다. 


프로바이더의 데이터 타입들

컨텐트 프로바이더는 다양한 데이터 타입들을 제공할 수 있습니다. 사용자 사전 프로바이더는 텍스트만 제공하고 있긴 하지만, 프로바이더들은 아래 데이터 타입들도 제공할 수 있습니다. 

  • 정수형 (integer)
  • 긴 정수형 (long)
  • 부동소수점 (floating point)
  • 긴 부동소수점 (double)

그 외에도 프로바이더가 종종 사용하는 데이터 타입 중 하나로 64KB 크기의 byte array인 Binary Large OBject (BLOB)가 있습니다. 

Cursor 객체의 "get" 메소드들을 살펴보면 어떤 데이터 타입들을 사용할 수 있는지 확인하기 쉬울 것입니다. 

프로바이더의 각 컬럼에 대한 데이터 타입은 보통 그 프로바이더에 대한 문서에 명시되어 있습니다. 그리고 Cursor.getType() 메소드를 통해 커서 객체가 가지고 있는 결과 데이터의 각 컬럼에 대한 데이터 타입을 확인할 수도 있습니다. 

그리고 프로바이더들은 각 컨텐트 URI들에 대하여 MIME 데이터 타입 정보를 지원합니다. 내 앱이 프로바이더가 제공하는 데이터를 다룰 수 있는지를 확인해야 하거나, MIME 타입에 기반하여 데이터 타입을 선택해야 하는 경우에 사용할 수 있습니다. 그리고 보통은 프로바이더가 복잡한 구조의 데이터나 파일들을 가지고 있을 때 MIME 타입을 필요로 하게 됩니다. 예를 들면, 주소록 프로바이더의 ContactsContract.Data 테이블은 저장된 주소록 데이터의 타입을 정의하는데에 MIME 타입을 사용합니다.

아래의 MIME 타입들 섹션에서는 표준 및 자체적 MIME 타입들의 문법에 대하여 설명합니다.


프로바이더에 액세스하는 방법들

프로바이더에 액세스하는 아래 세가지 방법은 앱 개발에 있어서 중요한 부분입니다:

  • 배치 액세스: ContentProviderOperation 클래스를 이용하여 배치 작업을 만들 수 있고, ContentResolver.applyBatch() 메소드를 배치 작업을 적용할 수 있습니다.  
  • 비동기 쿼리(Asynchronous queries): 쿼리는 별도의 쓰레드에서 요청해야 합니다. CursorLoader 객체를 사용하는 것이 한가지 방법입니다. 이와 관련한 예제 및 설명은 로더(Loaders) 문서에서 학습하실 수 있습니다. 
  • 인텐트를 이용한 데이터 액세스: 프로바이더로 직접 인텐트를 보낼 수는 없지만, 프로바이더의 앱으로는 인텐트를 보낼 수 있습니다. 프로바이더 앱이 그에 대한 처리를 제대로 하고 있다면, 프로바이더의 데이터를 읽고 변경하는데에 이 방법을 사용할 수 있습니다. 

배치 액세스와 인텐트를 이용한 데이터 액세스에 대해서는 이어지는 섹션에서 자세히 설명합니다. 


배치 액세스(Batch access)

프로바이더로의 배치 액세스 방식은, 많은 양의 데이터를 삽입하는 경우, 한번의 메소드 호출로 여러개의 테이블에 데이터들을 삽입하는 경우, 그리고 (하나 또는 그 이상의 프로세스에 대하여) 트랜젝션을 보장해줘야 하는 작업들의 집합을 실행해야 하는 경우에 유용합니다. 

"배치모드"로 프로바이더에 액세스하기 위해서는, ContentProviderOperation 객체들의 배열을 만들고, ContentResolver.applyBatch() 메소드를 호출하여 그 배열을 인자로 넘겨주면 됩니다. 여기서 applyBatch()의 인자로 컨텐트 URI를 넘기는 대신에 프로바이더의 authority 값(URI에서 scheme과 path의 사이)을 넘겨줍니다. 이렇게 함으로써 배열 안에 있는 각각의 ContentProviderOperation 객체들이 프로바이더 안에 있는 모든 테이블들에 대하여 요청된 작업을 할 수 있습니다. 그리고 ContentResolver.applyBatch() 메소드는 그 결과들에 대한 배열을 리턴해 줍니다. 

안드로이드 SDK에 포함되어 있는 샘플앱들 중 Contact Manager 샘플앱에 있는 ContactAdder.java 파일을 보면 배치 액세스에 대한 구현 내용을 확인할 수 있습니다. 


인텐트를 이용한 데이터 액세스

인텐트를 이용하면 컨텐트 프로바이더에 간접적으로 액세스할 수 있습니다. 내 앱이 액세스 권한을 가지고 있지 않더라도, 권한을 가지고 있는 앱으로부터 결과 인텐트를 받거나, 권한을 가지고 있는 앱을 실행하여 사용자가 거기서 필요한 일을 하도록 유도함으로써, 사용자가 간접적으로 데이터에 액세스하는 것입니다. 


임시 권한을 이용해서 액세스하기

내 앱이 필요한 권한을 가지고 있지 않더라도, 권한을 가지고 있는 앱에 인텐트를 보내서 "URI" 권한들을 포함하고 있는 결과 인텐트를 받아 데이터에 액세스할 수 있습니다. 이러한 URI 권한들은 인텐트를 받은 액티비티가 종료되기 전에 결과 인텐트에 담아준 컨텐트 URI에 대한 권한들입니다. 영구적인 권한을 가지고 있는 앱이 결과 인텐트에 플래그(flag)를 셋팅함으로써 임시 권한을 만들어 줄 수 있다는 것입니다:

  • 읽기 권한: FLAG_GRANT_READ_URI_PERMISSION
  • 쓰기 권한: FLAG_GRANT_WRITE_URI_PERMISSION

메모: 위의 플래그들은 프로바이더에 대한 일반적인 읽기 및 쓰기 권한을 주는 것이 아닙니다. 이것은 오직 해당 URI에만 주어지는 임시 권한입니다.

프로바이더는 매니페스트 파일에서, <provider> 요소의 android:grantUriPermission 속성값을 설정하거나, <provider> 요소의 자식 요소로 <grant-uri-permission> 요소를 추가함으로써, 컨텐트 URI들에 대한 URI 권한들을 정의합니다. 

예를 들어, 내 앱이 READ_CONTACTS 권한을 가지고 있지 않더라도 주소록 프로바이더에서 주소록 데이터를 얻을 수 있습니다. 주소록에 있는 누군가에게 생일 축하 메시지를 보내는 상황을 생각해 보겠습니다. 이때, 모든 주소록 정보에 액세스할 수 있는 READ_CONTACTS 권한을 요청하지 않고, 누구에게 보낼지 사용자가 직접 선택할 수 있도록만 하면 될 것입니다. 그러기 위해서는 아래 절차대로 진행하면 됩니다:

1. 내 앱은 액션값이 ACTION_PICK이고 CONTENT_ITEM_TYPE의 값이 "contacts"인 인텐트를 startActivityForResult()메소드를 통해 보냅니다.

2. 이 인텐트는 주소록앱의 사람 선택하는 액티비티와 매치되기 때문에, 그 액티비티가 전면(foreground)으로 나옵니다.

3. 그 액티비티에서 사용자가 사람을 선택하면, 액티비티는 setResult(resultCode, intent)를 호출함으로써 내 앱에 전해줄 결과 인텐트를 셋팅합니다. 그 결과 인텐트는 사용자가 선택한 사람의 주소록 정보에 대한 컨텐트 URI를 가지고 있으며, FLAG_GRANT_READ_URI_PERMISSION이 셋팅되어 있습니다. 이 플래그는 내 앱에서 해당 컨텐트 URI에 대한 정보를 읽을 수 있음을 의미합니다. 사람 선택하는 액티비티는 이렇게 setResult()메소드를 호출해주고 나서 finish()를 호출하여 종료됩니다. 

4. 그러면 내 앱의 마지막 액티비티가 다시 전면으로 나오게 되고, onActivityResult(requestCode, resultCode, intent) 메소드가 호출됩니다. 사람 선택 액티비티에서 셋팅한 결과 인텐트를 여기에서 매개변수로 받습니다. 

5. 이제 결과 인텐트에 있는 컨텐트 URI를 이용하여 주소록 프로바이더로부터 데이터를 읽을 수 있습니다. 매니페스트 파일에 읽기 권한을 선언하지 않았지만, 생일이나 이메일 등과 같은 주소록 데이터를 얻어낼 수 있는 것입니다. 


다른 앱을 이용하기

컨텐트 프로바이더가 제공하는 데이터를 변경하고 싶은데 내 앱에 그럴 권한이 없을때는, 권한이 있는 다른 앱을 실행하여 사용자가 거기서 데이터를 변경하도록 하면 됩니다.

예를 들어, 캘린더 앱은 ACTION_INTENT 액션값을 갖는 인텐트에 대한 동작이 정의되어 있어서 해당 인텐트를 받으면, 데이터를 추가할 수 있는 화면을 실행해 줍니다. 그리고 인텐트에 "extra" 데이터를 전달해 주면 화면의 입력란에 전달받은 값들을 미리 셋팅해 줍니다. 기념일을 설정하는 등의 작업을 API를 통해 직접 처리하려면 복잡하기 때문에, 캘린더 앱을 실행하여 사용자가 거기서 관련된 일을 하도록 하는 것이 좋습니다. 


컨트랙트 클래스(Contract Classes)

컨트랙트 클래스는 컨텐트 URI, 컬럼명, 인텐트의 액션값 및 컨텐트 프로바이더의 기타 다른 기능들에 대한 상수값들을 정의하는 클래스입니다. 컨트랙트 클래스는 프로바이더에 자동적으로 포함되는 것이 아니기 때문에, 프로바이더를 개발할 때 값들을 정의하고 다른 개발자들이 사용할 수 있도록 만들어줘야 합니다. 안드로이드 플랫폼에 포함되어 있는 프로바이더들의 컨트랙트 클래스들은 대부분 android.provider 패키지 안에 있습니다. 

예를 들어, 사용자 사전 프로바이더의 컨텐트 URI와 컬럼명들을 담고 있는 컨트랙트 클래스는 UserDictionary 클래스입니다. "words" 테이블에 대한 컨텐트 URI는 UserDictionary.Words.CONTENT_URI 에 정의되어 있습니다. UserDictionary.Words 클래스에는 "words" 테이블의 컬럼명들도 정의되어 있습니다. 그 사용예는 아래와 같습니다:

String[] mProjection =
{
   
UserDictionary.Words._ID,
   
UserDictionary.Words.WORD,
   
UserDictionary.Words.LOCALE
};

다른 예로, 주소록 프로바이더의 컨트랙트 클래스는 ContactsContract 클래스입니다. 이 클래스의 레퍼런스 문서에는 예제 코드들도 포함되어 있습니다. 이 클래스의 서브클래스인 ContactsContract.Intents.Insert 클래스는 인텐트에 담을 데이터들에 대한 상수값들을 정의하고 있습니다. 


MIME 타입들

컨텐트 프로바이더는 표준적인 MIME 미디어 타입을 리턴하거나, 커스텀 MIME 타입을 리턴할 수 있습니다.

MIME 타입은 아래와 같은 형태를 갖습니다:

type/subtype

예를 들어, 잘 알려진 MIME 타입 중에 text/html 이 있는데, type이 text이고 subtype이 html 입니다. 프로바이더가 리턴한 MIME 타입이 text/html 이라면, 그 내용은 HTML 태그를 포함한 텍스트입니다. 

"벤더 고유의(vendor-specific)" MIME 타입이라고도 불리는 커스텀 MIME 타입은 좀더 복잡하게 생긴 type과 subtype을 갖습니다.

type은 두개 이상의 행(row)을 위한 경우 아래와 같고,

vnd.android.cursor.dir

하나의 행(row)을 위한 경우는 아래와 같습니다:

vnd.android.cursor.item

subtype은 프로바이더를 나타냅니다(provider-specific). 안드로이드 플랫폼에 포함되어 있는 프로바이더들은 보통 간단한 subtype을 갖습니다. 예를 들어, 주소록 앱에서 전화번호 정보를 저장할 때, 저장되는 데이터의 MIME 타입은 아래와 같습니다:

vnd.android.cursor.item/phone_v2

다른 프로바이더 개발자들은 그들의 프로바이더의 authority 부분과 테이블명에 기반하여 subtype을 정의할 수도 있을 것입니다. 예를 들어, 열차시간표 정보를 제공하는 프로바이더가 있다고 가정해 보겠습니다. 프로바이더의 authority 부분은 com.example.trains 이고, 테이블은 Line1, Line2, Line3 가 있습니다. Line1 테이블의 데이터들에 대한 컨텐트 URI는 다음과 같을 것이고,

content://com.example.trains/Line1

리턴 받는 MIME 타입은 아래와 같을 것입니다:

vnd.android.cursor.dir/vnd.example.line1

그리고 Line2 테이블의 5번 행(row)에 대한 컨텐트 URI는 다음과 같을 것이고,

content://com.example.trains/Line2/5

리턴 받는 MIME 타입은 아래와 같을 것입니다:

vnd.android.cursor.item/vnd.example.line2

대부분의 컨텐트 프로바이더들은 사용할 MIME 타입들을 위해 컨트랙트 클래스를 정의합니다. 예를 들어, 주소록 프로바이더의 컨트랙트 클래스인 ContactsContract.RawContacts 클래스는 주소록 데이터의 MIME 타입으로 CONTENT_ITEM_TYPE 을 정의합니다. 

하나의 데이터(single row)를 얻기 위한 컨텐트 URI에 대해서는 컨텐트 URI 섹션에서 설명하고 있습니다. 


Posted by 개발자 김태우
,