안드로이드 앱개발 - 심화 1

kkomin·2023년 9월 12일
0

Android Studio

목록 보기
28/44

데이터 영구적으로 저장

데이터를 안드로이드인 앱 내에서 뭔가 데이터를 사용하고 날아가는 것이 아닌, 앱을 종료하거나 핸드폰을 재시작해도 데이터가 저장되어 있도록 하기위한 방법은 세가지가 존재한다.

  • SharedPreferences
  • DB로 데이터 저장
  • 파일 형태로 저장

SharedPreferences

간단하게 저장할 수 있는 정보, 예를 들어 자동로그인을 할 것인지에 대한 정보가 폰 안에 저장되어 있어야 앱 실행 시 자동 저장 여부를 불러와 자동 로그인에 대한 정보를 Preference라는 것을 통해서 저장한다. XML 포멧으로 텍스트 파일에 키-값 세트로 정보를 영구적으로 저장하는 특징이 있다. (알람-true / 알람-false)

응용프로그램 내의 Activity간에 공유하며, 한쪽 Activity에서 수정하면 다른 Activity에서도 수정된 값을 읽을 수 있다 !

👉 고유 정보로 외부에서는 읽는 것은 불가능


사용방법

- getSharedPreferences

여러가지 데이터를 한꺼번에 저장할 경우 사용한다. 인자로는 name과 mode가 있다. name은 preference 데이터를 저장할 XML 파일의 이름이고, mode는 파일의 공유 모드를 설정해주면 된다.

  • MODE_PRIVATE : 생성된 XML 파일은 외부에서 읽기 쓰기 ❌ (내부에서만 사용!!)
  • MODE_WORLD_READBLE, MODE_WORLD_WRITEABLE : 보안상 문제로 더 이상 사용 ❌
  val sharedPref = activity?.getSharedPreferences(
      getString(R.string.preference_file_key), Context.MODE_PRIVATE)

- getPreferences

한 개의 데이터를 저장할 경우 사용한다. 인자로는 mode만 필요하며, 생성한 activtiy의 전용이기 때문에 같은 패키지 내의 다른 activity를 읽는 것은 불가능하다.

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

예시

실행했을 때 android!!studio!! 를 입력하고 데이터 저장 버튼을 누른 뒤, 재실행하면 EditText에 그대로 android!!studio!!가 나오는걸 확인할 수 있다.

data 폴더 안의 data 폴더 안의 현재 생성한 프로젝트의 폴더안의 shared_prefs를 누르고 pref.xml을 확인해보면 안에 입력한 텍스트가 저장된 것을 확인할 수 있다.


DB로 데이터 저장 - Room

가볍게 만드는 DBSQLite를 쉽게 사용할 수 있는 DB의 객체 Mapping 라이브러리로, Query의 경우에도 직접 작성하지 않아도 쉽게 만들어준다는 특징이 있다.
Query 결과를 LivedData로 하여 DB가 변경될 때마다 쉽게 UI를 변경할 수 있다!

Room의 3요소

  • @Database : 클래스를 DB로 지정하는 annotation, RoomDatabase를 상속받은 클래스여야 하고, Room.databaseBuilder를 이용해서 인스턴스를 생성한다.

  • @Entity : 클래스를 테이블 스키마로 지정하는 annotation

  • @Dao : 클래스를 DAO(Data Access Object)로 지정하는 annotation

Room을 만들 때, 이 3요소는 꼭 필요하다!


사용방법

1. gradle에 파일 설정

여기 안드로이드 공식 문서를 접속해서 Room의 최신화 버전을 체크하고 사용할 것!

  plugins {
      id 'kotlin-kapt'
  }
  dependencies {
      // room_version은 공식문서에서 항상 최신화 버전으로 사용
      val room_version = "2.5.0"

      implementation"androidx.room:room-runtime:$room_version"
      annotationProcessor "androidx.room:room-compiler:$room_version"
      kapt "androidx.room:room-compiler:$room_version"
      implementation "androidx.room:room-guava:$room_version"
      testImplementation "androidx.room:room-testing:$room_version"
  }

2. Entity 생성

Entity는 아까정리했던대로 테이블 스키마를 정의해줄 것이다.

CREATE TABLE student_table(student_id INTEGER PRIMARY KEY, name TEXT NOT NULL);

여기서 student_idINTEGER PRIMARY KEY로 설정할건데 PRIMARY KEY유니크한 값으로 중복되지 않는 값을 말하므로 INTEGER PRIMARY KEY는 중복되지 않는 정수형 값이라 생각하면 되고, name에는 TEXT NOT NULLnull이 들어갈 수 없다는 뜻이다.

위 스키마 정의를 Entity 데이터 클래스로 만들면 다음과 같다.

// 테이블 이름 : student_table로 지정
@Entity(tableName = "student_table")
data class Student {
    @PrimaryKey
    @ColumnInfo(name = "student_id")
    val id : Int,
    val name : String
}

3. DAO 생성

DAO는 interfaceabstract class로 정의해야하며 Annotation에는 SQL쿼리를 정의하고 이 쿼리를 위한 메소드를 선언해줘야 한다. annotation으로는 @Insert, @Update, @Delete,@Query가 존재한다.

@Query("SELECT * from table") fun getAllData() : List<Data>

@Insert, @Update, @Delete

@Insert, @Update, @Delete는 SQL 쿼리를 따로 작성하지 않아도 컴파일러가 자동으로 생성해주며, @Insert, @Update는 key가 중복되는 경우의 처리를 위해 onConflict를 지정할수 있다는 특징을 가지고 있다.

즉, @Query를 제외한 annotation은 쿼리를 따로 작성하지 않아도 자동 생성된다!

😗 여기서 onConlict를 지정하는건 어떻게 하는거지 ?

  • OnConflictStrategy.ABORT : key 충돌 시 종료

  • OnConflictStrategy.IGNORE : key 충돌 무시

  • OnConflictStrategy.REPLACE : key 충돌 시 새로운 데이터로 변경

@Update@Delete는 고유키값인 primary key에 해당되는 tuple(셀 수 있는 수량의 순서 있는 열거)을 찾아서 변경하거나 삭제한다.

@Query

@Query로 리턴되는 데이터의 타입을 LiveData<>로 하면, 이후에 이 데이터가 업데이트될 때 메인쪽에서 Obsserver를 걸어두면 업데이트 된 시점을 따로 호출하지 않아도 자동으로 업데이트 되는 것을 이용할 수 있다. 참고로, LiveData비동기적으로 동작하기 때문에 coroutine으로 할 필요가 없다.

@Query("SELECT * from table") fun getAllData() : LiveData<List<Data>>

@Query에서 SQL을 정의할 때 메소드의 인자를 사용할 수도 있다. @Query 부분은 내 주전공인 빅데이터 전공 수업을 들을 때 SQL을 조금 사용해봐서 익숙한 편인 것 같다.

@Query("SELECT * FROM student_table WHERE name = ":name")
supsend fun getStudentBYname($name : String) : List<Student>

여기서 suspend를 붙이는 이유는 동시실행을 위한 coroutine 사용때문이다.

  @Dao
  interface MyDAO {
      @Insert(onConflict = OnConflictStratgy.REAPLCE) // insert 충돌 시 데이터 교체
      @Query("SELECT * FROM student_table")
      fun getAllStudents() : LiveData<List<Student>>

      @Query("SELECT * FROM student_table WHERE name = ":name")
      suspend fun getStudentByName(saname:String) : List<Student>

      @Delete
      suspend fun deleteStudent(student : Student);
  }

4. Database 생성

RoomDatabase를 상속해 자신의 Room 클래스를 생성하고 포함되는 Entity들과 DB 버전@Database annotation에 지정해준다.

DB 버전 ? 그게 뭔데?

예를 들어, 테이블을 하나 만들어서 데이터베이스가 설치되는 앱을 하나 생성했는데, 사용 중 추가개발이 필요해 테이블을 하나 추가해야 할 경우, 테이블이 추가된 채로 앱을 업데이트 시키게 되면 "기존에 깔려있던 앱의 데이터베이스는 업데이트가 되지 않아" 에러가 발생한다.

데이터베이스의 버전 관리가 중요하다 !

Migration

기존에 깔려있던 앱에 Migration을 통해 테이블을 추가 및 변경 등 가능하다.

RoomDatabase 객체의 addMigrations()를 통해 설정 가능

DB 생성방법

  @Database(entities = [Student::class, ClassInfo::class, Enrollment::class, Teacher::class], version = 1)
  abstract class MyDatabase : RoomDatabase() {
      abstract fun getMyDao() : MyDAO

      companion object {
          private var INSTANCE: MyDatabase? = null
          private val MIGRATION_1_2 = object : Migration(1, 2) {
              override fun migrate(database: SupportSQLiteDatabase) { 생략 }
          }

          private val MIGRATION_2_3 = object : Migration(2, 3) {
              override fun migrate(database: SupportSQLiteDatabase) { 생략 }
          }
          fun getDatabase(context: Context) : MyDatabase {
              if (INSTANCE == null) {
                  INSTANCE = Room.databaseBuilder(
                      context, MyDatabase::class.java, "school_database")
                      .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
                      .build()
              }
              return INSTANCE as MyDatabase
          }
      }
  }

Migration 생성방법

Migration은 여러개를 지정해 줄 수 있다.

MIGRATION(1,2)는 1부터 2까지의 Migration을 뜻하고, MIGRATION(2,3)은 2부터 3까지의 Migration을 뜻한다.

  Room.databaseBuilder(...).addMigrations(MIGRATION_1_2, MIGRATION_2_3)

  private val MIGRATION_1_2 = object : Migration(1, 2) {   // version 1 -> 2
      override fun migrate(database: SupportSQLiteDatabase) {
          database.execSQL("ALTER TABLE student_table ADD COLUMN last_update INTEGER")
      }
  }

  private val MIGRATION_2_3 = object : Migration(2, 3) {   // version 2 -> 3
      override fun migrate(database: SupportSQLiteDatabase) {
          database.execSQL("ALTER TABLE class_table ADD COLUMN last_update INTEGER")
      }
  }

5. UI 연결

Repository와 ViewModel 사용을 권장하며, RoomDatabase 객체에게 DAO 객체를 받아오고, 이 DAO 객체의 메소드를 호출해서 DB에 접근한다.

  myDao = MyDatabase.getDatabase(this).getMyDao()
  runBlocking { // (주의) UI를 블록할 수 있는 DAO 메소드를 UI 스레드에서 바로 호출하면 안됨
      myDao.insertStudent(Student(1, "james"))  // suspend 지정되어 있음
  }
  val allStudents = myDao.getAllStudents() // LiveData는 Observer를 통해 비동기적으로 데이터를 가져옴

UI 쓰레드에서의 네트워킹이나 DB를 직접 호출이 불가능하기 때문에 Coroutine을 사용하므로 runBlocking으로 걸어두고 insertStudent를 이용해 Student 객체를 하나 생성해 insert를 해준다. 또한, Observer를 통해 실시간으로 변동된 사항을 확인할 수 있다.

UI 연결 - LiveData

LiveData<> 타입으로 리턴되는 DAO 메소드의 경우, Observer 메소드를 통해 데이터가 변경하고 있는지 실시간으로 관찰이 가능하고, 데이터의 관찰이 있을 경우 observe가 호출되어 화면에 업데이트 된다.

  val allStudents = myDao.getAllStudents()
  allStudents.observe(this) {   
  // Observer::onChanged() 는 SAM 이기 때문에 lambda로 대체
      val str = StringBuilder().apply {
              for ((id, name) in it) {
                  append(id)
                  append("-")
                  append(name)
                  append("\n")
              }
          }.toString()
      binding.textStudentList.text = str
  }
profile
소소한 코딩 일기

0개의 댓글