본문 바로가기

안드로이드 앱개발

Android Room 사용하기

반응형

안녕하세요. 오늘은 Jetpack Compose에서의 Room에 대하여 작성해보겠습니다.

 

참조 문서(android developer 공식 문서)

https://developer.android.com/codelabs/basic-android-kotlin-training-persisting-data-room?hl=ko#0

 

Room을 사용하여 데이터 유지  |  Android Developers

Android Kotlin 앱에서 Room을 사용하는 방법을 알아보세요. Room은 Android Jetpack의 일부인 지속성 데이터베이스 라이브러리로, SQLite 위에 있는 추상화 레이어입니다. Room은 데이터베이스를 설정하고 구

developer.android.com

 

1. Room이란

1) Roomd의 개념

Android의 Room은 데이터베이스와 상호작용하기 위한 Jetpack 라이브러리로, 

SQLite의 추상화 계층을 제공하여 복잡한 SQLite API 사용 없이 간편하게 데이터베이스 작업을 수행할 수 있도록 해줍니다. 
 Room은 Android의 공식 데이터베이스 라이브러리로, SQLite 사용 시 자주 발생할 수 있는 오류를 방지하고, 

더 나은 성능과 유지보수를 가능하게 합니다.

 

2) Room의 기본구성요소

 

Entities

Entities는 앱 데이터베이스의 테이블을 나타냅니다. 테이블의 행에 저장된 데이터를 업데이트하고 삽입할 새 행을 만드는 데 사용됩니다.

 

DAO(Data Access Objects)

데이터 액세스 객체(DAO)는 앱이 데이터베이스의 데이터를 검색 및 업데이트, 삽입, 삭제하는 데 사용하는 메서드를 제공합니다.

 

Room Database

Room Database는 데이터베이스를 보유하며, 기본 앱 데이터베이스 연결을 위한 기본 액세스 포인트입니다. 

데이터베이스 클래스는 앱에 데이터베이스와 연결된 DAO 인스턴스를 제공합니다.

 

 

3) 내가 생각하는 프로젝트에서 Room을 사용해야 하는 경우

이번에 저는 Spring을 서바로 Jetpack Compose를 이용하여 Android App을 개발중입니다.
저의 경우 Room을 이용하여 스마트폰 내 최근 사용자 계정을 불러오기 위해 사용하였습니다.
Room은 주로 서버에 저장하는 것이 아닌 스마트폰 안의 저장소에 데이터를 저장하는 것으로
해당 스마트폰에서의 최근 사용자 계정 불러오기에 가장 적합하다고 생각했기 떄문입니다.

허나, 자동 로그인과 같은 저장 할 내용이 간단한 것들의 경우 인터넷을 찾아보니 sharedPreferences만 사용해도 충분하다고 해서
저 역시도 jwt token 저장의 경우 sharedPreferences를 사용하였습니다.

결론적으로 말씀드리자면 
자동 로그인 - 굳이 세팅이 번거로운 room보다는 sharedPreferences를 사용하라
room을 이용하여 사용자의 안드로이드 내에 데이터베이스를 생성관리할 수 있다.

이렇게 보시면 될 것 같습니다.

* SharedPreferences 역시 해당 시리즤의 후반부에 작성 예정이니 기대해주세요

 

2. Room의 구현

*저는 제 프로젝트에 맞게 세팅한것으로 이 글을 작성하였습니다.

1) Room 기초 세팅

1. build.gradle(app수준)

dependencies에 추가합니다.

implementation("androidx.room:room-runtime:2.6.1")
kapt("androidx.room:room-compiler:2.6.1")
implementation ("androidx.room:room-ktx:2.6.1")

 

2. . Entitiy 생성

@Entity(tableName = "Users")
data class User(
    @PrimaryKey
    val userName: String,
    val autoLogin: Boolean,
    val lastLoginTime: Date
)

 

Users 라는 테이블을 생성하고 각각의 컬럼을 설정합니다.

 

3. DAO 만들기

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(user: User)

    @Update
    suspend fun update(user: User)

    @Delete
    suspend fun delete(user: User)

    @Query("SELECT * from users WHERE userName = :userName")
    fun getUser(userName: String): Flow<User>

    @Query("SELECT userName FROM Users ORDER BY lastLoginTime DESC LIMIT 1")
    fun getMostRecentUserName(): String?

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

}

 

데이터베이스에 접근하고, CRUD(Create, Read, Update, Delete) 작업을 정의하는 인터페이스입니다.

@Dao 어노테이션을 사용해 정의하며, @Insert, @Delete, @Query 등의 어노테이션을 사용해 메서드를 구현합니다.

 

다른 메소드들은 일반적으로 많이 사용하는 메소드일 것 같은데

getMostRecentUserName의 경우 가장 최근 로그인한 사용자의 userName을 받아오기 위해 만든 custom method입니다.

 

 

4. RoomDatabase 만들기

@Database(entities = [User::class], version = 1, exportSchema = false)
@TypeConverters(Converters::class)
abstract class UserDatabase : RoomDatabase() {

    abstract fun userDao(): UserDao

    companion object {

        @Volatile
        private var Instance: UserDatabase? = null

        fun getDatabase(context: Context): UserDatabase {
            return Instance ?: synchronized(this) {
                Room.databaseBuilder(context, UserDatabase::class.java, "user_database")
                    .build()
                    .also { Instance = it }
            }
        }
    }
}

 

데이터베이스의 기본 클래스로, RoomDatabase를 상속하여 구현합니다.

@Database 어노테이션을 사용하고, Entity 및 DAO를 포함한 데이터베이스 구성을 정의합니다.

 

Database 클래스는 개발자가 정의한 DAO의 인스턴스를 앱에 제공합니다. 결과적으로 앱은 DAO를 사용하여 데이터베이스의 데이터를 연결된 데이터 항목 객체의 인스턴스로 검색할 수 있습니다. 앱은 정의된 데이터 항목을 사용하여 상응하는 테이블의 행을 업데이트하거나 삽입할 새 행을 만들 수도 있습니다.

 

5. Repository interface 구현

interface UsersRepository {

    suspend fun insertUser(user: User)

    suspend fun updateUser(user: User)

    suspend fun  deleteUser(user: User)

    fun  getUserStream(userName: String): Flow<User?>

    suspend fun getMostRecentUserName(): String?

    fun clearRoom()

 

 

본격적으로 repository에 구현하기 앞서 repository의 interface 먼저 설계해줍니다.

 

interface에 대한 개념이 부족하시다면 제가 이전에 쓴 글을 참조 부탁드립니다. 아래에 링크 달아드리겠습니다.

https://pinlib.tistory.com/entry/interface

 

자바 복습하기(extends, super, abstract, interface편)

* 해당 게시물은 자바의 정석(책)을 참고하여 작성하였습니다. 상속상속이란 기존의 class를 재사용하여 새로운 class를 작성하는 것으로 extends를 붙여서 사용한다.ex)class Child extends Parent {// Child, p

pinlib.tistory.com

 

6. Repository 구현

class OfflineUsersRepository (
    private val userDao: UserDao
) : UsersRepository {
    override suspend fun insertUser(user: User) = userDao.insert(user)
    override suspend fun updateUser(user: User) = userDao.update(user)
    override suspend fun deleteUser(user: User) = userDao.delete(user)

    override fun getUserStream(userName: String): Flow<User?> = userDao.getUser(userName)

    override suspend fun getMostRecentUserName(): String? = userDao.getMostRecentUserName()

    override fun clearRoom() = userDao.clear()
}

 

repository를 호출하여 room에 있는 데이터에 접근하여 CRUD 작업을 수행하게 됩니다.

 

7.  Room을 Hilt에 적용

package com.example.secretdiary.di.room

import android.app.Application
import android.content.Context
import com.example.secretdiary.di.room.repository.OfflineUsersRepository
import com.example.secretdiary.di.room.repository.UsersRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import androidx.room.Room
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Singleton


@Module
@InstallIn(SingletonComponent::class)
object RoomModule {



    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext context: Context): UserDatabase {
        return Room.databaseBuilder(context, UserDatabase::class.java, "user_database")
            .build()
    }


    @Provides
    fun provideUserDao(app: Application): UserDao {
        return UserDatabase.getDatabase(app).userDao()
    }


    @Provides
    fun provideUsersRepository(userDao: UserDao): UsersRepository {
        return OfflineUsersRepository(userDao)
    }
}

 

@Module 어노테이션으로 Hilt에게 이 클래스가 의존성을 제공하는 모듈임을 알립니다.

 

@InstallIn(SingletonComponent::class) 어노테이션은 이 모듈이 SingletonComponent에 설치됨을 나타내며, SingletonComponent는 애플리케이션 전체에서 사용할 수 있는 싱글톤 범위의 의존성을 제공합니다. 즉, 애플리케이션이 살아있는 동안 이 모듈에서 제공하는 객체들은 싱글톤으로 유지됩니다

 

@Provides: Hilt에게 이 함수가 의존성을 제공하는 공급자임을 나타냅니다.

 

@Singleton: 이 함수가 반환하는 인스턴스(UserDatabase)가 애플리케이션 전체에서 단일 인스턴스로 사용됨을 보장합니다.

 

이 함수가 하는 역할은 UserDatabase를 애플리케이션 전역에서 싱글톤으로 생성 및 유지하는 것입니다. 이를 통해 여러 곳에서 UserDatabase를 사용할 때 동일한 인스턴스를 재사용하여 메모리와 리소스를 절약할 수 있습니다.

 

요약해서 말씀드리자면 Room 데이터베이스를 Hilt를 사용하여 Dependency Injection(DI) 방식으로 주입하기 위한 코드입니다.

 

8. 실질적인 사용

@Composable과 viewModel에서호출하기 위한 방식 2가지 모두 알려드리겠습니다.

 

1) @Composable에서 Room 호출 방식

@Composable
fun SettingScreen(
    navController: NavHostController,
    settingViewModel: SettingViewModel,
    componentViewModel: ComponentViewModel,
    modifier: Modifier = Modifier
) {
    val context = LocalContext.current
    
    var userRoomEmail by remember { mutableStateOf<String?>(null) }
   

    // LaunchedEffect에서 userRoomEmail에 따라 동작하도록 설정
    LaunchedEffect(Unit) {
        val userDao = UserDatabase.getDatabase(context).userDao()
        val usersRepository: UsersRepository = OfflineUsersRepository(userDao)

        withContext(Dispatchers.IO) {
            userRoomEmail = usersRepository.getMostRecentUserName()
        }

    }

 
    if (userRoomEmail != null) {
        Log.d("Load User Room3", "userEmail = $userRoomEmail")
        settingViewModel.loadUserInfo(userRoomEmail!!)
    } else {
        Log.d("Load User Room", "Failed")
    }


    
}

 

이 코드는 제가 Secret Diary project로 사용한 코드 중 일부입니다.

여기서 핵심은 context와 userDao, usersRepository입니다.

 

여기서 중요한 부분은 userRepository의 method를 사용 할 때에는 coroutine 안에서 호출해야 한다는 점입니다.

 

2) viewModel에서의 호출

@HiltViewModel
class SecurityViewModel @Inject constructor(
    @ApplicationContext private val context: Context,
    private val userRepository: UsersRepository
) : ViewModel() {

 

우선 아까 Room을 Hilt에 적용하였으므로 viewModel의 생성자에 userReposiory를 넣어 DI를 부여합니다.

이것만으로 이제 viewModel에서의 사용이 가능하게 됩니다.

 

이후에는 

viewModelScope.launch(Dispatchers.IO) {
            

                //room
                val user = User(userName = loginEmail, autoLogin = true, lastLoginTime = Date())
                userRepository.insertUser(user)

 

composable에서의 경우와 마찬가지로 coroutine안에서 호출하면 됩니다.

 

여기까지 Jetpack Compose에서의 Room사용법에 관하여 알아보았습니다.

다음 글은 Room을 사용할 때 나왔던 Kotlin coroutine과 비동기에 관한 주제로 돌아오겠습니다.

반응형