안드로이드 6주차 정리

김성준·2022년 2월 27일
0

안드로이드

목록 보기
10/16

이 자료는 Android Kotlin 기초의 내용을 정리한 것입니다.

Android에서 데이터는 데이터 클래스로 표현됩니다. 이 데이터는 함수 호출을 사용하여 액세스하고 수정할 수 있습니다. 그러나 데이터베이스 세계에서는 데이터에 액세스하고 수정하기 위해 엔터티와 쿼리가 필요합니다.

  • 엔터티는 해당 속성과 함께 데이터베이스에 저장할 개체 또는 개념을 나타냅니다. 애플리케이션 코드에서 테이블을 정의하는 엔터티 클래스가 필요하며 해당 클래스의 각 인스턴스는 해당 테이블의 행을 나타냅니다. 엔티티 클래스에는 데이터베이스의 정보를 표시하고 상호 작용하는 방법을 Room에 알려주는 매핑이 있습니다. 앱에서 엔티티는 수면에 대한 정보를 보유할 것입니다.

  • 쿼리는 데이터베이스 테이블 또는 테이블 조합의 데이터 또는 정보에 대한 요청 또는 데이터에 대한 작업을 수행하라는 요청입니다. 일반적인 쿼리는 엔터티를 생성, 읽기, 업데이트 및 삭제하기 위한 것입니다. 예를 들어 쿼리를 실행하여 기록에 있는 모든 수면을 시작 시간별로 정렬하여 읽을 수 있습니다.

앱의 사용자 경험(다른 일반적인 사용 사례와 유사)은 일부 데이터를 로컬에 유지하면 큰 이점을 얻을 수 있습니다. 관련 데이터를 캐싱하면 사용자가 오프라인 상태에서도 앱을 즐길 수 있습니다. 앱이 서버에 의존하는 경우 캐싱을 통해 사용자는 오프라인 상태에서 로컬로 지속되는 콘텐츠를 수정할 수 있습니다. 앱이 다시 연결되면 캐시된 변경 사항을 백그라운드에서 원활하게 서버에 동기화할 수 있습니다.

Room은 Kotlin 데이터 클래스를 SQLite 테이블에 저장할 수 있는 엔터티입니다. 또한 함수를 SQLite 쿼리로 바꿀 수도 있습니다.

각 엔터티를 주석이 있는 데이터 클래스로 정의하고 해당 엔터티와의 상호 작용을 DAO(데이터 액세스 개체)라고 하는 주석이 있는 인터페이스로 정의해야 합니다. Room은 이러한 주석이 추가된 클래스를 사용하여 데이터베이스에 테이블을 생성하고 데이터베이스에서 작동하는 쿼리를 생성합니다.

1. Create the SleepNight entity

이 작업에서는 데이터베이스 엔터티를 나타내는 주석이 있는 데이터 클래스로 하루의 수면을 정의합니다.

수면에 대해 시작 시간, 종료 시간, 품질 등급을 기록해야 합니다.

그리고 날짜를 고유하게 식별할 수 있는 ID가 필요합니다.

1.1. 데이터베이스 패키지에서 SleepNight.kt 파일을 찾아 엽니다.

1.2. ID, start time(밀리초 단위), end time(밀리초 단위) 및 numerical sleep-quality rating에 대한 매개변수를 사용하여 SleepNight 데이터 클래스를 생성합니다.

data class SleepNight(
       var nightId: Long = 0L,
       val startTimeMilli: Long = System.currentTimeMillis(),
       var endTimeMilli: Long = startTimeMilli,
       var sleepQuality: Int = -1
)

1.3. 클래스 선언 전에 @Entity로 데이터 클래스에 주석을 답니다. 이 주석에는 몇 가지 가능한 인수가 있습니다. 기본적으로(@Entity에 대한 인수 없음) 테이블 이름은 클래스와 동일합니다. 그러나 Daily_sleep_quality_table이라는 유용한 테이블 이름을 사용합시다. tableName에 대한 이 인수는 선택 사항이지만 적극 권장됩니다. 문서에서 조사할 수 있는 @Entity에 대한 몇 가지 다른 인수가 있습니다.

@Entity(tableName = "daily_sleep_quality_table")
data class SleepNight(...)

1.4. nightId를 기본 키로 식별하려면 nightId 속성에 @PrimaryKey로 주석을 추가합니다. Room이 각 엔터티에 대한 ID를 생성하도록 매개변수 autoGenerate를 true로 설정합니다. 이렇게 하면 각 밤의 ID가 고유합니다.

@PrimaryKey(autoGenerate = true)
var nightId: Long = 0L,...

1.5. @ColumnInfo로 나머지 속성에 주석을 답니다. 아래와 같이 매개변수를 사용하여 속성 이름을 사용자 정의합니다.

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "daily_sleep_quality_table")
data class SleepNight(
       @PrimaryKey(autoGenerate = true)
       var nightId: Long = 0L,

       @ColumnInfo(name = "start_time_milli")
       val startTimeMilli: Long = System.currentTimeMillis(),

       @ColumnInfo(name = "end_time_milli")
       var endTimeMilli: Long = startTimeMilli,

       @ColumnInfo(name = "quality_rating")
       var sleepQuality: Int = -1
)

1.6. 앱을 빌드하고 실행해봅니다. 에러가 없는지 확인합니다.

2. Create the DAO

이 작업에서는 DAO(데이터 액세스 개체)를 정의합니다. Android에서 DAO는 데이터베이스를 삽입, 삭제 및 업데이트하기 위한 편리한 메서드를 제공합니다.

Room 데이터베이스를 사용할 때 코드에서 Kotlin 함수를 정의하고 호출하여 데이터베이스를 쿼리합니다. 이러한 Kotlin 함수는 SQL 쿼리에 매핑됩니다. 주석을 사용하여 DAO에서 이러한 매핑을 정의하면 Room에서 필요한 코드를 생성합니다.

DAO를 데이터베이스에 액세스하기 위한 사용자 정의 인터페이스를 정의하는 것으로 생각하십시오.

일반적인 데이터베이스 작업의 경우 Room 라이브러리는 @Insert, @Delete 및 @Update와 같은 편리한 주석을 제공합니다. 다른 모든 것에는 @Query 주석이 있습니다. SQLite에서 지원하는 모든 쿼리를 작성할 수 있습니다.

Android Studio에서 쿼리를 생성하면 컴파일러가 SQL 쿼리에서 구문 오류를 확인합니다.

sleep nights의 sleep-tracker 데이터베이스의 경우 다음을 수행할 수 있어야 합니다.

  • Insert new nights.
  • Update an existing night to update an end time and a quality rating.
  • Get a specific night based on its key.
  • Get all nights, so you can display them.
  • Get the most recent night.
  • Delete all entries in the database.

2.1. SleepDatabase DAO 생성

2.1.1. 데이터베이스 패키지에서 SleepDatabaseDao.kt를 엽니다.

2.1.2. 인터페이스 SleepDatabaseDao는 @Dao로 주석 처리됩니다. 모든 DAO는 @Dao 키워드로 주석을 달아야 합니다.

@Dao
interface SleepDatabaseDao {}

2.1.3. 인터페이스 본문 내부에 @Insert 주석을 추가합니다. @Insert 아래에 Entity 클래스 SleepNight의 인스턴스를 인수로 사용하는 insert() 함수를 추가합니다.

Room은 SleepNight를 데이터베이스에 삽입하는 데 필요한 모든 코드를 생성합니다. Kotlin 코드에서 insert()를 호출하면 Room은 SQL 쿼리를 실행하여 엔티티를 데이터베이스에 삽입합니다. (참고: 함수를 원하는 대로 호출할 수 있습니다.)

@Insert
fun insert(night: SleepNight)

2.1.4. 하나의 SleepNight에 대해 update() 함수를 사용하여 @Update 주석을 추가합니다. 업데이트되는 엔터티는 전달된 것과 동일한 키를 가진 엔터티입니다. 엔터티의 다른 속성 중 일부 또는 전체를 업데이트할 수 있습니다.

@Update
fun update(night: SleepNight)

나머지 기능에는 편의 주석이 없으므로 @Query 주석을 사용하고 SQLite 쿼리를 제공해야 합니다.

2.1.5. Long 키 인수를 사용하고 nullable SleepNight를 반환하는 get() 함수로 @Query 주석을 추가합니다. 누락된 매개변수에 대한 오류가 표시됩니다.

@Query
fun get(key: Long): SleepNight?

2.1.6. 쿼리는 @Query 주석에 문자열 매개변수로 제공됩니다. 특정 SleepNight 항목에서 모든 열을 검색하는 SQLite 쿼리인 @Query에 String 매개변수를 추가합니다.

  • Select all columns from the daily_sleep_quality_table
  • WHERE the nightId matches the :key argument.

:키를 주목하십시오. 쿼리에서 콜론 표기법을 사용하여 함수의 인수를 참조합니다.

("SELECT * from daily_sleep_quality_table WHERE nightId = :key")

2.1.7. clear() 함수와 SQLite 쿼리가 있는 또 다른 @Query를 추가하여 daily_sleep_quality_table의 모든 항목을 삭제합니다. 이 쿼리는 테이블 자체를 삭제하지 않습니다.

@Delete 주석은 하나의 항목을 삭제하고 @Delete를 사용하고 삭제할 SleepNight 목록을 제공할 수 있습니다. 단점은 테이블에 있는 내용을 가져오거나 알아야 한다는 것입니다. @Delete 주석은 특정 항목을 삭제하는 데 유용하지만 테이블에서 모든 항목을 지우는 데는 효율적이지 않습니다.

@Query("DELETE FROM daily_sleep_quality_table")
fun clear()

2.1.8. getTonight() 함수를 사용하여 @Query를 추가합니다. 함수가 테이블이 비어 있는 경우를 처리할 수 있도록 getTonight()에 의해 반환된 SleepNight를 nullable로 만듭니다. (테이블은 처음에는 비어 있고 데이터가 지워진 후에는 비어 있습니다.)

데이터베이스에서 "오늘밤"을 가져오려면 nightId로 정렬된 결과 목록의 첫 번째 요소를 내림차순으로 반환하는 SQLite 쿼리를 작성하십시오. 하나의 요소만 반환하려면 LIMIT 1을 사용합니다.

@Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC LIMIT 1")
fun getTonight(): SleepNight?

2.1.9. getAllNights() 함수로 @Query를 추가합니다.

  • SQLite 쿼리가 내림차순으로 정렬된 daily_sleep_quality_table의 모든 열을 반환하도록 합니다.
  • getAllNights()가 SleepNight 엔터티 목록을 LiveData로 반환하도록 합니다. Room은 이 LiveData를 업데이트된 상태로 유지하므로 데이터를 한 번만 명시적으로 가져오면 됩니다.
  • androidx.lifecycle.LiveData에서 LiveData를 가져와야 할 수도 있습니다.

2.1.10. 앱을 실행하여 오류가 없는지 확인하세요.

3. Create and test a Room database

이번에는 이전에 생성한 Entity 및 DAO를 사용하는 Room 데이터베이스를 생성합니다.

@Database 주석이 달린 추상 데이터베이스 홀더 클래스를 생성해야 합니다. 이 클래스에는 데이터베이스가 존재하지 않는 경우 데이터베이스 인스턴스를 생성하거나 기존 데이터베이스에 대한 참조를 반환하는 메서드가 있습니다.

Room 데이터베이스를 가져오는 것은 약간 복잡하므로 코드를 시작하기 전의 일반적인 프로세스는 다음과 같습니다.

  • RoomDatabase를 확장하는 공용 추상 클래스를 만듭니다. 이 클래스는 데이터베이스 홀더 역할을 합니다. Room이 구현을 생성하기 때문에 클래스는 추상입니다.

  • @Database로 클래스에 주석을 답니다. 인수에서 데이터베이스의 엔터티를 선언하고 버전 번호를 설정합니다.

  • 컴패니언 개체 내에서 SleepDatabaseDao를 반환하는 추상 메서드 또는 속성을 정의합니다. Room은 당신을 위해 body를 생성할 것입니다.

  • 전체 앱에 대해 하나의 Room 데이터베이스 인스턴스만 필요하므로 RoomDatabase를 싱글톤으로 만드십시오.

  • Room의 데이터베이스 빌더를 사용하여 데이터베이스가 존재하지 않는 경우에만 데이터베이스를 생성하십시오. 그렇지 않으면 기존 데이터베이스를 반환합니다.

팁:
코드는 모든 Room 데이터베이스에 대해 거의 동일하므로 이 코드를 템플릿으로 사용할 수 있습니다.

3.1. Create the database

3.1.1. 데이터베이스 패키지에서 SleepDatabase.kt를 엽니다.

3.1.2. 파일에서 RoomDatabase를 확장하는 SleepDatabase라는 추상 클래스를 만듭니다. (@Database 어노테이션을 포함하는)

@Database()
abstract class SleepDatabase : RoomDatabase() {}

3.1.3. 엔티티 및 버전 매개변수가 누락되었다는 오류가 표시됩니다. @Database 주석에는 Room이 데이터베이스를 빌드할 수 있도록 여러 인수가 필요합니다.

  • 엔티티 인수로 SleepNight를 제공하십시오.
  • 버전을 1로 설정하십시오. 스키마를 변경할 때마다 버전 번호를 늘려야 합니다.
  • 스키마 버전 기록 백업을 유지하지 않도록 exportSchema를 false로 설정합니다.
@Database(entities = [SleepNight::class], version = 1, exportSchema = false)

3.1.4. 데이터베이스는 DAO에 대해 알아야 합니다. 클래스 본문 내에서 SleepDatabaseDao를 반환하는 추상 값을 선언합니다. 여러 DAO를 가질 수 있습니다.

abstract val sleepDatabaseDao: SleepDatabaseDao

3.1.5. 이 추상 값 아래에 컴패니언 개체를 정의합니다. 컴패니언 개체를 사용하면 클라이언트가 클래스를 인스턴스화하지 않고도 데이터베이스를 생성하거나 가져오기 위한 메서드에 액세스할 수 있습니다. 이 클래스의 유일한 목적은 데이터베이스를 제공하는 것이므로 인스턴스화할 이유가 없습니다.

 companion object {}

3.1.6. 컴패니언 개체 내에서 데이터베이스에 대한 비공개 nullable 변수 INSTANCE를 선언하고 null로 초기화합니다. INSTANCE 변수는 생성된 데이터베이스에 대한 참조를 유지합니다. 이렇게 하면 계산 비용이 많이 드는 데이터베이스에 대한 연결을 반복적으로 여는 것을 방지할 수 있습니다.

@Volatile로 INSTANCE에 주석을 답니다. 휘발성 변수의 값은 캐시되지 않으며 모든 쓰기 및 읽기는 주 메모리에서 수행됩니다. 이렇게 하면 INSTANCE 값이 항상 최신 상태이고 모든 실행 스레드에서 동일한지 확인하는 데 도움이 됩니다. 즉, 한 스레드에서 INSTANCE에 대해 변경한 내용을 즉시 다른 모든 스레드에서 볼 수 있으며 두 개의 스레드가 각각 캐시에서 동일한 엔터티를 업데이트하여 문제가 발생하는 상황이 발생하지 않습니다.

@Volatile
private var INSTANCE: SleepDatabase? = null

3.1.7. INSTANCE 아래의 컴패니언 객체 내부에서 데이터베이스 빌더가 필요로 하는 Context 매개변수를 사용하여 getInstance() 메소드를 정의합니다. SleepDatabase 유형을 반환합니다. getInstance()가 아직 아무 것도 반환하지 않기 때문에 오류가 표시됩니다.

fun getInstance(context: Context): SleepDatabase {}

3.1.8. getInstance() 내부에서 동기화된{} 블록을 추가합니다. 컨텍스트에 액세스할 수 있도록 이것을 전달하십시오.

여러 스레드가 동시에 데이터베이스 인스턴스를 요청할 수 있으므로 하나가 아닌 두 개의 데이터베이스가 생성됩니다. 이 문제는 이 샘플 앱에서는 발생할 가능성이 없지만 더 복잡한 앱에서는 가능합니다. 데이터베이스를 동기화하기 위해 코드를 래핑한다는 것은 한 번에 하나의 실행 스레드만 이 코드 블록에 들어갈 수 있다는 것을 의미하며, 이는 데이터베이스가 한 번만 초기화되도록 합니다.

synchronized(this) {}

3.1.9. 동기화된 블록 내에서 INSTANCE의 현재 값을 로컬 변수 인스턴스에 복사합니다. 이는 로컬 변수에서만 사용할 수 있는 Kotlin의 스마트 캐스트 기능을 활용하기 위한 것입니다.

var instance = INSTANCE

3.1.10 동기화된 블록 내에서 동기화된 블록의 끝에서 인스턴스를 반환합니다. 반환 유형 불일치 오류를 무시하십시오. 완료되면 null을 반환하지 않습니다.

return instance

3.1.11 return 문 위에 if 문을 추가하여 instance가 null인지, 즉 아직 데이터베이스가 없는지 확인합니다.

if (instance == null) {}

3.1.12 인스턴스가 null인 경우 데이터베이스 빌더를 사용하여 데이터베이스를 가져옵니다. if 문의 본문에서 Room.databaseBuilder를 호출하고 전달한 컨텍스트, 데이터베이스 클래스 및 데이터베이스 이름 sleep_history_database를 제공합니다.

instance = Room.databaseBuilder(
                           context.applicationContext,
                           SleepDatabase::class.java,
                           "sleep_history_database")

Android Studio는 유형 불일치 오류를 생성합니다. 이 오류를 제거하려면 다음 단계에서 마이그레이션 전략과 build()를 추가해야 합니다.

3.1.13 빌더에 필요한 마이그레이션 전략을 추가하십시오. .fallbackToDestructiveMigration()을 사용합니다.

일반적으로 스키마가 변경될 때 마이그레이션 전략을 마이그레이션 개체에 제공해야 합니다. 마이그레이션 개체는 데이터가 손실되지 않도록 이전 스키마의 모든 행을 새 스키마의 행으로 변환하는 방법을 정의하는 개체입니다. 마이그레이션은 이 코드랩의 범위를 벗어납니다. 간단한 해결책은 데이터베이스를 파괴하고 재구축하는 것입니다. 즉, 데이터가 손실됩니다.

.fallbackToDestructiveMigration()

3.1.14 마지막으로 .build()를 호출합니다. 이렇게 하면 Android Studio 오류가 제거됩니다.

.build()

3.1.15 INSTANCE = instance를 if 문 내부의 마지막 단계로 지정합니다.

INSTANCE = instance

3.1.16 최종 코드는 다음과 같아야 합니다.

@Database(entities = [SleepNight::class], version = 1, exportSchema = false)
abstract class SleepDatabase : RoomDatabase() {

   abstract val sleepDatabaseDao: SleepDatabaseDao

   companion object {

       @Volatile
       private var INSTANCE: SleepDatabase? = null

       fun getInstance(context: Context): SleepDatabase {
           synchronized(this) {
               var instance = INSTANCE

               if (instance == null) {
                   instance = Room.databaseBuilder(
                           context.applicationContext,
                           SleepDatabase::class.java,
                           "sleep_history_database"
                   )
                           .fallbackToDestructiveMigration()
                           .build()
                   INSTANCE = instance
               }
               return instance
           }
       }
   }
}

3.1.17 앱을 빌드하고 실행해보세요.

이제 Room 데이터베이스 작업을 위한 모든 빌딩 블록이 있습니다. 이 코드는 컴파일되고 실행되지만 실제로 작동하는지 알 수 있는 방법이 없습니다. 따라서 몇 가지 기본 테스트를 추가해야합니다.

3.2. SleepDatabase 테스트

이 단계에서는 제공된 테스트를 실행하여 데이터베이스가 작동하는지 확인합니다. 이렇게 하면 데이터베이스를 구축하기 전에 데이터베이스가 작동하는지 확인하는 데 도움이 됩니다. 제공된 테스트는 기본입니다. 프로덕션 앱의 경우 모든 DAO에서 모든 기능과 쿼리를 실행합니다.

앱에는 androidTest 폴더가 있습니다. 이 androidTest 폴더에는 테스트에 Android 프레임워크가 필요하므로 실제 또는 가상 장치에서 테스트를 실행해야 한다고 말하는 멋진 방법인 Android 계측과 관련된 단위 테스트가 포함되어 있습니다. 물론 Android 프레임워크를 포함하지 않는 순수 단위 테스트를 만들고 실행할 수도 있습니다.

3.2.1. Android 스튜디오의 androidTest 폴더에서 SleepDatabaseTest 파일을 엽니다.

3.2.2. 코드의 주석 처리를 해제하려면 주석 처리된 코드를 모두 선택하고 Cmd+/ 또는 Control+/ 키보드 단축키를 누릅니다.

3.3.3. 파일을 살펴보십시오.

테스트 코드에 대한 간략한 설명입니다.

  • SleepDatabaseTest는 테스트 클래스입니다.
  • @RunWith 주석은 테스트를 설정하고 실행하는 프로그램인 테스트 실행기를 식별합니다.
  • 설정하는 동안 @Before 주석이 달린 함수가 실행되고 SleepDatabaseDao를 사용하여 메모리 내 SleepDatabase를 생성합니다. "In-memory"는 이 데이터베이스가 파일 시스템에 저장되지 않고 테스트 실행 후 삭제됨을 의미합니다.
  • 또한 메모리 내 데이터베이스를 구축할 때 코드는 또 다른 테스트 전용 메서드인 allowMainThreadQueries를 호출합니다. 기본적으로 기본 스레드에서 쿼리를 실행하려고 하면 오류가 발생합니다. 이 방법을 사용하면 테스트 중에만 수행해야 하는 기본 스레드에서 테스트를 실행할 수 있습니다.
  • @Test로 주석이 달린 테스트 메소드에서 SleepNight를 생성, 삽입 및 검색하고 잘 작동하는지 확인합니다. 문제가 발생하면 예외를 throw합니다. 실제 테스트에서는 여러 @Test 메서드가 있습니다.
  • 테스트가 완료되면 @After 어노테이션이 붙은 함수가 실행되어 데이터베이스를 닫습니다.

3.3.4. 프로젝트 창에서 테스트 파일을 마우스 오른쪽 버튼으로 클릭하고 'SleepDatabaseTest' 실행을 선택합니다.

3.3.5. 테스트를 실행한 후 SleepDatabaseTest 창에서 모든 테스트가 통과했는지 확인합니다.

m1은 room라이브러리와 잘 호환이 안되므로 build.gradle(Module)에
kapt 'org.xerial:sqlite-jdbc:3.34.0'를 추가하면 정상적으로 작동합니다.

모든 테스트를 통과했으므로 이제 몇 가지 사항을 알게 되었습니다.

  • 데이터베이스가 올바르게 생성됩니다.
  • 데이터베이스에 SleepNight를 삽입할 수 있습니다.
  • SleepNight를 다시 받을 수 있습니다.
  • SleepNight는 sleep-quality에 대한 올바른 값을 가지고 있습니다.

Summary

  • @Entity로 주석이 달린 데이터 클래스로 테이블을 정의하십시오. @ColumnInfo로 주석이 달린 속성을 테이블의 열로 정의합니다.

  • @Dao 주석이 달린 인터페이스로 DAO(Data Access Object)를 정의합니다. DAO는 Kotlin 함수를 데이터베이스 쿼리에 매핑합니다.

  • 주석을 사용하여 @Insert, @Delete 및 @Update 함수를 정의합니다.

  • 다른 쿼리에 대한 매개변수로 SQLite 쿼리 문자열과 함께 @Query 주석을 사용합니다.

  • 데이터베이스를 반환하는 getInstance() 함수가 있는 추상 클래스를 만듭니다.

  • 테스트를 사용하여 데이터베이스와 DAO가 예상대로 작동하는지 테스트하십시오. 제공된 테스트를 템플릿으로 사용할 수 있습니다.

profile
수신제가치국평천하

0개의 댓글