Android developer site study
- Getting Started
- Building Your First App
- Supporting Different Devices
- uilding a Dynamic UI with Fragments
- Saving Data
- Interacting with Other Apps
- Working with System Permissions
Android는 데이터를 저장하기 위해 주로 아래 세 가지의 방법을 사용한다.
- Shared Preference에 Key-Value 쌍 저장
- File system에 file 저장
- SQLite Database에 저장
Shared Preference의 사용
저장하고자 하는 데이터의 수가 적은 경우 SharedPreferences
API를 사용한다.
SharedPreferences
객체는 Key-Value 쌍을 포함하는 파일을 가리키며 이를 읽고 쓸 수 있는 간단한 메서드를 제공한다.
SharedPreferences API는 Key-Value 쌍을 읽고 쓰는 용도로만 사용된다.
App 설정을 위한 사용자 interface 빌드를 지원하는Preference
API와는 다르다.
SharedPreferences에 대한 Handle 가져오기
아래 메서드 중 하나를 호출하여 새로운 SharedPreference 파일을 생성하거나 기존 파일에 접근할 수 있다.
getSharedPreferences()
- 여러 개의 SharedPreference 파일이 필요한 경우 사용한다. 이 파일은 첫 번째 매개변수로 지정하는 일므으로 식별된다. App 내부의 모든 Context에서 이 메서드를 호출할 수 있다.
getPreferences()
- Activity에 하나의 SharedPreference 파일만 사용해야 하는 경우 Activity 에서 이 메서드를 사용한다. 이 메서드는 Activity에 속한 기본 SharedPreference 파일을 가져오기 때문에 이름을 제공할 필요가 없다.
아래 코드는 Fragment 내부에서 실행된다. 이 코드는 리소스 문자열 R.string.preference_file_key에 의해 식별되는 SharedPreference 파일에 액세스하며, App 자신만 파일에 액세스할 수 있도록 전용 모드에서 파일을 연다.
|
|
SharedPreference 파일의 이름을 지정할 때에는 “com.example.myapp.PREFERENCE_FILE_KEY”와 같이 App에 대한 고유 식별 이름을 사용해야 한다.
Activity에 대한 SharedPreference만 필요한 경우 getPreferences()
메서드를 사용하면 된다.
|
|
MODE_WORLD_READABLE 또는 MODE_WORLD_WRITEABLE을 사용하여 SharedPreference 파일을 생성하는 경우 파일 식별자를 인식하는 모든 App이 데이터에 접근할 수 있으므로 주의해야 한다.
SharedPreferences에 쓰기
SharedPreferences
에서edit()
를 호출하여SharedPreferences.Editor
를 생성한다.putInt()
,putString()
등의 메서드를 사용하여 Key-Value를 전달한다.commit()
을 호출하여 변경사항을 저장한다.
|
|
SharedPreferences에서 읽어오기
검색을 위해서는 원하는 Key를 제공해야 하며, Key 에 대한 Value가 저장되어 있지 않을 경우 반환할 기본 값도 넣어준 후 getInt()
, getString()
등의 메서드를 호출해야 한다.
|
|
파일에 저장
File
API를 사용하여 파일을 읽고 쓸 수 있다.
File
객체는 대량의 데이터를 처음부터 끝까지 순서대로 읽거나 쓸 때 적합하다. 이미지 파일이나 네트워크에서 교환되는 모든 항목에 적합하다.
내부/외부 저장소 선택
Android 기기에는 “내부 저장소”와 “외부 저장소(sd card 등)”가 나뉘어져 있다.
내부 저장소
내부 저장소는 다른 App이 자신의 파일에 접근하는 것을 원치 않을 때 적합하다.
- 항상 사용 가능하다.
- 내부 저장소에 저장된 파일은 자신의 App에서만 접근할 수 있다.
- 사용자가 App을 삭제하면 시스템이 내장 저장소에서 App의 모든 파일을 제거한다.
외부 저장소
외부 저장소는 접근 제한이 필요하지 않은 파일과 다른 App과 공유하기 원하는 파일 또는 사용자가 컴퓨터에서 접근할 수 있도록 허용하는 파일에 적합하다.
- 항상 사용 가능하지는 않다. 왜냐하면 사용자가 USB 저장소 등 외부 저장소를 마운트할 수도 있고 긱에서 외부 저장소를 제거할 수 있기 때문이다.
- 모든 사람이 읽을 수 있다.
- 사용자가 App을 삭제하면
getExternalFilesDir()
의 디렉토리에 저장한 App 파일에 한해서 시스템이 제거한다.
기본적으로 App은 내부 저장소에 설치되지만 Manifest에
android:installLocation
특성을 지정하여 외부 저장소에 설치할 수도 있다.
사용자는 APK 크기가 매우 크고 내부 저장소 공간보다 외부 저장소 공간이 더 클 때 이 옵션을 유용하게 사용할 수 있다.
외부 저장소에 대한 권한 취득
외부 저장소에 데이터를 읽고 쓰기 위해서는 Manifest 파일에 READ_EXTERNAL_STORAGE
, WRITE_EXTERNAL_STORAGE
권한을 요청해야 한다.
WRITE_EXTERNAL_STORAGE
가 선언된 경우 READ_EXTERNAL_STORAGE
권한이 없어도 묵시적으로 READ 권한도 포함되어 있다.
|
내부 저장소에 파일 저장하기
내부 저장소에 파일을 저장할 경우 아래 두 메서드 중 하나를 사용하여 디렉토리를 File로 얻을 수 있게 된다.
getFileDir()
- App에 대한 내부 디렉토리를 나타내는
File
을 반환한다.
- App에 대한 내부 디렉토리를 나타내는
getCacheDir()
- App의 임시 캐시 파일에 대한 내부 디렉토리를 나타내는
File
을 반환한다. 더 이상 필요하지 않은 파일은 모두 삭제하고 언제든지 사용할 수 있는 메모리 크기에 합리적인 크기로(ex. 1MB) 제한하여 구현해야 한다. - 저장 공간이 부족해지기 시작하면 경고 없이 시스템이 캐시 파일을 삭제할 수도 있다.
- App의 임시 캐시 파일에 대한 내부 디렉토리를 나타내는
디렉토리 중 하나에서 새 파일을 생성하기 위해 File()
생성자를 사용하고 위의 메서드 중 하나를 통해 제공되는 File을 전달하면 된다.
|
openFileOutput()
을 호출하여 내부 디렉토리의 파일에 데이터를 쓰는 FileOutputStream
을 가져올 수 있다.
|
|
파일을 캐싱해야 할 경우 createTempFile()
을 사용한다. 아래 코드는 URL로부터 파일 이름을 추출한 뒤 해당 이름을 사용하여 App의 내부 캐시 디렉토리에 파일을 생성한다.
|
|
App의 내부 저장소 디렉토리는 Android v아리 시스템의 특별한 위치에 있는 App의 Package 이름으로 지정된다.
파일 모드를 읽기 가능으로 설정할 경우 다른 App이 내부 파일을 읽을 수 있다. 하지만 이는 다른 App도 나의 App package 이름 및 파일 이름을 알아야 가능하다.
달느 App은 나의 App 내부 디렉토리를 탐색할 수 없으며, 명식적으로 읽기/쓰기 가능으로 파일을 설정하지 않으면 파일에 접근할 수 없다.
따라서, MODE_PRIVATE을 내부 저장소 내 파일에 사용한다면 다른 App은 접근할 수 없게 된다.
외부 저장소에 파일 저장
사용자가 외부 저장소를 PC에 마운트 했거나 SD Card를 제거한 경우 외부 저장소를 사용할 수 없기 때문에 접근하기 전 항상 사용 가능한지 확인해야 한다.
이 때 getExternalStorageState()
를 사용하여 외부 저장소 상태를 확인할 수 있다. Return된 상태가 MEDIA_MOUNTED
라면 사용이 가능한 것이다.
|
|
외부 저장소에 저장할 수 있는 파일의 종류는 크게 두 가지가 있다.
- 공용 파일
- 다른 App 및 사용자가 자유롭게 사용 가능한 파일들.
- 사용자가 App을 제거해도 파일은 유지되어야 함. (ex. 캡쳐된 사진, 다운로드된 파일)
- 전용 파일
- App이 삭제될 때 같이 삭제되어야 하는 파일들.
- 사용자 및 다른 App의 접근이 가능하지만 App 외부에서의 가치는 없는 파일들.
- 사용자가 App을 제거하면 전용 파일들도 모두 삭제되어야 함. (ex. 추가 리소스 또는 임시 미디어 파일)
외부 저장소에 공용 파일을 저장하려는 경우 getExternalStoragePublicDirectory()
메서드를 사용하여 외부 저장소에 적절한 디렉토리를 나타내는 File을 가져온다.
이 메서드는 DIRECTORY_MUSIC
또는 DIRECTORY_PICTURES
와 같은 다른 공개 파일과 논리적으로 구성될 수 있도록 저장하고자 하는 파일의 유형을 지정하는 매개변수를 받는다.
|
|
개인 파일을 App에 저장하고자 하는 경우, getExternalFilesDir()
을 호출하고 원하는 디렉토리 유형을 나타내는 이름을 전달하여 적절한 디렉토리를 얻을 수 있다.
이런 식으로 생성된 각 디렉토리는 부모 디렉토리에 추가된다.
이 디렉토리는 사용자가 App을 제거할 때 시스템이 삭제하는 App의 모든 외부 저장소 파일을 캡슐화한다.
|
|
미리 정의된 하위 디렉토리 이름 중 파일에 알맞은 이름이 없을 경우 대신 getExternalFilesDir()
을 호출하고 null
을 전달할 수 있다. 그렇게 되면 App의 외부 저장소 내의 전용 디렉토리의 root 디렉토리를 가져올 수 있다.
사용자가 App을 제거할 때 getExternalFilesDir()
이 삭제된 디렉토리 내에 디렉토리를 생성한다는 것을 꼭 기억해야 한다.
App이 삭제되어도 사진 등이 남아야 한다면 사용자가 App을 제거한 후에도 저장한 파일을 사용 간으하게 유지하기 위해 getExternalStoragePublicDirectory()
를 사용해야 한다.
공유 파일에 getExternalStoragePublicDirectory()
를 사용하든 개인 파일에 getExternalFilesDir()
을 사용하든지 관계 없이 DIRECTORY_PICTURES
와 같이 API 상수로 제공되는 디렉토리 이름을 사용해야 한다.
제공되는 디렉토리 이름을 사용함으로써 시스템이 파일을 적절하게 처리할 수 있게 된다. 예를 들어, DIRECTORY_RINTONES
에 저장된 파일은 시스템 미디어 스캐너에 의해 음악 대신 벨소리로 자동 분류된다.
여유 공간 쿼리하기
저장하는 데이터의 크기를 미리 알고 있을 경우, getFreeSpace()
또는 getTotalSpace()
를 호출하여 사용 공간이 충분한지 확인할 수 있다.
이를 확인하지 않고 저장을 시도할 경우 저장 공간이 충분하지 않다면 IOException
이 발생할 수 있다.
이런 정보를 통해 저장소가 임계치를 초과하는지 감시할 때도 사용할 수 있다.
하지만, 시스템은 getFreeSpace()
로 지정된만큼 용량이 사용 가능하다는 것을 보장하지는 않는다. 저장하고자 하는 데이터보다 남은 용량이 수 MB 크거나 저장 공간이 90% 이상일 때에만 시스템은 안정성을 제공한다.
파일을 저장하기 전에 반드시 사용 가능한 공간을 체크할 필요는 없다. 하지만 IOException이 발생하는 경우 이를 catch하면 안 된다.
파일 삭제하기
더 이상 필요하지 않은 파일은 항상 삭제하는 습관을 들이자. 파일을 삭제하는 가장 간단한 방법은 열린 파일 참조가 delete()
를 직접 호출하도록 하는 것이다.
|
파일이 내부 저장소에 저장되어 있는 경우, Context에 위치를 요청하고 deleteFile()
을 호출하여 파일을 삭제할 수도 잇다.
|
사용자가 App을 제거하면 Android는 아래 사항들을 삭제한다.
- 내부 저장소에 저장한 모든 파일
getExternalFilesDir()
을 사용해 외부 저장소에 저장한 모든 파일하지만
getCacheDir()
로 생성된 모든 캐시 파일과 더 이상 필요치 않은 다른 파일은 정기적으로 직접 삭제해야 한다.
SQL Database에 Data 저장하기
연락처 정보 등 반복적이거나 구조적인 데이터의 경우 Database에 저장하는 것이 이상적이다.
Android에서 Database 사용에 필요한 API는 android.database.sqlite
패키지에 있다.
Schema 및 Contract의 정의
Schema는 Database 기본 원칙 중 하나로 Database 구성 체계에 대한 공식적인 선언이다.
Contract라고 하는 helper class를 생성하면 도움이 될 수 있다. Contact class는 체계적이고 자기 문서화 방식으로 Schema의 레이아웃을 명시적으로 지정한다.
Contract class는 URI, Table 및 Column의 이름을 정의하는 상수를 유지하는 컨테이너이다. Contract class를 사용하면 동일한 package 내 모든 class에 동일한 상수를 사용할 수 있게 된다.
즉, 어느 한 장소에서 Column 이름을 변경하면 코드 전체에 변경 사항이 반영된다.
Contract class를 구성하는 좋은 방법은 class의 root 레벨에 전체 Database에 전역적인 정의를 추가하는 것이다. 그 후 Column을 열거하는 각 테이블에 대해 내부 class를 생성한다.
BaseColumns 인터페이스를 구현함으로써, 내부 class는
_ID
라고 하는 기본 key field를 상속할 수 있다. Cursor adapter 같은 일부 Android class의 경우 내부 class가 이러한 기본 key field를 가지고 있을 것이라 예상하고 동작한다.내부 class는 반드시 필요한 것은 아니지만, Database가 Android framework와 조화롭게 작업하는데 도움이 될 수 있다.
예를 들어 아래 sniffet은 테이블 이름과 단일 테이블의 column 이름을 정의한다.
|
|
SQL Helper를 이용한 Database 생성하기
Database의 모양을 정의한 후에는 Database 및 테이블을 생성 및 유지하는 메서드를 구현해야 한다.
아래는 테이블을 생성하고 삭제하는 몇 가지 일반적인 명령문이다.
|
|
기기의 내부 저장소에 저장하는 파일처럼 Android는 Database를 App과 관련된 전용 디스크 공간에 저장한다.
기본적으로 이 공간은 다른 App이 접근할 수 없기 때문에 저장된 데이터는 안전하게 유지된다.
유용한 API 집합이 SQLiteOpenHelper
class에서 제공된다. Database에 대한 참조를 가져오기 위해 이 class를 사용하는 경우 시스템은 필요한 경우에 한해서면 Database 생성 및 업데이트와 같이 장시간 실행될 수 있는 작업을 수행하며, App이 시작되는 동안에는 이러한 작업을 수행하지 않는다.
getWritableDatabase()
또는 getReadableDatabase()
를 호출하기만 하면 된다.
이러한 작업은 장시간 실행될 수도 있기 때문에
AsyncTask
또는IntentService
와 같이 백그라운드 스레드에서getWritableDatabase()
또는getReadableDatabase()
를 호출해야 한다.
SQLiteOpenHelper
를 사용하려면 onCreate()
, onUpgrade()
, onOpen()
콜백 메서드를 재정의하는 서브클래스를 생성해야 한다.
꼭 필요한 것은 아니지만 onDowngrade()
를 구현해야 하는 경우도 있다.
아래는 SQLiteOpenHelper
의 예시이다.
|
|
Database에 접근하려면 SQLiteOpenHelper
의 서브클래스를 인스턴스화한다.
|
Database에 정보 삽입하기
ContentValues
객체를 insert()
메서드에 전달하여 data를 database에 삽입한다.
|
|
insert()
의 첫 번째 인수는 테이블 이름이다.
두 번째 인수는 ContentValues
가 비어있는 경우 즉, put
에 어떤 값도 지정하지 않은 경우에 framework가 수행할 작업을 알려준다. column의 이름을 지정하면 framework는 row를 삽입하고 column의 값을 null로 설정한다.
위의 코드와 같이 null로 지정하면 framework는 값이 없는 경우 row를 삽입하지 않는다.
Database에서 정보 읽어오기
Database로부터 정보를 읽어오기 위해서는 query()
메서드를 사용해야 하고 선택 기준과 원하는 column을 전달한다.
이 메서드는 insert()
와 update()
를 결합한다.
단, 삽입하려는 data가 아닌, 가져오려는(fetch) data를 정의한 column list는 예외로 한다.
쿼리 결과는 Cursor 객체로 반환된다.
|
|
Cursor 안의 row를 보려면 Cursor의 이동 메서드
중 하나인 moveToFirst()
를 항상 제일 먼저 호출해야 한다.
이 메서드를 통해 일반적으로 결과의 처음 항목에 “읽기 위치”를 배치하게 된다.
이후 getString()
이나 getLong()
등과 같은 Cursor 가져오기 메서드
중 하나를 호출하여 각 row에 대한 column값을 읽어올 수 있게 된다.
가져오기 메서드
각각에 대해 원하는 column의 인덱스 위치를 전달해야 하며, 이는 getColumnIndex()
또는 getColumnIndexOrThrow()
를 호출하여 가져올 수 있다.
|
|
Database에서 정보 삭제하기
테이블에서 row를 삭제하려면 row를 식별하는 선택 기준을 제공해야 한다. Database API는 SQL injection을 방지하는 선택 기준을 생성하는 메커니즘을 제공한다.
이 매커니즘은 선택하는 방식을 선택 절(selection creteria)
과 선택 인수(selection arguments)
로 나눈다. 선택 절
은 보려는 column을 정의하고, 이를 통해 column 테스트를 결합할 수 있다.
선택 인수
는 선택 절
안에 묶여 테스트되는 값이다. 이 결과는 일반 SQL 문과 같이 처리되지 않기 때문에 SQL injection의 영향을 받지 않는다.
|
|
Database 업데이트 하기
update()
메서드를 사용한다.
테이블을 업데이트하면 insert()
의 콘텐츠 값 구문과 delete()
의 where
구문이 결합된다.\
|
|