본문 바로가기

Frontend/Kotlin

Unit 7

<데이터 저장방식>

-내부 저장소& 외부 저장소

내부 저장소는 특정 앱의 사용자가 접근할 수 있는 영역이다. 설치한 앱에 제공되는 디렉토리로 앱 삭제시 함께 삭제된다. 더불어 특정 앱에서만 사용이 가능하다.

외부 저장소는 모든 앱이 공용으로 사용할 수 있는 영역이다. 따라서 서로 다른 앱간에 공유가 필요한 데이터를 저장한다. 이 저장소를 사용하려면 사용자의 승인이 필요하다.

 

<File>

FileReader / FileWriter: 파일에서 문자열 스트림으로 데이터를 읽거나 쓰는 클래스

 *파일 사용하기

 내부 저장소에서 파일을 읽는 경우이다. 파일 정보를 사용하려면 File 클래스를 먼저 생성해야한다. 생성된 클래스를 통해서 각종 정보를 얻거나 기능을 사용할 수 있다. 

val file = File(context.filesDir, filename)

 파일 경로와 파일명을 입력해서 생성할 수도 있다.

val filename = "myfile"
val fileContents = "Hello world!"
context.openFileOutput(filename, Context.MODE_PRIVATE).use {
        it.write(fileContents.toByteArray())
}

 파일을 스트림으로 읽으려면 openFileInput()을 사용해야한다

context.openFileInput(filename).bufferedReader().useLines { lines ->
    lines.fold("") { some, text ->
        "$some\n$text"
    }
}

 파일 목록을 보기 위해서 fileList를 호출하여 filesDir 디렉터리에 있는 모든 파일의 이름이 포함된 배열을 가져올 수 있다.

var files: Array<String> = context.fileList()

 코틀린 기반 코드에서 getDir()을 호출하여 또는 자바 기반 코드에서 루트 디렉터리와 새 디렉터리 이름을 File 생성자에 전달하여 중첩된 디렉터리를 만들거나 내부 디렉터리를 열 수도 있다.

context.getDir(dirName, Context.MODE_PRIVATE)

 

<SharedPreferences>

sharedPreferences는 내부 저장소를 이용하기 때문에 권한 설정이 없이 훨씬 간단한 코드로 사용할 수 있다. 주로 로그인 정보나 앱의 상태 정보를 저장하는 용도로 사용된다.

데이터를 key-value 값으로 저장할 수 있고 데이터는 xml형식으로 된 파일로 저장되며 앱이 종료되어도 남아있다. 

 

*사용하기

 -공유환경설정의 핸들 가져오기

 getSharedPreferences(): 이름으로 식별되는 공유 환경설정 파일이 여러 개 필요한 경우 이 메서드를 사용한다. 이름을 첫 번째 매개변수로 지정할 수 있다. 앱의 모든 context에서 이 메서드를 호출할 수 있다. 

 getPreferences(): 활동에 공유 환경설정 파일을 하나만 사용해야 하는 경우 Activity에서 이 메서드를 사용한다. 이 메서드는 활동에 속한 기본 공유 환경설정 파일을 검색하기 때문에 이름을 제공할 필요가 없다. 

이 메서드중 하나를 호출하여 새로운 공유 환경설정 파일을 생성하거나 기존 파일에 액세스 할 수 있다.

val sharedPref = activity?.getSharedPreferences(
        getString(R.string.preference_file_key), Context.MODE_PRIVATE)

위의 코드는 리소스 문자열  R.string.preference_file_key로 식별되는 공유 환경ㅇ설정 파일에 액세스하고 비공개 모드를 사용하여 파일을 열기 때문에 앱에서만 파일에 액세스 할 수 있다.

val sharedPref = activity?.getPreferences(Context.MODE_PRIVATE)

 

-공유 환경설정에 쓰기

 공유 환경설정 파일에 쓰려면 SharedPreferences에서 edit()을 호출해서 SharedPreferences.Editor를 만든다. putInt() 및 putString()과 같은 메서드를 사용하여 키와 값을 전달한다. 그다음 apply() 또는 commit()을 호출하여 변경사항을 저장한다.

val sharedPref = activity?.getPreferences(Context.MODE_PRIVATE) ?: return
with (sharedPref.edit()) {
    putInt(getString(R.string.saved_high_score_key), newHighScore)
    apply()
}

apply()는 메모리 내 SharedPreferences 객체를 즉시 변경하지만 업데이트를 디스크에 비동기적으로 쓴다. commit()을 사용하면 데이터를 디스크에 동기적으로 쓸 수 있다. 하지만 이 경우 동기적이므로 기본 스레드에서 호출하는 것은 피해야 한다. ui렌더링이 일시중지될 수 있다.

 

 -공유 환경설정에서 읽기

 공유 환경설정 파일에서 값을 검색하려면 getInt(), getString() 과 같은 메서드를 호출하여 원하는 값에 키를 제공하고 키가 없으면 선택적으로 반환할 기본값을 제공한다.

val sharedPref = activity?.getPreferences(Context.MODE_PRIVATE) ?: return
val defaultValue = resources.getInteger(R.integer.saved_high_score_default_key)
val highScore = sharedPref.getInt(getString(R.string.saved_high_score_key), defaultValue)

<SQLite>

안드로이드의 기본 데이터 베이스이다. 

 -SQLiteOpenHelper 사용하기

  이 데이터베이스를 사용하기 위해서는 안드로이드의 컨텍스트가 가지고 있는 createDatabase() 메서드를 사용하거나        SQLiteOpenHelper 클래스를 상속받아 사용해야한다. 이 클래스는 데이터베이스를 파일로 생성하고 코틀린 코드에서 사용할 수 있도록 데이터베이스와 연결하는 역할을 한다. 

class DBHelper(context: Context?): SQLiteOpenHelper(context, "memodb", null,1) {
    override fun onCreate(db: SQLiteDatabase) {
        val memoSQL = ("create table tb_memo" +
                "(_id integer primary key autoincrement,"
                + "title,"
                +"content)"
                )
        db.execSQL(memoSQL)
    }

기기의 내부 저장소에 저장한 파일과 마찬가지로 이 데이터베이스를 앱의 비공개 폴더에 저장하여 다른 앱이 접근할 수 없다. SQLiteOpenHelper 클래스를 사용하여 데이터베이스의 참조를 가져오면 시스템은 앱이 시작되고 있는 동안이 아닌 필요할때에만 데이터베이스 생성 및 업데이트와 같이 장시간 실행될 수 있는 작업을 실행한다. 또한 이 클래스를 사용하려면 onCreate()와 onUpgrade() 콜백 메서드를 필수적으로 재정의해야한다. 

 

  -데이터베이스에 정보 삽입

   다음과 같이 ContentValues객체를 insert()메서드에 전달하여 데이터를 데이터베이스에 삽입할 수 있다. 

 

// Gets the data repository in write mode
val db = dbHelper.writableDatabase

// Create a new map of values, where column names are the keys
val values = ContentValues().apply {
    put(FeedEntry.COLUMN_NAME_TITLE, title)
    put(FeedEntry.COLUMN_NAME_SUBTITLE, subtitle)
}

// Insert the new row, returning the primary key value of the new row
val newRowId = db?.insert(FeedEntry.TABLE_NAME, null, values)

  -데이터베이스에서 정보 읽어오기

  정보를 읽어오려면 query() 메서드를 사용하고 이 메서드에 선택 기준 및 원하는 열을 전달한다. 쿼리 결과는 cursor 객체    로 반환된다. 

(*커서란? 데이터셋을 처리할 때 현재위치를 포함하는 데이터 요소이다. 커서 사용시 쿼리를 통해 반환된 데이터셋을 반복문으로 반복하며 하나씩 처리할 수 있다. )

val db = dbHelper.readableDatabase

// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(BaseColumns._ID, FeedEntry.COLUMN_NAME_TITLE, FeedEntry.COLUMN_NAME_SUBTITLE)

// Filter results WHERE "title" = 'My Title'
val selection = "${FeedEntry.COLUMN_NAME_TITLE} = ?"
val selectionArgs = arrayOf("My Title")

// How you want the results sorted in the resulting Cursor
val sortOrder = "${FeedEntry.COLUMN_NAME_SUBTITLE} DESC"

val cursor = db.query(
        FeedEntry.TABLE_NAME,   // The table to query
        projection,             // The array of columns to return (pass null to get all)
        selection,              // The columns for the WHERE clause
        selectionArgs,          // The values for the WHERE clause
        null,                   // don't group the rows
        null,                   // don't filter by row groups
        sortOrder               // The sort order
)

  커서의 moveToNext() 메서드가 실행되면 다음줄에 사용할 수 있는 레코드가 있는지 여부를 반환하고 해당 커서를 다음 위치로 이동시킨다. 레코드가 없다면 반복문을 빠져나가게 된다.

val itemIds = mutableListOf<Long>()
with(cursor) {
    while (moveToNext()) {
        val itemId = getLong(getColumnIndexOrThrow(BaseColumns._ID))
        itemIds.add(itemId)
    }
}
cursor.close()

 

  -데이터베이스에서 정보삭제

   테이블에서 행을 삭제하려면 행을 식별하는 선택 기준을 delete()메서드에 제공해야한다. 

val deletedRows = db.delete(FeedEntry.TABLE_NAME, selection, selectionArgs)

 

  -데이터베이스 업데이트

   데이터베이스 값의 일부를 수정해야한다면 update()메서드를 사용한다. 

val db = dbHelper.writableDatabase

// New value for one column
val title = "MyNewTitle"
val values = ContentValues().apply {
    put(FeedEntry.COLUMN_NAME_TITLE, title)
}

// Which row to update, based on the title
val selection = "${FeedEntry.COLUMN_NAME_TITLE} LIKE ?"
val selectionArgs = arrayOf("MyOldTitle")
val count = db.update(
        FeedEntry.TABLE_NAME,
        values,
        selection,
        selectionArgs)

 <RoomDB>

  -ORM은 객체와 관계형 데이터베이스의 데이터를 매핑하고 변환하는 기술로 복잡한 쿼리를 잘 몰라도 코드만으로 데이   터 베이스의 모든것을 컨트롤할 수 있도록 도와준다. 안드로이드는 SQLite를 코드관점에서 접근할 수 있도록 ORM라이     브  러리인 Room을 제공한다. 

 -Room을 사용하면 이점

  *SQL쿼리의 컴파일 시간 확인

  *반복적이고 오류가 발생하기 쉬운 상용구 코드를 최소화하는 편의 주석

  *간소화된 데이터베이스 이전 경로

 

1.설정

gradle파일에 종속 항목을 추가한다.

dependencies {
    val room_version = "2.5.0"

    implementation("androidx.room:room-runtime:$room_version")
    annotationProcessor("androidx.room:room-compiler:$room_version")

    // To use Kotlin annotation processing tool (kapt)
    kapt("androidx.room:room-compiler:$room_version")
    // To use Kotlin Symbol Processing (KSP)
    ksp("androidx.room:room-compiler:$room_version")

    // optional - Kotlin Extensions and Coroutines support for Room
    implementation("androidx.room:room-ktx:$room_version")

    // optional - RxJava2 support for Room
    implementation("androidx.room:room-rxjava2:$room_version")

    // optional - RxJava3 support for Room
    implementation("androidx.room:room-rxjava3:$room_version")

    // optional - Guava support for Room, including Optional and ListenableFuture
    implementation("androidx.room:room-guava:$room_version")

    // optional - Test helpers
    testImplementation("androidx.room:room-testing:$room_version")

    // optional - Paging 3 Integration
    implementation("androidx.room:room-paging:$room_version")
}

 

*기본 구성요소

  -데이터베이스 클래스: 데이터베이스를 보유하고 앱의 영구 데이터와의 기본 연결을 위한 기본 액세스 포인트 역할을 한    다.

  -데이터 항목: 앱 데이터베이스의 테이블을 나타낸다

  -데이터 액세스 객체(DAO):앱이 데이터베이스의 데이터를 쿼리, 업데이트 삽입, 삭제하는데 사용할 수 있는 메서드를 제    공한다. 앱은 이를 사용해 데이터베이스의 데이터를 연결된 데이터 항목 객체의 인스턴스로 검색할 수 있게 된다.

2.데이터 항목

 각 User 인스턴스는 앱 데이터베이스의 user테이블에 있는 행 하나를 나타낸다

@Entity
data class User(
    @PrimaryKey val uid: Int,
    @ColumnInfo(name = "first_name") val firstName: String?,
    @ColumnInfo(name = "last_name") val lastName: String?
)

3.데이터 액세스 객체 DAO

 UserDao는 앱의 나머지 부분이 user테이블의 데이터와 상호작용하는데 사용하는 메서드를 제공한다.

@Dao
interface UserDao {
    @Query("SELECT * FROM user")
    fun getAll(): List<User>

    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    fun loadAllByIds(userIds: IntArray): List<User>

    @Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
           "last_name LIKE :last LIMIT 1")
    fun findByName(first: String, last: String): User

    @Insert
    fun insertAll(vararg users: User)

    @Delete
    fun delete(user: User)
}

 4. 데이터베이스

 다음은 데이터베이스를 보유할 AppDatabase클래스를 정의한다. 여기에 데이터베이스 구성을 정의하고 영구 데이터에 대한 앱의 기본 액세스 포인트 역할을 한다.

(*데이터베이스 클래스의 조건*)

-클래스에는 데이터베이스와 연결된 데이터 항목을 모두 나열하는 entities 배열이 포함된 @Database 주석이 달려야 한다.

-클래스는 RoomDatabase를 확장하는 추상클래스여야 한다.

-데이터베이스와 연결된 각 DAO클래스에서 데이터베이스 클래스는 인수가 0개이고 DAO 클래스의 인스턴스를 반환하는 추상메서드를 정의해야한다.

@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

5.사용

val db = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java, "database-name"
        ).build()
val userDao = db.userDao()
val users: List<User> = userDao.getAll()

다음과 같이 데이터베이스와 상호작용할 수 있다. 

 

<SharedPreference 와 RoomDB>

-SharedPreference 는 가벼운 데이터를 저장할 목적으로 로컬 DB를 사용한다. 영속적으로 보관할 데이터가 있을때 사용하고 앱이 죽거나 다시 시작되는 경우, 기기가 다시 시작되는 경우에도 사용된다. 유저의 설정이나 게임 스코어와 같이 세션이 지나도 보존되어야할 데이터일 경우 SharedPreference 를 사용한다. 

 

-RoomDB는 큰 사이즈의 데이터를 저장할 목적으로 로컬 DB를 사용하게 된다. 상당히 구조화되어있다. 기기가 네트워크에 액세스가 불가할때 오프라인 상태에도 사용자는 관련 데이터를 캐싱해놓았기에 여전히 탐색이 가능하다. 나중에 기기가 온라인 상태가 되면 사용자가 시작한 콘텐츠 변경사항이 서버에 동기화된다. 

'Frontend > Kotlin' 카테고리의 다른 글

About DataBinding  (0) 2023.07.29
[안드로이드] 코틀린 아키텍처 패턴  (0) 2023.07.21
Unit 7-2(데이터 베이스 프로그램) 강의정리  (0) 2023.01.21
Unit 7-1 강의정리  (1) 2023.01.21
Unit 6  (0) 2023.01.18