본문 바로가기
깡샘 코틀린

21-1 파이어스토어 데이터베이스

by 농농씨 2023. 7. 14.

이번 장에서는 파이어스토어 데이터베이스와 스토리지에 데이터나 파일을 저장하고 불러오는 방법을 살펴볼 것이다. 그리고 앱 사용자에게 알림을 보낼 수 있는 클라우드 메시징 기능도 살펴볼 것이다. 참고로 이번 장의 내용은 이전 장에서 이어지므로 반드시 20장의 실습까지 모두 마친 후에 학습하기 바란다. 

 

파이어베이스는 파이어스토어 데이터베이스(Firestore Database)와 실시간 데이터베이스(Realtime Database) 이렇게 2가지 클라우드를 기반으로 한 데이터베이스를 제공한다. 실시간 데이터베이스는 여러 클라이언트에서 상태를 실시간으로 동기화해야 하는 모바일 앱을 만드는 솔루션이다. 그리고 파이어스토어 데이터베이스는 실시간 데이터베이스보다 더 많고 빠른 쿼리를 제공한다. 이 책에서는 더 최신인 파이어스토어의 사용법을 살펴보겠다.

 

 

파이어스토어 사용 설정

파이어스토어를 사용하려면 먼저 파이어베이스에서 새로운 데이터베이스를 만들어야 한다. 파이어스토어를 만드는 방법은 21-3에서! 여기서는 안드로이드 앱에서 파이어스토어 사용방법에 집중!

 

파이어베이스에 데이터베이스를 만들었으면 그다음으로는 모듈 수준의 빌드 그래들 파일에 다음처럼 파이어스토어 라이브러리를 등록해야 한다.

// 파이어스토어 사용 등록
dependencies {
    (... 생략 ...)
    implementation 'com.google.firebase:firebase-firestore-kts:21.2.1'
}

 

 

파이어스토어 데이터 모델

파이어스토어는 NoSQL 데이터베이스라서 SQL 데이터베이스와 달리 테이블이나 행이 없다. 그 대신 컬렉션으로 정리되는 문서에 데이터가 저장된다.

각 문서에는 키-값 쌍의 데이터가 저장되며 모든 문서는 컬렉션에 저장된다. 파이어스토어에 레스토랑 정보를 저장한 예시를 가정하자. 레스토랑마다 문서 1개에 데이터를 저장하고 각 문서에는 name, city 등 레스토랑 정보를 키-값으로 저장한다. 그리고 모든 레스토랑 문서는 다시 restaurant라는 컬렉션에 저장한다. 그리고 문서에는 하위 컬렉션(sub collection)도 포함할 수 있다. 레스토랑 정보를 저장한 문서에 ratings라는 하위 컬렉션을 포함할 수 있다.

 

파이어스토어 보안 규칙

파이어스토어에 저장된 데이터를 이용할 때는 다양한 보안 규칙을 설정할 수 있다. 예를 들면 모든 데이터를 조건 없이 앱에서 읽거나 쓸 수 있게 설정할 수도 있고, 읽는 데는 조건이 없지만 로그인했을 때만 쓸 수 있게 설정할 수도 있다.

파이어스토어의 보안 규칙은 콘솔의 [규칙] 탭에서 설정할 수 있으며 match와 allow 구문을 조합해서 작성한다. match 구문으로 데이터베이스 문서를 식별하고 allow 구문으로 접근 권한을 작성한다.

 

파이어베이스에서 [프로덕션 모드]로 베이터베이스를 만들면 보안 규칙이 기본으로 다음처럼 작성된다. 이 규칙은 모든 문서의 읽기와 쓰기를 거부한다.

// 모든 문서의 읽기/쓰기 거부
rules_version = '2' ;
service cloud.firestore {
  match /database/{database}/documents { // match 구문으로 데이터베이스 문서 식별
    match /{document=**} { // 모든 문서
      allow read, write: if false; // allow 구문으로 접근 권한에서 읽기/쓰기 거부
    }
  }
}

만약 규칙을 다음처럼 고치면 반대로 모든 문서의 읽기와 쓰기를 허용한다.

// 모든 문서의 읽기/쓰기 허용
rules_version '2' ;
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if true; // 읽기/쓰기 허용
    }
  }
}

인증된 사용자에게만 모든 문서의 읽기와 쓰기를 허용하는 규칙은 다음처럼 작성한다. 앱에서 파이어베이스의 인증 서비스를 사용한다면 데이터를 요청하는 사용자의 인증 정보request.auth 변수에 포함된다.

// 인증된 사용자에게만 모든 문서의 읽기/쓰기 허용
rules_versino = '2' ;
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if request.auth.uid != null; // 인증된 사용자에게만 읽기/쓰기 허용
    }
  }
}

allow 구문에서 쓰기 권한을 나타내는 write문서의 생성, 수정, 삭제를 포함한다. 만약 쓰기 권한을 따로 지정하고 싶다면 create, update, delete를 사용한다. 또한 match 구문 하나에 allow 구문을 나열해 여러 가지 조건을 설정할 수도 있다. 다음은 이러한 내용을 활용해 사용자가 자신의 데이터만 읽고 쓸 수 있게 설정한 것이다.

// 자신의 데이터만 읽기/쓰기 허용
rules_version = '2' ;
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read, update, delete: if request.auth.uid == userId;
      // 자신의 데이터만 읽기read, 수정update, 삭제delete 허용-
      allow create: if tequest.auth.uid != null;
      // 인증된 사용자에게만 문서 생성 허용, match 구문속에 두번째로 나열된 allow 구문
    }
  }
}

조건을 설정할 때 문서에 저장된 데이터를 이용할 수도 있다. 다음 예에서 resource.data는 문서에 저장된 데이터를 의미한다.

// 문서에 저장된 데이터 활용
rules_version = '2' ;
service cloud.firestore {
  match /databases/{database}/documents {
    match /cities/{city} {
      allow read: if resource.data.visibility == 'public';
      // 문서의 visibility 값이 public일 때만 읽기 허용
    }
  }
}

또한 사용자에게 전달받은 데이터를 데이터베이스에 저장된 데이터와 비교할 수도 있다. 다음 예에서 request.resource.data는 사용자에게 전달받은 데이터를 의미한다.

// 전달받은 데이터 활용
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {{
      allow update: if request.resource.data.population > 0
                    && request.resource.data.name == resource.data.name;
      // 전달받은 데이터가 0 이상일 때만 population 데이터 수정 허용. 단, name 데이터는 수정할 수 없음
    }
  }
}

 

 

 

데이터 저장하기

파이어베이스 콘솔에서 파이어스토어를 만들고 보안 규칙을 설정했다면 이제 안드로이드 앱에서 파이어스토어를 이용할 수 있다. 이때 가장 먼저 파이어스토어 객체를 얻어야 한다. 이렇게 얻은 FirebaseFirestore 객체로 컬렉션을 선택하고 문서를 추가하거나 가져오는 작업을 한다.

// 파이어스토어 객체 얻기
val db: FirebaseFirestore = FirebaseFirestore.getInstance()

 

add() 함수로 데이터 저장하기

파이어스토어에 데이터를 저장하는 방법을 살펴보자. 파이어스토어에는 데이터가 문서 단위로 저장되고 문서는 컬렉션에 저장된다. 따라서 데이터를 저장하려면 먼저 1️⃣컬렉션을 선택하고 2️⃣문서 작업을 하는 CollectionReference 객체를 얻어야 한다.

다음 코드에서 db.collection("users")는 users라는 컬렉션을 선택하는 구문이며 만약 해당 컬렉션이 없으면 새로 만든다. 그리고 collection() 함수의 반환값인 CollectionReference 객체의 add(), set(), get() 등의 함수로 문서 작업을 한다.

// add() 함수로 데이터 저장
val user = mapOf(
    "name" to "kkang",
    "email" to "a@a.com",
    "avg" to 10
)
val colRef: CollectionReference = db.collection("users")
// 컬렉션을 선택하는 구문, 해당 컬렉션이 없으면 새로 만듦
val docRef: Task<DocumentReference> = colRef.add(user) 
// 위의 컬렉션 선택 함수의 반환값인 CollectionReference 객체의 add() 함수로 문서작업하기
// colRef.add(user)는 지정된 컬렉션에 문서 저장함. 그 문서의 데이터는 여기서는 Map 형태로 저장됨
// add() 함수는 문서의 작업 객체인 Task<DocumentReference>를 반환함. 이 객체의 콜백함수로 성공여부 판단.
docRef.addOnSuccessListener { documentReference ->
// 성공 콜백
    Log.d("kkang", "DocumentSnapshot added with ID: $ {documentReference.id}")
    // 성공 콜백 함수의 매개변수인 documentReference의 id 프로퍼티로 저장된 문서를 식별할 수 있음
}
docRef.addOnFailureListener { e ->
// 실패 콜백
    Log.w("kkang", "Error document", e)
}

colRef.add(user)는 지정된 컬렉션에 문서를 저장한다. 문서의 데이터는 키-값으로 저장되며 위 예처럼 Map 형태로 저장하거나 객체의 데이터를 저장할 수도 있다. add() 함수는 문서의 작업 객체인 Task<DocumentReference>를 반환한다. 이 객체의 addOnSuccessListener의 addOnFailureListener 콜백 함수를 이용해 성공/실패를 판단할 수 있다. 그리고 성공할 때 호출되는 콜백 함수의 매개변수인 documentReference의 id 프로퍼티로 저장된 문서를 식별할 수 있다.

그런데 보통은 함수의 결과를 변수에 저장해서 사용하지 않고 다음처럼 줄여서 작성한다.

// add() 함수로 데이터 저장
db.collection("users")
    .add(user)
    .addOnSuccessListener { documentReference ->
        Log.d("kkang", "DocumentSnapshot added with ID: ${documentReference.id}")
    }
    .addOnFailureListener { e ->
        Log.w("kkang", "Error adding document", e)
    }

파이어스토어에 저장된 데이터는 파이어베이스 콘솔에서 확인할 수 있다.

 

객체 저장하기

앞선 예에서는 데이터를 Map으로 저장했는데 객체의 데이터를 저장할 수도 있다. 다음은 Users 컬렉션에 User 클래스의 객체를 저장하는 예이다. 앞서 Map으로 저장한 문서의 필드는 name, email, avg였지만, 다음 예에서는 name, email, avg, isAdmin, isTop이다. 이처럼 같은 컬렉션에 저장된 문서는 필드가 달라도 된다.

// 객체 저장하기
class User(val name: String, val email: String, val avg: Int,
    @JvmField val isAdmin: Boolean, val isTop: Boolean) 
    // 데이터를 문서로 저장할 때 필드의 키는 객체의 프로퍼티명이 됨.
    // isAdmin 프로퍼티는 프로퍼티명을 문서의 키로 그대로 사용하기 위해 @Jvmield 애너테이션 추가함
val user = User("kim", "kim@a.com", 20, true, true)
db.collection("users")
    .add(user)

객체의 데이터를 문서로 저장할 때 필드의 키는 객체의 프로퍼티명이 된다. 그런데 이때 주의할 점이 있다. Boolean 타입으로 선언된 프로퍼티명이 'is'로 시작한다면 필드의 키에서는 'is'가 제거된다. 따라서 User 클래스의 isTop 프로퍼티가 문서에 저장될 때는 다음처럼 'is'가 제거된 top이라는 키로 저장된다. 만약 Boolean 타입의 프로퍼티명을 문서의 키로 그대로 사용하려면 @JvmField 애너테이션을 추가해야 한다. 앞의 예에서 isAdmin도 Boolean 타입이며 프로퍼티명이 'is'로 시작하지만, @JvmField 애너테이션을 추가해서 프로퍼티명 그대로 문서에 저장되었다.

 

set() 함수로 데이터 저장하기

앞에서 살펴본 add() 함수 외에 set() 함수로도 데이터를 저장할 수 있다. set() 함수는 신규 데이터뿐만 아니라 기존의 데이터를 변경할 때도 사용된다. add() 함수는 CollectionReference 객체에서 제공하므로 문서를 추가할 때 식별자가 자동으로 지정된다. 하지만 set() 함수는 DocumentReference 객체에서 제공하므로 document() 함수로 작업 대상 문서를 먼저 지정해야 한다.

// set() 함수로 데이터 저장
val user = User("lee", "lee@a.com", 30)
db.collection("users")
    .document("ID01") // set() 함수 사용하여 데이터 저장하기 전 작업대상 문서 지정함
    .set(user)

document("ID01")이라고 지정하면 식별자가 ID01인 문서를 가리킨다. 만약 컬렉션에 이 문서가 없으면 새로 만들어 데이터를 추가하고, 문서가 있으면 해당 문서 전체를 덮어쓴다. 즉, 문서가 업데이트된다.

 

 

데이터 업데이트와 삭제

파이어스토어에 저장된 문서의 데이터는 앞에서 살펴본 set() 함수를 이용해 업데이트할 수 있다. 하지만 set() 함수는 문서 전체를 덮어쓰므로 만약 기존 문서의 특정 필드값만 업데이트하려면 update() 함수를 이용해야 한다.

 

update() 함수로 데이터 업데이트하기

update() 함수로 문서의 특정 필드를 업데이트하려면 먼저 document() 함수로 문서를 지정해야 한다. 그리고 update() 함수의 매개변수로 업데이트할 필드명과 값을 지정해 준다. 다음은 ID01 문서의 email 필드값을 lee@b.com으로 업데이트하는 구문이다.

// 특정 필드값만 업데이트
db.collection("users")
    .document("ID01") // update() 함수로 문서 업데이트 하기 전에 먼저 문서를 지정함
    .update("email", "lee@b.com") // 매개변수로 업데이트할 필드명과 값을 지정함

만약 update() 함수로 여러 필드값을 한꺼번에 변경하고 싶다면 매개변수를 Map 객체로 지정하면 된다.

// 여러 필드값 업데이트
db.collection("users")
    .document("ID01") // 문서 지정
    .update(mapOf( // 매개변수를 Map 객체로 지정
        "name" to "lee01"
        "email" to "lee@c.com"
    ))

 

delete() 함수로 데이터 삭제하기

문서나 문서의 필드값을 삭제할 때는 delete() 함수를 이용한다. 특정 필드값을 삭제하려면 update() 함수로 필드값을 지정하고 FieldValue.delete() 함수를 호출한다.

// 특정 필드값 삭제
db.collectio("users")
    .document("ID01") // 문서 지정
    .update(mapOf( // update() 함수로 
        "avg" to FieldValue.delete() // 필드값 지정하고 함수 호출해서 특정 필드값 삭제함
    ))

만약 문서 전체를 삭제하려면 document() 함수로 문서를 지정하고 delete() 함수를 호출한다.

// 문서 전체 삭제
db.collection("users")
    .document("ID01") // 문서 지정
    .delete() // 문서 전체 삭제

 

 

데이터 불러오기

컬렉션의 문서를 가져올 때get() 함수를 이용한다. 이 함수는 컬렉션의 모든 문서나 단일 문서, 또는 조건에 맞는 문서를 가져온다.

 

get() 함수로 컬렉션의 전체 문서 가져오기

컬렉션의 전체 문서를 가져올 때는 컬렉션을 지정한 다음 get() 함수를 호출한다. get() 함수로 가져온 문서는 addOnSuccessListener의  콜백으로 지정한 함수의 매개변수가 되며, 가져온 문서의 데이터는 document.data처럼 매개변수의 data 프로퍼티에 있다.

// 전체 문서 가져오기
db.collection("users") // 문서 가져올 컬렉션 지정
    .get() // get() 함수 호출
        .addOnSuccessListener { result ->
        // get() 함수로 가져온 문서는 adOnSuccessListener의 콜백으로 지정한 함수의 매개변수 됨
            for (document in result) {
                Log.d("kkang", "${document.id} -> ${document.data}")
                // 가져온 문서의 데이터는 document.data처럼 매개변수의 data 프로퍼티에 있음
            }
    }
    .addOnFailuteListener { exception ->
        Log.d("kkang", "Error getting document; ", exception)
    }

 

get() 함수로 단일 문서 가져오기

만약 단일 문서를 가져오려면 document() 함수에 식별값으로 문서를 지정한 다음 get() 함수를 호출하면 된다. 다음 예에서는 식별값이 ID01인 문서를 가져온다.

// 단일 문서 가져오기
val docRef = db.collection("users".document("ID01")) 
// 단일문서 가져오기 위해 document() 함수에 식별값으로 문서 지정한 다음
docRef.get() // get() 함수를 호출함
    .addOnSuccessListener { document ->
        if ( document != null) {
            Log.d("kkang", "documnetSnapshot data: ${document.data}")
        } else {
            Log.d("kkang", "No such document")
        }
    }
    .addOnFailuteListener { exception ->
        Log.d("kkang", "get failed with ", exception)
    }

 

이렇게 가져온 문서를 객체에 담아서 사용할 때는 콜백 매개변수의 toObject() 함수를 이용한다. 이 함수에 클래스를 지정하면 문서의 데이터를 자동으로 객체에 담아준다. 이때 주의할 점은 toObject() 함수에 지정하는 클래스는 매개변수가 없는 생성자가 있어야 하며 클래스의 프로퍼티가 public 게터 함수를 가져야 한다.

// 문서를 객체에 담기
class User{
    var name: String? = null
    var email: String? = null
    var avg: Int = 0
}
val docRef = db.collection("users").document("ID01")
docRef.get().addOnSuccessListener { documentSnapshot ->
// 콜백 매개변수의
    val selecUser = documentSnapshot.toObject(User::class.java)
    // toObject() 함수를 이용하여 이 함수에 클래스를 지정하면 문서의 데이터를 자동으로 객체에 담아줌
    // !주의! toObject() 함수에 지정하는 클래스는 매개변수가 없는 생성자가 있어야 하며 ex.User()
    // 클래스의 프로퍼티가 public 게터 함수를 가져야 한다
    Log.d("kkang", "name: ${selecctUser?.name}")
}

 

 

whereXXX() 함수로 조건 설정

이번에는 조건에 맞는 문서만 가져오는 방법을 알아보자. 먼저 CollectionReference 객체의 whereXXX() 함수로 조건을 지정한 Query 객체를 만들고, 그 Query 객체의 get() 함수를 호출하면 된다. Query 객체를 만드는 함수로는 whereEqualTo(), whereGreaterThan(), whereIn(), whereArrayContains(), whereLessThan(), whereNotEqualTo(), whereNotIn() 등을 제공한다.

// 조건에 맞는 문서 가져오기
db.collection("users") // CollectionReference 객체의 
    .whereEqualTo("name", "lee")
    // whereXXX() 함수로 조건을 지정한 Query 객체를 만들고
    .get() // 그 Query 객체의 get() 함수 호출함
    .addOnSuccessListener { document ->
        for (document in documents) {
            Log.d("kkang", "${documnet.id} => ${document.data}")
        }
    }
    .addFailureListener { exception ->
        Log.w("kkang", "Error getting documents: ", exception)
    }