codingeverybody / codingyahac

https://coding.yah.ac
291 stars 50 forks source link

안드로이드 room의 update 사용법 #834

Closed kammaii closed 5 years ago

kammaii commented 5 years ago

해결하고자 하는 문제

안녕하세요?

안드로이드 room을 이용해 Database를 만들고 있습니다. room이 간편한 library라고 들었는데 나온지 얼마 안되었는지 검색을 해도 자료가 많이 나오지 않는 것 같습니다. 아무튼 제가 생각하는 update는 database의 어떤 a자료를 b로 수정하는 것일 것 같은데 사용 코드를 보면

    @Update
    void update(CollectionTable collectionTable);

매개변수가 하나만 들어가 있어 어떻게 수정해야 하는것인지 모르겠습니다. 혹시 제가 room의 update에 대해 잘못 이해하고 있는 것인가요?

Haytsir commented 5 years ago

Room에서는 DAO(Data Access Object)라고 하는 객체를 통해 데이터베이스와 통신하는데, 여기서 말하는 데이터베이스는 DBMS 전체를 일컫는 말이 아니라, 어느 기능을 위해 조작하고자 하는 값들의 집합으로, 쉽게 말하면 테이블 정도의 개념이라고 보시면 되겠습니다.

DAO는 Database에서 조작하고자 하는 칼럼들의 명세를 담고있는 Entity를 통해 얻어온 값을 저장하고, 값을 수정합니다.

@Entity
public class 엔티티클래스이름 {
    @PrimaryKey
    public int id;
    ...
}
@Dao
public interface DAO인터페이스명 {
    ...
    @Update
    void update(엔티티클래스이름 collectionTable);
    ...
}

올려주신 코드는 DAO안에 메서드로서 들어가게 되고, 사용할 때에는 DB클래스 내에서 아래처럼 사용됩니다.

@Database(entities = {엔티티클래스이름.class}, version = 1)
public abstract class 데이터베이스클래스이름 extends RoomDatabase {
    public abstract DAO인터페이스명 DAO식별자명();
    ...
}

최종적으로 사용될 때에는

    데이터베이스클래스이름 db = Room.databaseBuilder(getApplicationContext(),
        데이터베이스클래스이름.class, "데이터베이스-파일-이름").build();
    ...
    new 엔티티클래스이름 collectionTable = new 엔티티클래스이름();
    ...
    db.DAO식별자명().update(collectionTable);

처럼 사용합니다.

여기에서 간단한 예제를 찾아볼 수 있고, 순서대로 정리된 예제가 여기있습니다. 한 번 참고해보세요.

kammaii commented 5 years ago

안드로이드에서 room을 사용할 때 Entity, Dao, Database 등 3개의 요소가 필요하고 각각의 역할은 어느정도 이해를 했는데 보내주신 예제에는 Repository라는 class가 새로 등장해서 다시 혼란스러워졌습니다.

DB의 table에서 필요한 값들을 가져오기 위한 명령어를 Dao에서 'insert', 'update' 등으로 정의 했는데 왜 이것들을 Repository class에서 더욱 복잡한 방법으로 다시 정의하는 것인지 잘 모르겠습니다.

아마 'backGround'에서 동작이 되어야 하는 어떤 이유가 있어서 Repository class를 만든 것 같은데 이렇게 명령어들을 다시 정의해야 하는 것이라면 Dao는 필요가 없어지는 것 아닌가요? Repository class가 정확히 어떤 역할을 하는 것인지 가르쳐 주시면 감사하겠습니다.

그리고 Repository class를 보면 각각의 명령어(insert, delete, update, getAll 등...)각각에 대해 doInBackground()를 재정의 하는 것으로 나오는데 실재로도 이렇게 반복적으로 재정의 하는 작업을 해야 하는 것인지도 궁금합니다.

아! 마지막으로 db 자료들은 컴퓨터의 어디에 저장이 되는 것인가요? 파일로 확인하고 직접 수정할 수 도 있나요?

감사합니다!

Haytsir commented 5 years ago

Repository는 Dao의 질의 메서드를 재정의하는 클래스가 아니라, 질의 메서드를 사용하는 클래스라고 보셔야합니다.

질의 메서드들에 대한 doInBackground()를 재정의하는것이 아닌, AsyncTask라는 내부 클래스를 만들어 이 클래스의 메서드 doInBackground()를 재정의하는 것입니다. 이 클래스를 통해 백그라운드 작업을 수행하도록 하기 위함이에요.

예제를 확인해보시면 AsyncTask클래스의 doInBackground() 메서드 내에서 질의 메서드를 수행하는 걸 볼 수 있습니다.

Room에서는 [allowMainThreadQueries()](https://developer.android.com/reference/android/arch/persistence/room/RoomDatabase.Builder.html#allowMainThreadQueries())메서드를 사용하지 않는 한, 메인 쓰레드에서 질의를 수행하는 걸 막고있습니다. 메인 쓰레드에서 질의 작업을 수행하면 긴 작업의 경우에는 화면이 멈추거나 하는 문제가 생기기 때문이죠.

앞서 링크로 보여드린 예제는 LiveData를 사용하는 예제인데, LiveData도 데이터에 접근하는데 왜 Background에서 동작하도록 하지 않는지 궁금하실 수 있습니다. LiveData는 내부적으로, 필요할 때 비동기 작업을 수행하도록 만들어 져 있기때문에 굳이 백그라운드 쓰레드에 놓을 필요가 없기 때문이에요.

또, Repository는 꼭 이렇게 사용해야 한다는 약속이 아니고, 다른 개발자들이 권장하는, 프로젝트 패턴에 존재하는 개념이에요.

이곳, 저곳에서 혼잡한 방식으로 DB에 접근하기보다 Repository라는 클래스를 통해서만 DB에 접근하도록 하면서, 백그라운드에서 동작하는 질의 쓰레드를 이 곳에서 모두 관리하도록 해 정돈된 형식으로 DB에 접근하도록 하자는 목적입니다.

여기에서 패턴에 대한 자세한 설명을 보실 수 있어요.

DB파일은 /data/data/패키지이름 경로에 있다고 해요. 이 경로는 Room에서 지정한 경로고, Sqlite는 어떤 경로에 놓아야 한다는 지침을 갖고있지 않습니다. 순전히 설계자의 몫이죠.

kammaii commented 5 years ago

자세히 설명해 주셔서 감사합니다! 이제 이해가 되었습니다! :)

kammaii commented 5 years ago

안녕하세요?

찾아주신 자료로 열심히 공부하고 있는데 좀 이상한 부분이 있어서 다시 문의 드립니다. https://android.jlelse.eu/5-steps-to-implement-room-persistence-library-in-android-47b10cd47b24

이 자료의 step 5:create the repository class 를 보면 아래와 같이 getTask를 LiveData\<Note> 로 정의 하였습니다.

public LiveData<Note> getTask(int id) {
        return noteDatabase.daoAccess().getTask(id);
    }

그리고 Sample implementation의 2.Update 를 보면 아래와 같이 getTask를 Note 타입으로 받고 있습니다.

   Note note = noteRepository.getTask(2);

제가 똑같이 따라 해보니 Note note 부분에서 에러가 발생하고 LiveData\<Note>로 바꾸라고 나옵니다. LiveData\<Note>로 바꾸게 되면 아래와 같이 updateTask를 실행할 수가 없게 됩니다.

noteRepository.updateTask(note);

자료에 오류가 있는 것인지 아니면 제가 놓치고 있는 부분이 있는지 가르쳐 주시면 감사하겠습니다!

Haytsir commented 5 years ago

해당 예제에서 그 부분을 유심히 보진않았는데 많이 혼란스러우셨을 것 같습니다. 죄송합니다.

예제에서 그 부분에 오류가 있는것이고, LiveData는 자체적으로 비동기적으로 값을 가져오기 때문에, 동기적인 코드상에서 받아오고, 처리하는 과정을 모두 할 수 없습니다. (엄밀히 따지자면, 그 LiveData 값을 받기때문에 이 값이 null일 수도 있고, 또는 다른 값일 수 있습니다.)

LiveData 클래스 타입으로 받아온 내용을 observe 메서드를 통해 감시하도록 등록하고,

note.observe(this, new Observer<Note>() {
    @Override
    public void onChanged(@Nullable Note note){

    }
});

처럼 바뀌었을 때 값을 받아와 어떤 행동을 할 것인지 등의 조작을 할 수 있습니다.

kammaii commented 5 years ago

감사합니다! :) 예제가 틀리는 경우도 있군요! 덕분에 쉽게 해결할 수 있었습니다! 👍

kammaii commented 5 years ago

죄송하게도 질문을 다시 오픈하게 되었습니다 ㅜ.ㅜ

Id 값을 통해 entity를 얻을 수 있는 getById()를 위 질문에서 LiveData로 해결을 했었습니다. 하지만 사실 getById()는 LiveData 로 Observe 할 만한 대상이 아닌 것 같습니다. 그냥 필요할 때 실행 시켜서 원하는 값을 얻기만 하면 되니까요. 그래서 LiveData를 사용하지 않고 Repository를 다시 코딩하려고 합니다. 그런데 아래와 같이 return에 대한 문제가 발생합니다.

    public CollectionEntity getById(final int index) {

        new AsyncTask<Void, Void, CollectionEntity>() {
            @Override
            protected CollectionEntity doInBackground(Void... voids) {
                CollectionEntity entity = db.collectionDao().getById(index);
                return entity;
            }
        }.execute();

        return ???;
    }

비동기로 실행 된 database의 결과를 어떻게 return 할 수 있을까요?

사실 그냥 LiveData 로 하면 비동기를 할 필요 없이 바로 return할 수 있어서 이 문제는 해결이 되었지만 해당 id의 데이터를 지우는 코드를 만들 때 LiveData 때문에 문제가 발생하여 다시 질문을 드리게 되었습니다. 위의 예제 자료에서는 deleteTask(final int id)를 정의할 때 task.getValue()를 사용했는데 이 부분도 좀 이상합니다. 저는 이 부분에서 null이 발생했고 제 생각에 애초에 getTask(int id)는 LiveData로 할 필요가 없다는 생각이 들어 예제와 다른 방법으로 해 보려 합니다.

Haytsir commented 5 years ago

AsyncTask의 문서에서 간략한 예제와 함께 자세한 설명을 보실 수 있습니다.

비동기 코드 패턴에서는 return을 통한 데이터 전달을 할 수 없기 때문에, 작업이 모두 완료됐을 때... 등의 event들이 발생할 때 실행되는 Callback 함수(메서드)라는 개념이 사용됩니다. 콜백은 비동기 패턴에서의 핵심 개념 중 하나입니다.

public CollectionEntity getById(final int index) {
    new AsyncTask<Void, Void, CollectionEntity>() {
        @Override
        protected CollectionEntity doInBackground(Void... voids) {
            CollectionEntity entity = db.collectionDao().getById(index);
            return entity;
        }
        @Override
        protected void onPostExecute(CollectionEntity entity) {
            // entity를 사용하는 코드부
        }
    }.execute();
}

AsyncTask는 내부적으로 doInBackground()에서 반환된 값을 onPostExcute()라는 Callback의 매개변수로 사용하도록 약속돼 있습니다.

다른 여러 언어들은 return을 시키는 대신 callback함수를 직접 실행하는 방식으로 동기적인 코드를 비동기적인 패턴으로 바꿉니다.

// 함수의 인자로 callback 함수를 전달받음
function doAsync(callback) {
    var date = new Date();
    var timestamp = date.getTime();

    console.log('an async task');
    setTimeout(function(){ // 5초 뒤에 익명 함수를 실행하는 timeout 생성
        callback(timestamp);  // callback함수 실행
    }, 5000);
}

// 함수를 호출하면서, 인자로 익명의 함수를 전달, 이 익명 함수는 doAsync()의 마지막에 실행된다.
// doAsync()에서 전달해주는 timestamp를 인자로 받는다.
doAsync(function (startTimestamp) {
    var date = new Date();
    var endTimestamp = date.getTime();

    console.log('async task done');
    console.log(endTimestamp - startTimestamp);
});

위 코드는 크롬 등의 브라우저의 Console 탭에 붙여넣어 실행해볼 수 있습니다.

함수를 매개변수로 넘기는 것은 함수형 언어들의 특성인데, 최근 여러 언어들이 함수형 언어의 이런 특성들을 지원하고 있습니다.

안드로이드 Java에서도 OnEventListener 클래스를 생성 인자로 받는 방식 등으로 클래스를 만들어 간편하게 사용할 수 있는 AsyncTask의 자식 클래스를 만들 수 있습니다.

kammaii commented 5 years ago

와! 빠르고 이해하기 쉬운 답변 정말 감사합니다! 비동기에서는 callback의 개념에 대해 알게 되었고 onPostExecute로 간단히 해결했습니다! :) 정말 정말 감사합니다!!