Firebase 실시간 데이터베이스(Realtime database) 예제를 쉽게 이해하기 위해 전체적인 개념을 정리하였고,

여기서는 주요 코드에 대한 설명을 정리한다.

여기에서는 Firebase 실시간 데이터베이스를 이용하여 구현한 코드(Android)들을 중심으로 정리한다.

Firebase 실시간 데이터베이스에 대한 구체적인 설명은 관련 문서를 한번 읽어 보길 바란다.


1. Firebase realtime database 실행과 테스트

2. Firebase realtime database 구조와 프로그램 구조

3. Firebase realtime database 코드

4. Firebase realtime database를 Cloud Firestore로


포스트(post) 예제는 데이터 베이스 단순 입출력 예제로 단순 게시판과 유사하기 때문에

편의상 포스트(post)라는 단어보다 [글]이라는 단어로 사용한다.

먼저, 글쓰기(NewPostActivity.java)부터 정리한다.

글쓰기에서는 사용자가 작성한 글 제목(title)과 내용(body)을 Firebase 실시간 데이터베이스에 저장하는 기능을 구현한다.

NewPostActivity.java

코드가 복잡해 보이지만

사용자가 저장 버튼을 누르면 전처리를 위한 submitPost()함수[라인 4]를 호출한다.

submitPost()에서는 사용자 이름을 조회해서[라인6] 없으면[라인 14] 오류를 발생시키고,

조회가 되면 실제 데이터를 저장하는 writeNewPost() 함수를 호출한다.

현재 글을 작성하는 사용자의 UID(고유번호) [라인 5]로 사용자 이름을 조회하는 것은 글을 저장할 때 같이 저장하기 위해서 이다.

이 방식은 좋은 방식이 아니지만,

Firebase 실시간 데이터베이스가 NoSQL로 RDBMS의 Join과 같은 기능이 없어서 이렇게 구현한 것 같다.

writeNewPost() 함수에서는 사용자가 입력한 값(title, body)을 사용자 이름, 사용자 UID와 함께Post 개체(모델)에 담아서 저장한다 [라인 45].


여기서 사용된 저장 방식은 관련 문서에 없는 내용으로 제법 유용한 팁을 제공해 준다.

먼저, 데이터가 중복되면 안되기 때문에 getKey()을 이용하여 고유한 값(Primary Key, Unique value)을 받아온다 [라인 44].

저장할 정보를 Post 개체(모델)로 생성하고 [라인 45],

생성한 Post 개체를 Map로 변환한다 [라인 46].

Firebase 실시간 데이터베이스는 Json형태(Map과 비슷)로 저장하기 때문이다.


다시 Map을 하나 더 생성한다 [라인 48].

이 맵은 작성한 글을 두 군데 동시에 저장하기 위해서 사용한 방법이다.

앞서서 데이터 베이스 구조에서 정리하였지만

사용자가 작성한 글은 전체 게시글(posts) [라인 49]과 작성자별 게시글(user-posts)에 [라인 50] 각각 저장한다.

이렇게 하는 것은 Firebase 실시간 데이터베이스가 검색 기능이 약하기 때문이다.

전체 게시글에 데이터를 저장하고, 지정된 사용자 글만 가져오도록 구현 할 수 없다(?).

따라서, 기능에 맞추어 데이터를 미리 가공해 두어야 한다.


전체 글은 posts 스키마 하위의 고유값(key) 아래에 저장하고 [라인 49]

사용자별 게시글은 user-posts 스키마 하위의 사용자별 UID 하위에 고유값(key) 아래에 저장한다 [라인 50]

사용자별 UID 하위에 저장해야 사용자별 글만 가져올 수 있다.

updateChildren으로 한번에 두 개의 스키마에 저장한다 [라인 52].


Firebase 콘솔로 접속해서 다음과 같이 저장된 결과를 볼 수 있다.

전체 글은 posts 스키마 하위의 고유값(key) 아래에 ,

사용자별 게시글은 user-posts 스키마 하위의 사용자별 UID 하위에 고유값(key) 아래에 저장된 것을 볼 수 있다.


저장 기능을 구현한 writeNewPost()의 내용은 다음과 같이 작성할 수 있다.

    private void writeNewPost(String userId, String username, String title, String body) {
        String key = mDatabase.child("posts").push().getKey();
        Post post = new Post(userId, username, title, body);
        mDatabase.child("posts").child(key).setValue(post);
        mDatabase.child("user-posts").child(userId).child(key).setValue(post);

}

이 내용은 Firebase 문서에 있는 방식으로 구현한 것으로 저장(setValue)을 두번 실행하도록 작성한 것이다.

데이터 계층은 슬래쉬(/)로 작성해도 되고 child로 지정해서 작성해도 된다.


간단해 보여서 좋아 보지만

예제가 이렇게 작성하지 않은 이유는 Transaction처리 때문으로 보여 진다.

위와 같이 작성하면, posts에 저장하고 user-posts에 저장하다 여러가지 이유로 문제가 생겨서 저장하지 못 할 경우

두 스키마 간에 불일치가 발생할 수 있다.

둘다 없거나 둘다 있어야지 하나만 저장되면 안되기 때문에 두 스키마에 저장할 경우에는 Transaction처리 해야한다.


Firebase에서는 Transaction처리를 지원하는데 (문서 하단 참조), Transaction의 의미가 RDMBS와는 조금 다른 것 같다.

이 부분은 좀더 사용해 보고 찾아보고 차후에 추가할 예정이다.

다음 예제에서는 Transaction을 사용하였다.


이번에는 글 리스트(PostListFragment)에 대해서 정리한다.

앞서 정리했지만, 글 리스트는 세가지가 존재하고 PostListFragment에 모든 기능이 구현되어 있다.

코드가 많아 보이지만, 주의 깊게 봐야할 코드는 FirebaseRecyclerAdapter와 Transaction 사용이다.

PostListFragment.java

리스트를 사용하기 위해 RecyclerAdapter를 사용하는데,

좀더 쉽게 사용할 수 있는 기능을 제공하는 FirebaseRecyclerAdapter가 사용되었고 [라인 17],

사용자가 게시물에 별표시(좋아요)를 눌렀을 떼, 관련 처리를 하기 위해Transaction을 사용하였다 [라인 68].

하나씩 정리하면 FirebaseRecyclerAdapter은 RecyclerAdapter와 대부분 동일하지만 데이터를 관리하는 기능을 좀더 단순화 시켰다.

RecyclerAdapter는 출력할 데이터들을 List나 ArrayList 형태의 변수로 보관하고 있지만

위 코드에서는 이것과 관련된 코드를 볼 수 없다.

RecyclerAdapter에서 화면에 출력할(onBindViewHolder) 때에는 [라인 26]

다음 코드처럼 현재 출력하는 데이터 순번(position)으로 List나 ArrayList 변수에서 해당 개체를 찾아서 처리한다.

          Post model = List.get(position)

FirebaseRecyclerAdapter에서는 onBindViewHolder의 마지막 파라메터에 현재 출력하려는 데이터 개체를 지정해서(Post model) 반환해 준다.

즉, Post 클래스에 값을 넣어서 model 개체로 제공된다 [라인 26].

이렇게 제공된 model의 값을 각각의 view(TextView등)에 넣어서 출력한다 [라인 49].

기본적인 값 출력은 별도의 파일인 ViewHolder의 bindToPost에 작성되어 있다[라인 49].


이렇게 쉽게 사용하기 위해, 데이터를 조회하도록 하는 부분에 차이가 있다.

Firebase는 데이터를 가지고 오는 조건에 ValueEventListener나 SingleValueEventListener를 사용한다.

여기에서는 그런 지정없이 FirebaseRecyclerAdapter 생성시에 데이터 조건을 FirebaseRecyclerOptions으로 지정하여 처리한다 [라인 11~17].

사용법의 차이일 뿐이니 기억해서 사용하면 된다.

다른 곳에서 값을 변경하면 자동으로 반영되는 것을 볼 때,

FirebaseRecyclerAdapter는 ValueEventListener를 사용하는 것 같다.

(SingleValueEventListener을 사용하면 한번만 호출하는 것으로 값이 변해도 반영되지 않는다.)


다음으로 주의 깊게 봐야 하는 것은 Transaction 처리 방법이다 [라인 68~].

사용자가 별 표시를 누르면,

사용자 uid를 해당 글의 stars 필드에 넣어주거나 [라인 83] 빼고 [라인 79]

별의 개수를 증가 [라인 82] 시키거나 감소[라인 78]시킨다.

문서에 따르면, 여러 사용자가 동시에 별 표시를 눌렀을 경우를 대비하여 구현하였다고 한다.

전체 게시글 [라인 57]과 사용자 게시글을 [라인 58]을 따로 Transaction처리하였는데

여기에 Transaction을 처리하는 것이 맞지 않나 라고 생각한다.


이외에 게시물 하나(행)을 선택하면 게시물 읽기로 가기 위해

행에 클릭 이벤트 핸들러를 작성하였다 [라인 31].

행은 viewHolder의 itemView을 의미한다.


마지막으로 글 읽기(PostDetailActivity.java)를 정리한다.

코드 양이 많아 게시글 읽기, 댓글 저장, 댓글 리스트로 기능에 따라 3가지로 나누어 정리한다.




PostDetailActivity.java


게시글 읽기는 전형적인 Firebase 실시간 데이터베이스의 데이터 가져오기 코드로 작성되었다.

글 리스트에서 사용자가 선택한 글번호(EXTRA_POST_KEY)를 받아와서 [라인 10],

Firebase 실시간 데이터베이스에서 해당 글 내용을 가지고 오도록 조건을 설정한다 [라인 16].

지정된 데이터는 ValueEventListener [라인 30]을 이용하여 받아서 화면에 출력하도록 [라인 34~38]

주어진 조건을[라인 16] 실행한다 [라인 52].


addValueEventListener를 사용하여 다른 곳에서 수정하여도, 수정된 내용이 반영되도록 하였다.

(수정 기능이 없으므로 Firebase 콘솔에서 수정해 보면 알 수 있다.)

addValueEventListener는 해당 Activity가 종료되어도 계속 작동하기 때문에 별도의 핸들러를 생성하여 [라인 16],

Activity가 종료될 때(onStop) 해당 핸들러를 종료시켜야 한다 [라인 69].


PostDetailActivity.java

댓글 저장은 (postComment) 게시글 저장과 동일한 방식으로 작성되었다.

해당 글 작성자의 UID로 [라인 2] 작성자의 이름을 가지고 와서 [라인 3~, 특히 라인 9]

Comment 모델에 필요한 정보(UID, 이름, 댓글)을 지정하여 개체를 생성하고 [라인 13]

이 개체(comment)를 setValue로 저장하였다 [라인 16].

일반적인 데이터 저장 방식이다.


작성자 이름은 한번만 호출해서 가져오면 되기 때문에 addListenerForSingleValueEvent가 사용되었다 [라인 4].


글 읽기의 마지막 기능으로 댓글 리스트가 구현되었다.

이 코드에서 재미 있는 코드는 childEventListener이다.

PostDetailActivity.java

ValueEventListener는 조회 중인 데이터에 변화가 생기면 전체 리스트를 다시 반환하지만

childEventListener는 변화가 생긴 데이터에 대하여만 반환하고 발생한 기능 이벤트 별로 처리한다.

즉, 어떤 글(데이터)이 추가 되거나(add), 수정(Changed) 또는 삭제(Removed)되면 해당 이벤트에 대하여 각각 발생 발생한다.

여기서는 새로운 댓글이 추가되면 onChildAdde가 실행되고 [라인 18]

추가된 글 내용이 반환된다 [라인 22].

추가된 글을 데이터를 관리하는 리스트에 추가하고 [라인 26, 27],

RecyclerView에 마지막에 행이 추가되었다고(notifyItemInserted()) 알려준다 [라인 28].

댓글이 수정되면 동일한 방식으로 onChildChanged가 실행되고 [라인 33],

데이터 관리하는 리스트에서 수정된 데이터를 찾아서 [라인 42] 수정하고 [라인 45]

RecyclerView에 지정된 행이 수정되었다고(notifyItemChanged()) 알려준다 [라인 48].


삭제도 같은 방식으로 작성되었다 [라인 56].

데이터의 위치가 바뀌면 발생하는 onChildMoved는 예제로 작성되지 않았다 [라인 79].


childEventListener는 처리가 발생한 데이터에 대하여 구체적인 기능에 따라 처리하기 때문에 효율적으로,

보다 상세한 기능 설명은 관련 문서를 읽어보는 것이 좋다.

ValueEventListener는 모든 데이터에 대하여 처리하므로 비효율적이지만 코딩 양이 적은 장점이 있다.

이런 것이 왜 Firebase 실시간 데이터베이스(Realtime database)라고 하는지에 대한 이유가 될 것 같다.


이외에 조금 특이한 작성법은 mCommentIds와 mComments로, 2 개의 List를 사용한 것이다.

mComments는 Comment형으로 선언되었고[라인 8] 댓글 정보를 저장하는 리스트이다 [라인 27].

mCommentIds는 문자열(String)로 선언되었고[라인 7] 댓글 번호를(getKey) 가지고 있다 [라인 26].


수정(onChildChanged)과 삭제(onChildRemoved) 이벤트가 발생되었을때, 

수정된 행의 정보만 가지고 있기 때문에 수정된 행이 mComments의 몇 번째 글인지 알 수가 없다.

몇 번째 인지 알아야 mComments에서 수정하거나 삭제하고 RecyclerView도 갱신할 수 있다.

몇 번째 인지 알기 위해서는 반환된 정보에 있는 글번호(key)로 mComments의 개수만큼 돌면서 같은 값을 찾으면 된다.

다소 복잡한 코드를 작성해야 하는데,

이러한 코드를 줄이기 위해 mComments와 동일한 순서로 mCommentIds에 글 번호를 저장하고

mCommentIds의 indexOf로 찾은 위치가 mComments에서의 위치인 것이 된다.

mCommentIds는 문자열로 구성되니 indexOf를 사용할 수 있지만,

mComments는 Comment로 구성되어 indexOf와 같은 검색 기능을 사용할 수 없기 때문에 이렇게 구현 한 것 같다.




+ Recent posts