본문 바로가기
IT 기술/Android

[Android] Room Database 기초

by Geunny 2020. 8. 5.
반응형

Room Database

  • Room은 SQLite에 대한 추상화 레이어를 제공하여 원활한 데이터베이스 액세스를 지원하는 동시에 SQLite를 완벽히 활용하는 라이브러리이다. ( 일명 ORM )

  • Room 라이브러리는 데이터를 로컬로 유지하여 데이터 캐싱이 가능하다. 이는 모바일 기기가 네트워크에 접속할 수 없게 되었을 때 오프라인 상태에서도 사용자가 여전히 콘텐츠를 탐색할수 있으며, 나중에 다시 온라인 상태가 되면 사용자가 오프라인 상태에서의 콘텐츠 변경사항이 서버에 동기화 시킬수 있다. (모바일 DB의 사용 이유중 하나)

Room Database 구조

 

Room Library를 사용하기 위해선 gradle에 다음 종속을 추가해준다.

 

def room_version = "2.2.5"
      implementation
"androidx.room:room-runtime:$room_version"
      annotationProcessor
"androidx.room:room-compiler:$room_version"

 

코틀린 언어 사용시에는 annotationProcessor 대신 kapt 을 사용해야 한다.

 

Room Database는 ORM(Object-relational mapping) 으로 동작하여며

크게 3가지로 구성되어 있다.

 

 

1. Database : 데이터베이스 홀더를 포함하여앱의 지속적인 관계형 데이터의 기본 연결을 위한

기본 엑세스 포인트 역할을 한다.

 

2. Entity : 데이터베이스 내의 테이블을 나타낸다.

 

3.DAO : 데이터베이스에 접근하는데 사용하는 메서드가 포함되어있다.

 

 

Android Documentation 에 있는 기본 예제를 실행해 보자.

 

먼저 Entity로 사용될 유저객체는 다음과 같이 구성되어 있다.

 

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

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

        @ColumnInfo(name = "last_name")
        public String lastName;
        
        @Override
    	public String toString() {
        	return uid + "번 이름 :" + firstName + lastName;
    	}
    }
    

@Entity 어노테이션을 이용하여 해당 Class를 Entity로 사용해 준다는 것을 명시해 준다.

만약 코틀린을 사용할 경우에는 해당 클래스를 data class로 생성한다.

또한 Entity는 1개 이상의 PrimaryKey를 꼭 갖고있어야 한다.

PrimaryKey 로 사용되지 않는 나머지 필드들은 @ColumnInfo 어노테이션을 이용하여

해당 컬럼의 이름을 명시해 줄 수 있다.

(toString() 매서드는 나중에 로그 확인을 편하게 하기위해 작성해 주었다.)

 

 

다음은 DAO 인터페이스이다.

    @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);
    }
    

DAO 는 스프링 부트에서 사용되는 것과 유사하게 Interface로 구성되어 있다.

Entity 와 마찬가지로 @DAO 어노테이션을 이용하여 해당 인터페이스를 DAO로 사용해 줄것을 명시한다.

 

인터페이스 내부에는 Database에 접근하여 이용할 메서드들이 Abstract Method로

반환값과 매개변수의 타입을 지정한후

여러 어노테이션을 이용하여 구현할 수 있다.

@Query 어노테이션을 이용하면 수행할 SQL 문장을 명시하여 해당 메서드의 동작을 구현할 수 있다.

 

@Insert 와 @Delete 와 같은 어노테이션은 sql문을 직접 사용하지 않고도 구현이 가능하다.

 

다음은 Database를 실제 앱에서 사용할 때 DB 객체를 받아올 추상 클래스를 작성한다.

보통 AppDatabase 이름으로 추상클래스를 만들어준다.

    @Database(entities = {User.class}, version = 1)
    public abstract class AppDatabase extends RoomDatabase {
        public abstract UserDao userDao();
    }
    

해당 추상클래스는 @Database 어노테이션을 작성한다.

또한 해당 클래스는 RoomDatabase 클래스를 상속받는다.

클래스 내부에는 DAO의 클래스를 반환받는 추상매서드가 존재한다.

 

해당 추상클래스를 앱에서 사용할 때 아래와 같이 코드를 작성하여 사용이 가능하다.

 

    AppDatabase db = Room.databaseBuilder(getApplicationContext(),
            AppDatabase.class, "database-name").build();
    

 

위와 같은 코드로 작성하면 이제 서비스 내부에서 "db.method(~~~~)" 의 형태로 Room Database 로 접근하여

Moblie Database 를 사용할 수 있게 된다.

 

단, 해당 코드를 사용할 때 Database에 접근하는 형태이므로 Thread를 이용하여 비동기식으로 접근해야 사용이 가능하다.

동기식으로 처리하게 되면 Database 접근시 해당 Context(Activity)가 멈추는 현상이 나타나기 때문이다.

 

아래는 위의 코드를 실제 Service 에서 사용하고 Main Activity를 이용하여

Databaes에 유저객체를 하나 생성하고 조회하는 예제이다.

 

Activity

// in Activity

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Intent intent = new Intent(MainActivity.this, MyService.class);

        startService(intent);
    }

 

Service

// in Service

@Override
    public void onCreate() {
        super.onCreate();

        Thread t = new Thread(() -> {
                AppDatabase db = Room.databaseBuilder(getApplicationContext(),
                        AppDatabase.class, "database-name").build();
                User user = new User(0, "Hong", "Gildong");
                User user2 = new User(1, "Kim", "Gildong");



            	db.userDao().insertAll(user);
            	db.userDao().insertAll(user2);

                List<User> userset = db.userDao().getAll();
                for (User u : userset) {
                    Log.d("user", u.toString());
                }
            db.close();
        });
        t.start();

    }

 

 

 

 

 

참고사항

 

처음 이 예제를 진행하고서 DB에 데이터를 넣은후 해당 앱을 다시 실행하였더니 아래와 같은 오류가 나타났다.

 

분명 처음 실행에는 잘 되었는데... 왜 이럴지 오류코드를 분석해 보았다.

 

결론부터 말하자면 이 에러는 DB UNIQUE 제약조건을 위반했다는 내용이다.

왜냐하면... 위의 코드대로라면

Service 내부에서 현재 DB에 존재하는 Primary key가 같은 객체를 DB에 insert 하려고 하여

유니크 제약조건에 위반되어 앱이 강제로 종료되게 된다.

 

이러한 문제를 해결하기 위해서는

try catch를 이용하여 에러를 잡을수 있다.

 

            try {
            
                db.userDao().insertAll(user);
                db.userDao().insertAll(user2);
                
            } catch (SQLiteConstraintException e) {
                Log.d("SQLiteConstraintException", e.getMessage());
            }

 

나중에 앱을 설계할 때 Database에 Entity를 insert 할 때

위와 같은 Unique Constraint 를 지키도록 try catch로 꼭 예외 처리를 해주는 것이 좋을 것이다.

댓글