마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

Room을 이용해서 DB를 사용하려는데 데이터를 어떻게 넣어야할지 모르겠습니다.

0 추천

https://ibb.co/gZGL9nc

https://ibb.co/QfgQZFF

링크의 사진처럼 각 부위별로 데이터베이스에서 데이터를 받아와 뿌려주려고하는데요, 

로컬이던 서버든 뭐든 일단 뭐라도 사용해보고자해서 로컬 DB를 사용하기로 했고, Room을 이용하고 있습니다.

샘플 코드를 뒤적거리면서 따라하고는 있는데..DB에 데이터를 이제 어떻게 넣어야할지 ViewModel 클래스에서 막혔는데요.

1.엔티티 설계도 부위별로 프로퍼티를 모두 선언해주는게 맞는지..

2.탭에 보이는 데이터가 속성(애트리뷰트) 별로 보여지기 때문에 이것을 속성 별로 저장해야하는데,
어떻게 해야하는지.

3.마찬가지로 속성별로 보여지기때문에 속성별로 데이터를 가져와야하는데 DAO의 쿼리문을 건드리면 되는지..

4. ViewModel 클래스에서 Repository 클래스를 이용해서 데이터를 가져오는것이니까, repository는 미리 어디선가 string 리소스 파일에서 데이터를 가져와야할것같은데.. 그부분이 어디가 되어야할지 모르겠어요..

조언좀 부탁드립니다.

Entity 클래스

@Entity
data class Workout(
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    val chest: String,
    val back: String,
    val leg: String,
    val shoulder: String,
    val biceps: String,
    val triceps: String,
    val abs: String
)

WokroutDatabase

@Database(entities = [Workout::class], version = 1)
abstract class WorkoutDatabase : RoomDatabase() {
    abstract fun workoutDao() : WorkoutDao

    companion object {
        private var INSTANCE: WorkoutDatabase? = null

        fun getInstance(context: Context): WorkoutDatabase =
            INSTANCE ?: synchronized(this) {
                INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
            }

        private fun buildDatabase(context: Context) =
            Room.databaseBuilder(context.applicationContext,
                WorkoutDatabase::class.java, "Workout.db")
                .build()
    }
}

DAO

@Dao // Data Access Object
interface WorkoutDao {
    @Query("SELECT * FROM Workout") // Workout 테이블에서 모든 값을 가져옴
    abstract fun getAll(): List<Workout>

    @Insert
    abstract fun insert(workout: Workout)

    @Update
    abstract fun update(workout: Workout)

    @Delete
    abstract fun delete(workout: Workout)
}

Repository

class WorkoutRepository(application: Application) {
    private val workoutDB: WorkoutDatabase = WorkoutDatabase.getInstance(application)
    private val workoutDao: WorkoutDao = workoutDB.workoutDao()
    private val workout: List<Workout> = workoutDao.getAll()

    fun getAll() : List<Workout> = workout
    fun insert(workout: Workout) {
        workoutDao.insert(workout)
    }
    fun delete(workout: Workout) {
        workoutDao.delete(workout)
    }
}

ViewModel (막힘....)

class WorkoutListViewModel(application: Application) : AndroidViewModel(application){
    private var _part :MutableLiveData<String> = MutableLiveData()
    private var _list : MutableLiveData<List<String>> = MutableLiveData(arrayListOf())
    private val resources = application.resources
    private val repo : WorkoutRepository = WorkoutRepository(application)
    private val workout = repo.getAll()

    private val workoutListSource : WorkoutListSource by lazy { WorkoutListLocalSource(resources) }

    val list = _list
    val part = _part

    fun setList(part : String) {
        _part.value = part
//        _list.value = workoutListSource.getWorkoutListByPart(BodyType.valueOf(part))
        when(_part.value) {
            "가슴" -> _list.value = workoutListSource.getWorkoutListByPart(BodyType.CHEST)
            "등" -> _list.value = workoutListSource.getWorkoutListByPart(BodyType.BACK)
            "하체" -> _list.value = workoutListSource.getWorkoutListByPart(BodyType.LEG)
            "어깨" -> _list.value = workoutListSource.getWorkoutListByPart(BodyType.SHOULDER)
            "이두" -> _list.value = workoutListSource.getWorkoutListByPart(BodyType.BICEPS)
            "삼두" -> _list.value = workoutListSource.getWorkoutListByPart(BodyType.TRICEPS)
            "복근" -> _list.value = workoutListSource.getWorkoutListByPart(BodyType.ABS)
        }
    }

    fun getList() : List<String> = list.value!!
}

 

codeslave (3,940 포인트) 님이 2021년 8월 11일 질문
codeslave님이 2021년 8월 11일 수정

3개의 답변

0 추천

화면에 보이는 것과 DB에 들어가는 데이터구조는 정확하게 일치하지 않을 경우가 대부분입니다.  그리고 DB 에 들어가는 데이터(entity)는 Key값(Room은  보통 ID필드)을 통해 연결됩니다.

우선 앱에 필요한 데이터가 단위별로 뭕지 축출합니다.

가슴, 등, 하체, 어깨, 이두, 삼두 - 운동부위
덤벨풀오버, 덤벨프레스... - 운동부위별 운동종목
운동부위별 운동종목에 대한 운동기록

위의 각각은 테이블이 될겁니다. Room에서는  @Entity 를 붙인 데이터 클래스에 해당하구요.

일단 위의 세개 테이블에 이름을 부여할게요. (더 적절한 이름을 찾아보세요.)
MuscleGroup - 운동부위별 운동종목
Exercise - 운동부위별 운동종목
Logbook - 운동부위별 운동종목에 대한 운동기록

Exercise에는 Foreign key로 MuscleGroup Id가 필요합니다.
Logbook에는 MuscleGroup Id와 Exercise Id가 필요한데 Exercise에 이미 MuscleGroup Id가 존재하므로, 생략해도 될 것같습니다.

@Entity
data class MuscleGroupEntity(
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    val name: String
)

@Entity
data class ExerciseEntity (
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    val muscleGroupId: Int
)

@Entity
data class LogbookEntity(
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    val date: Date,
    ...
    val exerciseId: Int
)

 

위와 비슷한 구조가 될 겁니다. 화면에서는 운동부위 탭을 클릭할 때 (탭이 어떤 BodyPartEntity의 ID인지 View단에서 매핑이 필요합니다.) ExerciseTypeEntity에서 동일한 ID를 가지고 오도록 쿼리를 하면 됩니다.

ViewModel에 MuscleGroupEntity에서 getAll을 하셔서 List<MuscleGroupEntity>가져오신 다음 이걸 기반으로 탭을 만드시고, 탭을 누르면, position을 알 수 있을 겁니다. 그러면 viewModel로 어떤 위치의 탭이 눌렸는지 보내면, viewModel에서는 위치에 해당하는 MuscleGroupEntity를 알 수가 있겠죠. 그럼 MuscleGroupEntity 의 ID 도 찾을 수 있고 이 ID 를 가지고 ExcerciseTypeEntity에서 동일한 bodyPartId만 쿼리를 해오시면 됩니다.

class WorkoutListViewModel ... {
    // 저같은 경우는 DB에 사용되는 data class와 ViewModel에 사용하는 data class를 구분해서 사용합니다.
    // 그래서 클래스 이름이 살짝 다릅니다.
    private val _muscleGroupsLiveData = MutableLiveData<List<MuscleGroup>>()
    val muscleGroupsLiveData: LiveData<List<MuscleGroup>> get() = _muscleGroupsLiveData

    private val _excercisesLiveData = MutableLiveData<List<Exercise>>()
    val excercisesLiveData: LiveData<List<Exercise>> get() =  _excercisesLiveData
   
    fun fetchMuscleGroupList() {
          viewModelScope.launch {
               val muscleGroupList = getMuscleGroupList()
               
                //Entity를 View용 데이터 타입으로 변환(생략)
                _muscleGroupsLiveData.postValue(muscleGroupList)
          }
          
    }

    fun tabSelected(int position) {
          viewModelScope.launch {
              val muscleGroup = muscleGroupsLiveData.value?.getOrNull(position) ?: throw NoSuchElementException("$position에 해당하는 데이터를 찾을 수 없음.");
              val exerciseList = getExerciseList(muscleGroup.id)    
              _excercisesLiveData.postValue(exerciseList)     
          }
    }
}

 

spark (224,800 포인트) 님이 2021년 8월 11일 답변
spark님이 2021년 8월 11일 수정
MuscleGroup을 운동부위별 운동종목으로 표시하셨던데 운동부위를 잘못표시하신거죠?
아무튼, MuscleGroup 엔티티(테이블)도 만드셨는데 이건 단순히 탭의 이름을 지정해주기 위한 테이블인가요? 생각해보니 저는 현재 앱에서 대충 리스트만 만들어서 탭의 이름을 지정해주고 있었고 DB에서는 운동목록에 대해서만 생각하고 있었거든요.
만약 이게 아니라면 굳이 운동 부위에대한 테이블을 따로 만들어주기보다는
그냥 운동에 대한 테이블을 하나만 만들고 속성들은 각부위 이름으로 지정하고
속성값들을 운동리스트들로 채우면 안되는건가요?
제가 본문에서 작성한 엔티티처럼요..

추가로 결국에는 DB에는 값을 넣어줘야하는것이니까.. string 파일에 있는 값을 가져오는 로직을 사용할수 밖에 없죠? DB에 넣어주는 코드는 선생님 뷰모델 코드에서는 안보이는데 repository에서 리소스파일의 값들을 가져오는것은 context나 application값을 사용못하니까 불가능할것같고.. viewmodel에서 해야할까요
아 그리고 코드를 읽어보는데 LiveData로 설정하셨는데 이유가 있을까요?
단순히 뷰에 운동리스트들을 뿌려주는거라 리스트가 추가 및 삭제는 될수 있을지언정 실시간으로 이름이 바뀌거나 하지는 않을것같은데, 이유가 무엇인지 궁금해요
데이터가  DB에서 오기 때문이죠. 뷰는 DB에서 바로 데이터를 읽으면 안되고 ViewModel에게 요청을 해서 ViewModel이 데이터를 가져와서 View에게 전달을 해줘야 합니다. 이렇게 해야 서로의 역할이 분명해 지겠죠. ViewModel은 View에 필요한 데이터를 가져와 View가 사용할 수 있도록 가공해줘야 합니다. 이게 MVVM 의 데이터 흐름이예요. 이걸 안지키면 더이상 MVVM이 아니예요.
0 추천

그리고 Repository가 좀 이상해요.

// Repository는  Domain layer에 해당합니다. 안드로이드 관련 클래스 Application을 참조하면 안됩니다.
// 생성자에 WorkoutDatabase를 대신 넘겨받으세요. 인터페이스를 사용하시면 더 좋습니다.
class WorkoutRepository( private val workoutDB: WorkoutDatabase) { 
    private val workoutDao: WorkoutDao by lazy { workoutDB.workoutDao() }

     // 여기처럼 데이터를 생성시에 가져오면 DB 에 데이터가 추가/수정/삭제 되었을 때 변경된 내용이 반영되지 않은 수 있어요.
    // 왜냐하면 대부분 Repository 는 앱 내에서 인스턴스가 하나만 존재하는 Singleton 을 사용하기 때문이예요.
    private val workout: List<Workout> = workoutDao.getAll()  //잘못 됨.
 
    fun getAll() : List<Workout> = withContext(Dispatchers.IO) {
       workoutDao.getAll()
    }

    suspend fun insert(workout: Workout) = withContext(Dispatchers.IO) {
        workoutDao.insert(workout)
    }

    suspen fun delete(workout: Workout)  = withContext(Dispatchers.IO) {
        workoutDao.delete(workout)
    }
}

그리고 API호출이나 DB 관련 호출은 Corouine을 사용해서 처리하세요. Repository에 있는 함수들은  suspend 를 붙여주고 withContext(Dispatchers.IO)로 백그라운드 스레드에서 실행될 수 있도록 하시고, ViewModel에서는 ViewModelScope.launch 안에서  처리하세요.

*중요한 부분인데, 예외 처리가 하나도 안보이네요. 예외는 언제나 일어날 수 있기 때문에, 예외 처리를 항상 염두에 두세요.

spark (224,800 포인트) 님이 2021년 8월 11일 답변
그럼 viewmodel 클래스에서 WorkoutDatabase를 getInstance 를 이용해서 생성하고 이 생성한 프로퍼티를 넘겨주면 될까요?
0 추천

자바라면 그렇게 하면 되는데, 코틀린은 object클래스만 쓰면 Singleton인스턴스를 만들어 줘요.

object WorkoutRepository  {

}

근데 object은 생성자 파라미터를 가지지 않기 때문에 멤버변수를 이용하시던가, 아니면
Dependency를 관리하는 클래스를 하나 만들고(좀 더 나은 방법으로 생각함) 거기에서 필요한 인스턴스를 만들어 줄 수도 있어요. 예를 들면,

class AppModule(private val context: Context) {
    
   val workoutRepository by lazy {
             provideWorkoutRepository(workoutDao)
    }
   
    private val workoutDao by lazy {
          provideWorkoutDao(provideWorkoutDatabase(context))
    }

    private fun workRepository(workoutDao: WorkoutDao: ): WorkRepositoy {
         reurn provideWorkoutRepository(workoutDao)
    }

    privatefun provideWorkoutDatabase(context: Context): WorkoutDatabase {
         return WorkoutDatabase.getInstance(context)
    }

    private fun provideWorkoutDao(workoutDB: WorkoutDatabase ): WorkoutDao {
          return workoutDB.workoutDao()
   }
}

class WorkoutApp : Application {
      privat lateint var _appModule: AppModule
      val appModule: AppModule get() = _appModule
      
      override onCreate() {
         super.onCreate()
         _appModule = AppModule(this)
      }
}

 

이런 식으로 Application클래스를 상속받아서 AndroidMenifest.xml에 등록한 다음 BaseActivity나 BaseFragment를 만들어서 AppModule을 가져오도록 합니다.

abstract class BaseActivtiy : AppCompatActivity {
    protect lateinit var appModuel: AppModule

     override onCreate(...) {
         initializeDependencies();
         super.onCreate(...)
     }

    private fun initializeDependencies() {
        appModule = (application as WorkoutApp).appModule
    }
}

class MainActivity : BaseActivity {
   
} 

이런 식으로 BaseActivtiy 를 상속하면 모든 액티비티에서 쉽게 AppModule를 가져다 쓸 수 있습니다. 더 나아가서는 BaseActivity의 inject하는 부분도 별도의 클래스로 옮겨서 처리할 수 있습니다.

class Injector (private val context: Context) {
    private val appModule: AppModule by lazy {
          AppModule(context)
    }

    fun inject(activity: MainActivity) {
          ...
    }
}

 

물론, 이부분이 제대로 동작하려면 추가적인 코드가 좀 필요합니다. 이런 방식은 Dagger 나 Hilt 같은 Dependency Injection 라이브러리가 내부적으로하는 동작이예요. 그리고 실제업무에서는 Dagger나 Hilt같은 라이브러리를 사용해서 Depedency 를 관리해 줍니다. 하지만 이런 라이브러리 없이 하신다면 앞에서 말씀드린 방법으로 하시면 됩니다. 참 그리고 ViewModel의 생성자에 context를 넘기는 건 일반적으로 사용하지 않습니다. (안좋은 코드 중의 하나임). 그리고 생성자가의 인자가 ApplicationContext가 아닌는 ViewModelFactory 를 통해서 ViewModel 인스턴스를 제공하셔야 합니다.

spark (224,800 포인트) 님이 2021년 8월 12일 답변
하하 감사합니다... 생각 보다 넘 어렵네요ㅋㅋ 잘될지 의문이지만..시도해보겠습니다
참, DB에 데이터를 초기화하는 부분이 부담스러우시면 생각하신 대로 string resource를 병행하시는 형태로 진행을 하세요. 그리고 Room같은 경우는 앱설치시에 바로 같이 배포할 수도 있어요. 그리고 별도의 DB를 초기화하는 로직이 필요하시다면 Application클래스를 상속받아서 그 안에서 onCreate 메소드에서 DB초기화를 수행하는 로직을 집어넣으세요. 대신 작업이 너무 무거우면 곤란하겠죠.
...