[Android] DB Room이란?

Oxong·2021년 7월 16일
0

21.07.15
공부한 것을 정리하는 용도의 글이므로 100% 정확하지 않을 수 있습니다.
참고용으로만 봐주시고, 내용이 부족하다고 느끼신다면 다른 글도 보시는 것이 좋습니다.
+ 틀린 부분, 수정해야 할 부분은 언제든지 피드백 주세요. 😊
                                            by. ryalya



들어가기 전

안드로이드 앱을 만들면서 내부 저장소(DB)를 사용해야 할 때, 나는 SQLite를 사용했다.

안드로이드는 어플리케이션의 효과적인 데이터 관리를 위해 파일 형식으로 데이터를 저장하며, 소규모 데이터를 관리하는데 적합한 관계형 DB인 SQLite를 지원한다.

SQLite의 사용이 권장되는 곳은 임베디드 기기, IoT, 응용 프로그램 파일 형식, 웹사이트(트래픽이 적거나 중간 정도인), 엔터프라이즈 데이터용 캐시 등이다.

안드로이드 공식 문서에서 Room을 사용해야 하는 이유를 설명하고 있으므로 자세한 이유를 알고싶다면 참고하길 바란다.



Room이란?


Room은 쉽게말해서 AAC(Android Architecture Components ), 스마트폰 내장 DB에 데이터를 저장하기 위해 사용하는 라이브러리이다.

Room은 ORM(Object Relational Mapping)라이브러리로서 DB 데이터를 Java 또는 코틀린 객체로 매핑해준다.

Room은 SQLite를 내부적으로 사용하고 있지만, DB를 구조적으로 분리하여 데이터 접근의 편의성을 높여주고 유지보수에 편리하다.

또한, 다양한 Annotation을 통해 컴파일시 코드들을 자동으로 만들어주며 LiveData, RxJava와 같은 Observation 형태를 지원하고 MVP, MVVM과 같은 아키텍쳐 패턴에 쉽게 활용할 수 있도록 되어있다.



SQLite보다 Room 사용을 권장하는 이유는?


SQlite는 쿼리의 컴파일 시간 검증을 할 수 없다. 그러나 Room에는 컴파일 타임에 SQL 유효성 검사가 있다.

그리고 SQLite는 스키마가 변경되면 영향을 받는 SQL 쿼리를 수동으로 업데이트해야 한다.

또한, SQL 쿼리와 Java 데이터 개체 간에 변환하려면 많은 상용구 코드를 사용해야 한다.
그러나 Room은 상용구 코드 없이 데이터베이스 데이터를 Java 또는 Kotlin 객체에 매핑할 수 있다.

이렇듯, Room은 데이터 관찰을 위해 LiveData 및 RxJava와 함께 작동하도록 구축되었지만 SQLite는 그렇지 않다.

게다가 Room은 SQLite에서는 되지않던 기능들을 사용할 수 있게 되어 내부 DB를 좀더 간편하게 구현할 수 있다.

Room에서 허용된 기능들을 정리하면 아래와 같다.

  • 컴파일 도중 SQL에 대한 유효성 검사 가능

  • Schema가 변경될 시 자동으로 업데이트 가능

  • Java 데이터 객체를 변경하기 위해 상용구 코드 없이 ORM 라이브러리를 통해 매핑 가능

  • LiveData와 RX Java를 위한 Observation 생성 및 동작 가능



Room 구조 & Annotation

이미지 출처

Room 라이브러리는 엔티티(Entity), 데이터 접근 객체(DAO), 데이터베이스(DB)로 구성되어 있다.

- Entity

: DB 내의 Table, 즉 DB에 저장할 데이터 형식으로 class의 변수들이 컬럼(column)이 되어 table이 된다.

Annotation

@Entity(tableName = StudentEntry.TABLE_NAME)
: Table 이름을 선언한다. (기본적으로 entity class 이름을 database table 이름으로 인식)

@PrimaryKey
: 각 entity 는 1개의 primary key 를 가져야 한다.

@ColumnInfo
: Table 내 column 을 변수와 매칭

@Entity
    public class User {
        @PrimaryKey
        public int uid;

        @ColumnInfo(name = "first_name")
        public String firstName;

        @ColumnInfo(name = "last_name")
        public String lastName;
    }

- DAO

: 데이터베이스에 접근하여 수행할 작업을 메소드 형태로 정의 (SQL 쿼리 지정 가능)

Annotation

@insert
: Entity set 삽입. @Entity로 정의된 class만 인자로 받거나, 그 class의 collection 또는 array 만 인자로 받을 수 있다.
인자가 하나인 경우 long type 의 return (insert 된 row Id)을 받을 수 있고, 여러 개인 경우 long[], List 을 받을 수 있다.

"onConflict = OnConflictStrategy.REPLACE" option 으로 update 와 동일한 기능을 할 수 있다.

@update
: Entity set 업데이트. Return 값으로 업데이트된 행 수를 받을 수 있다.

@delete
: Entity set 삭제. Return 값으로 삭제된 행 수를 받을 수 있다.

@query
: @Query를 사용하여 DB를 조회할 수 있다.
Compile time 에 return 되는 object 의 field 와 sql 결과로 나오는 column 의 이름이 맞는지 확인하여 일부가 match되면 warning, match 되는게 없다면 error를 보낸다.

@Dao
    public interface UserDao {
        @Query("SELECT * FROM user")
        List<User> getAll();

        @Query("SELECT * FROM user WHERE uid IN (:userIds)")
        List<User> loadAllByIds(int[] userIds);

        @Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
               "last_name LIKE :last LIMIT 1")
        User findByName(String first, String last);

        @Insert
        void insertAll(User... users);

        @Delete
        void delete(User user);
    }
    

- Room DB

: 데이터베이스의 전체적인 소유자 역할, DB 생성 및 버전 관리.

RecyclerView의 어댑터 같은 느낌으로 Entity만큼 정의된 Dao 객체들을 반환할 수 있는 함수들을 가지고 있는 추상 클래스 형태로 정의.

RooM DB에서 DAO를 가져와서 객체를 통해 데이터를 CRUD함.

Annotation

@Database
: class가 Database임을 알려주는 어노테이션.

  • entities
    : 이 DB에 어떤 테이블들이 있는 지 명시.

  • version
    : Scheme가 바뀔 때 이 version도 바뀌어야 함.

  • exportSchema
    : Room의 Schema 구조를 폴더로 Export 할 수 있음. 데이터베이스의 버전 히스토리를 기록할 수 있다는 점에서 true로 설정하는 것이 좋다.

@Database(entities = {User.class}, version = 1)
    public abstract class AppDatabase extends RoomDatabase {
        public abstract UserDao userDao();
    }
    
// 위의 파일 생성 후, 아래 코드로 DB instance 가져올 수 있다.
    
 AppDatabase db = Room.databaseBuilder(getApplicationContext(),
 AppDatabase.class, "database-name").build();    


Room 사용법


1. Gradle 설정

dependencies {
  ...
  def room_version = "2.2.5"
  // Room components
  implementation "androidx.room:room-runtime:2.2.5"
  annotationProcessor "androidx.room:room-compiler:2.2.5"
  androidTestImplementation "androidx.room:room-testing:2.2.5"

  // Lifecycle components
  implementation "androidx.lifecycle:lifecycle-viewmodel:2.2.0"
  implementation "androidx.lifecycle:lifecycle-livedata:2.2.0"
  implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
  ...
}

장황하게 gradle을 늘어놓았지만 코틀린 코드를 사용할 것이 아니라면 아래 코드만 설정해도 무방하다.

implementation 'androidx.room:room-runtime:2.2.6'
annotationProcessor 'androidx.room:room-compiler:2.2.6'

2. Entity Class 생성

Room에서는 class로 Table을 명시할 수 있다. 따라서 데이터 모델인 Entity를 만들어야 한다.

Annotation으로 Entity를 추가해서 class를 만든 후, 해당 class에 들어갈 Entity들을 정의해준다.

여기서 각 Entity는 PrimaryKey가 반드시 필요한데, autoGenerate=true를 하면 키가 자동으로 생성된다.

나는 지금 당이 필요하므로 먹고 싶은 dessert class를 만들어보겠다.

@Entity
public class Dessert {
    @PrimaryKey(autoGenerate = true)
    public int id;

    @ColumnInfo(name="dessertName")
    public String dessertName;

    @ColumnInfo(name="origin")
    public String origin;

    @ColumnInfo(name="amount")
    public long amount;
  • @Ignore 를 이용하면 해당 필드는 들어가지않는다.
  • Entity 어노테이션에서 foreinKey인자를 통해 테이블간 관계 설정 가능
  • Relation 어노테이션을통해 선언된 테이블 관계에서 매칭되는 외래키를 설정해줄 수 있습니다.

3. DAO 생성

DB에 접근해 query, insert, delete등을 수행할 메소드를 포함하여 만든다.

@Dao
public interface DessertDao {
    @Query("SELECT * FROM Dessert")
    List<Dessert> getAll();

    @Query("SELECT * FROM Dessert WHERE id IN (:dessertIds)")
    List<Dessert> loadAllByIds(int[] dessertIds);

    @Insert
    void insertAll(Dessert... desserts);
    
     @Delete
    void delete(Dessert dessert);
}

일반적으로 위처럼 생성할 수 있다.

그런데 리턴값을 RxJava에서 사용할 수 있는 Observe 형태로도 리턴할수도 있다.

하지만 Rx를 사용할 경우, 오탐 에러를 피하기위해 Observable형태로 데이터로 리턴받기보다는 되도록 Flowable, Completable, Single을 사용하는게 좋다.


4. Database

데이터베이스의 정의는 @Database로 Database 클래스임을 명시하며, RoomDatabase을 상속받아 abstract로 선언되어야 하며 abstract의 Dao를 가지고 있다.

Room 객체는 많은 리소스를 소모하기 때문에 Singleton 패턴으로 정의해야 한다.

@Database(entities = {Dessert.class}, version = 1)
public abstract class DessertDB extends RoomDatabase {

    private static DessertDB INSTANCE = null;

    public abstract DessertDao dessertDao();


    public static DessertDB getInstance(Context context) {
        if (INSTANCE == null) {
            INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
                    DessertDB.class, "dessert.db").build();

        }
        return INSTANCE;
    }

    public static void destroyInstance() {
        INSTANCE = null;
    }

}

기본적인 삽입/수정/삭제 외에 다른 기능을 가진 메서드를 만들고 싶다면 @Query 어노테이션을 붙이고 그 안에 어떤 동작을 할 건지 sql 문법으로 작성을 해주면 된다.


5. Activity에서 Room접근(사용)

private DessertDB dessertDB = null;

// onCreate()

   // DB 생성
   dessertDB = DessertDB.getInstance(this);
   
   // main thread에서 DB 접근 불가하므로 data를 읽고 사용할 thread 사용 필요
   class InsertRunnable implements Runnable {
   	@Override
    	public void run(){
        
        }
   }
   InsertRunnable insertRunnble = new InsertRunnable();
   Thread t = new Thread(insertRunnable);
   t.start();

  // 버튼 사용하여 실행할 경우
  InsertRunnable insert = new InsertRunnable();
  Thread addThread = new Thread(insert);
  addThread.start(); // 삽입
  
  Intent intent = new Intent(getApplicationContext(), MainActivity.class);
  startActivity(intent);
  finish();
  
  // DB 닫기
  
   @Override
    protected void onDestroy() {
        super.onDestroy();
        DessertDB.destroyInstance();
    }
  


마무리

기본적인 Room에 대한 사용법은 위와 같다.

하지만 위에서 언급한 것 처럼 안드로이드 공식 문서에서도 데이터베이스 객체를 인스턴스 할 때 싱글톤 패턴으로 구현하기를 권장하고 있다.

일단 여러 인스턴스에 액세스를 꼭 해야 하는 일이 거의 없고, 객체 생성에 비용이 많이 들기 때문이다.

싱글톤 패턴을 사용하지 않은 경우, allowMainThreadQueries()를 사용해 강제로 실행시킬 수 있지만 문제가 될 수 있다고 한다.

싱글톤 패턴을 사용한다면 비동기 실행으로 다른 Thread에 일을 시키면 된다.

해당 부분에 대해 추가적인 공부가 필요할 것 같다.



Reference

안드로이드 Room의 사용법과 예제

SQLite 에서 ROOM 까지 (7) - ROOM 이란?

Room 개념 및 예제 [JAVA]

0개의 댓글