🙇 **안녕하세요 ! **
Mash-Up 10기 Android Developer 정현성 인사드립니다. 🙇
이번 포스팅 주제로 무엇을 할까 고민을 하다,,,,
최근 취업 준비를 하며 그동안 개발했던 애플리케이션들을 하나의 포트폴리오로 보여주는 애플리케이션을 개발한 적이 있습니다.
개발하며 사용했던 기술 스택 중 하나인 Room에 대해 다시 정리해볼 겸 포스팅 주제로 선정하였습니다.
안드로이드에서는 앱의 데이터를 효율적으로 관리하기 위한 저장소로 SQLite라는 데이터베이스를 제공하고 있습니다. 이는 다른 외부 Database들과 달리 소규모 데이터를 관리하고 사용하는 데 적합한 관계형 데이터베이스입니다. 예를 들어, 애플리케이션 사용 과정에서 발생하는 용량이 크지 않은 데이터들은 굳이 서버에 접속하고 저장하는 수고를 들이지 않고 내부 데이터베이스를 통해 저장하고 관리할 수 있습니다. 이렇게 적은 데이터를 관리하는데 최적화된 만큼 속도가 빠르고 가볍다는 장점이 존재하여 현시점의 많은 애플리케이션이 공통으로 내부 DB를 활용하고 있습니다.
Local Database 종류
SQLite Database Library, Room Database Library
안드로이드에서 Local Database를 구현하는 방법으로, SQLite Database Library 사용을 권장해왔습니다.
하지만 이를 사용하기 위해서는 상당한 시간과 노력 등 사용 방법이 복잡하여, 구글에서는 이러한 문제를 해결하고자 새롭게 Room Database Library를 배포하였습니다.
👏 Room에 새롭게 추가된 기능
1. 컴파일 시점에 SQL 쿼리에 대한 유효성 검사 기능
2. Schema가 변경될 시 자동으로 업데이트 기능
3. Java 데이터 객체를 변경하기 위해 상용구 코드 없이 ORM 라이브러리를 통해 매핑 기능
4. LiveData와 Rx Java를 위한 Observation 생성 및 동작 기능
위와 같이 SQLite Database Library에서는 허용되지 않던 기능들이 새롭게 추가되면서 조금 더 개발자들이 Local DB를 간편하게 구현할 수 있도록 설계된 모습을 볼 수 있습니다. 그에 따라 SQLite Database의 사용 빈도는 줄어들고, Room Database의 사용 빈도는 증가하고 있습니다.
ORM이란?
먼저, Room을 자세히 알아보기 전에 ORM에 대한 개념을 간단히 살펴볼 필요가 있습니다.
Object-Relational Mapping
ORM이란, Object Relational Mapping, 객체-관계 매핑의 줄임말입니다. 객체 지향 프로그래밍(Object Oriented Programming, OOP)은 클래스를 사용하고, 관계형 데이터베이스(Relational DataBase, RDB)는 테이블을 사용합니다. 이러한 객체 모델과 관계형 모델 간에 불일치가 존재하게 되는데, ORM을 통해 객체 간의 관계를 바탕으로 SQL문을 자동으로 생성하여 불일치를 해결할 수 있습니다.
장점
단점
Room이란?
안드로이드 아키텍쳐 컴포넌트(Android Architecture Components, AAC)에 속하는 Room은 애플리케이션에서 SQLite 데이터베이스를 쉽고 편리하게 사용할 수 있도록 하는 ORM 라이브러리입니다.
Room Library 구성요소
엔티티(Entity), 데이터 접근 객체(DAO), 룸 데이터베이스(Room Database), 총 세 개의 구성 요소를 통해 Room 데이터베이스 라이브러리가 구성됩니다.
Entity
데이터베이스 내의 릴레이션 즉, 테이블을 뜻하며 DB에 저장할 데이터 형식을 정의합니다.
DAO(Data Access Object)
데이터베이스에 접근하여 수행할 작업을 메소드 형태로 정의합니다.
Room Database
데이터베이스의 전체적인 소유자 역할을 하며 DB를 새롭게 생성하거나 버전을 관리합니다.
Room Database를 활용하여 포트폴리오 애플리케이션을 구현하였습니다.
- Entity (데이터베이스의 테이블 담당)
PortfolioEntity, ProfileEntity
- DAO (각각의 테이블에 접근하여 작업 수행을 담당)
PortfolioDao, ProfileDao
- Room Database (데이터베이스의 전체적인 소유자 역할)
PortfolioDatabase
데이터베이스 구성요소 입니다.
Build.Gradle 설정하기
dependencies {
//Room Database Library
implementation "androidx.room:room-runtime:2.2.6"
implementation "androidx.room:room-ktx:2.2.6"
implementation "androidx.room:room-testing:2.2.6"
kapt "androidx.room:room-compiler:2.2.6"
}
PortfolioEntity
@Entity(tableName = "portfolio")
data class PortfolioEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
val id: Long,
@ColumnInfo(name = "thumbnail")
val thumbnail: Int,
@ColumnInfo(name = "image_drawable")
val image: String,
@ColumnInfo(name = "project")
val project: String,
@ColumnInfo(name = "introduction")
val introduction: String,
@ColumnInfo(name = "programming_language")
val programming: String,
@ColumnInfo(name = "architecture")
val architecture: String,
@ColumnInfo(name = "library")
val library: String,
@ColumnInfo(name = "server")
val server: String,
@ColumnInfo(name = "term")
val term: String,
@ColumnInfo(name = "link")
val link: String
)
@Entity 어노테이션과 함께 데이터베이스 내의 릴레이션(테이블)을 생성하고 있습니다. 클래스의 이름은 PortfolioEntity이지만 tableName을 portfolio로 설정하였기 때문에 클래스 이름과는 별개로 위 데이터베이스 내의 릴레이션 이름은 portfolio로 설정됩니다. 또한, 클래스의 생성자로 릴레이션 내의 각각의 속성들을 정의하고 있습니다.
PortfolioDao
@Dao
interface PortfolioDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertPortfolio(portfolioEntity: PortfolioEntity)
@Query("SELECT * FROM portfolio")
suspend fun getPortfolio(): List<PortfolioEntity>
@Query("SELECT * FROM portfolio WHERE id = :id")
suspend fun getPortfolioInfo(id: Long): PortfolioEntity
}
@Dao 어노테이션과 함께 데이터베이스에 접근하여 수행할 작업들을 메서드 형태로 지정합니다. 이 메소드들은 interface 내에 포함되므로 모두 추상 메소드들이며 조회, 추가, 수정, 삭제 기능( 저는 추가, 조회 기능만 구현했습니다. )을 구현합니다.
또한, insert 메소드 부분을 보면, onConflict 설정을 통해 만약 데이터베이스 내에 중복된 데이터 값이 존재한다면, 그 값 위에 덮어 씌우는 설정을 해두었습니다.
PortfolioDatabase
@Database(
entities = [
ProfileEntity::class,
PortfolioEntity::class
],
version = VERSION,
exportSchema = false
)
abstract class PortfolioDatabase : RoomDatabase() {
abstract fun profileDao(): ProfileDao
abstract fun portfolioDao(): PortfolioDao
companion object {
const val VERSION = 1
private const val TABLE_NAME = "portfolio_db"
@Volatile
private var instance: PortfolioDatabase? = null
fun getInstance(context: Context): PortfolioDatabase {
return instance ?: synchronized(this) {
instance ?: buildDatabase(context).also { instance = it }
}
}
private fun buildDatabase(context: Context): PortfolioDatabase {
return Room.databaseBuilder(context, PortfolioDatabase::class.java, TABLE_NAME)
.build()
}
}
}
@Database 어노테이션과 함께 데이터베이스의 전체적인 소유자 역할을 하고 있으며 앞에서 생성한 Entity, DAO 클래스를 통합적으로 묶어 데이터베이스를 생성하거나 버전 관리를 담당합니다.
**Dagger DI ( 의존성 주입에 대한 설명은 생략하겠습니다 :( ) **
DatabaseModule
@Module class DatabaseModule { @Singleton @Provides fun providesPortfolioDatabase(context: Context): PortfolioDatabase = PortfolioDatabase.getInstance(context) @Singleton @Provides fun providesPortfolioDao(db: PortfolioDatabase): PortfolioDao = db.portfolioDao() }
@Singleton 어노테이션을 통해, 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고, 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴하도록 하였습니다.
PortfolioRepository
interface PortfolioRepository { suspend fun insertPortfolio(portfolio: Portfolio) suspend fun getPortfolio(): List<Portfolio> suspend fun getPortfolioInfo(id: Long): Portfolio }
PortfolioRepositoryImpl
class PortfolioRepositoryImpl( private val portfolioDao: PortfolioDao ) : PortfolioRepository { override suspend fun insertPortfolio(portfolio: Portfolio) = portfolioDao.insertPortfolio(portfolio.mapPortfolioEntity()) override suspend fun getPortfolio(): List<Portfolio> = portfolioDao.getPortfolio().mapPortfolioList() override suspend fun getPortfolioInfo(id: Long): Portfolio = portfolioDao.getPortfolioInfo(id).mapPortfolio() }
PortfolioMapper
fun Portfolio.mapPortfolioEntity(): PortfolioEntity = PortfolioEntity( id, thumbnail, image, project, introduction, programming, architecture, library, server, term, link ) fun PortfolioEntity.mapPortfolio(): Portfolio = Portfolio( id, thumbnail, image, project, introduction, programming, architecture, library, server, term, link ) fun List<PortfolioEntity>.mapPortfolioList(): List<Portfolio> = map { it.mapPortfolio() }
가져온 Data를 Mapping하는 매소드입니다.
SampleData
val INIT_PORTFOLIO: List<Portfolio> = listOf(
Portfolio(
..
),
Portfolio(
..
),
..
)
HomeViewModel
class HomeViewModel @Inject constructor(
private val portfolioRepository: PortfolioRepository
) : ViewModel() {
private val portfolio = MediatorLiveData<List<Portfolio>>()
private val _homePortfolioUiModel = MediatorLiveData<List<HomePortfolioUiModel>>()
val homePortfolioUiModel: LiveData<List<HomePortfolioUiModel>>
get() = _homePortfolioUiModel
init {
_homePortfolioUiModel.addSource(portfolio) {
_homePortfolioUiModel.value = it.mapToHomePortfolioUiModel()
}
}
private fun insertPortfolio() {
viewModelScope.launch {
INIT_PORTFOLIO.iterator().forEach {
portfolioRepository.insertPortfolio(it)
}
}
portfolio.value = INIT_PORTFOLIO
}
private fun getPortfolio() {
viewModelScope.launch {
portfolio.value = portfolioRepository.getPortfolio()
}
}
}
이번 포스팅을 통해 SQLite Database에 비해 Room Database가
성능면에서도 뛰어나고 편리하고 직관적인 코드를 작성할 수 있다는 것을 체감할 수 있었습니다 :)
그럼 👋