우선 현 프로젝트의 firestore 내부 post페이지의 post data를 작업하다가 고민이 시작되었다. 프로젝트 막바지지만, 그래도 짚고 나갈 것은 짚고 나가야 하기 때문에..
주목해야할 데이터는 다음과 같다.
userId, subjectId
userId를 통해, users 컬렉션 내부를 조회해서 이름을 받아오고,
subjectId를 통해, subjects 컬렉션 내부를 조회해서 주제 이름을 받아오는 것으로 구현하고자 했다.
이는 noSQL의 역정규화를 전혀 사용하지 않고, RDB적으로 설계를 한 것이라는 것을 프로젝트 진행 중에도 알고 있었지만, 이대로 진행한 이유는 다음과 같다.
- user의 데이터를 변화할 일이 생긴다. (프로필 수정)
- subject별 post데이터를 따로 또 저장하고 있다.
- post를 수정할 경우 post, subject를 모두 조회해서 수정해야 한다.
그래서 id를 통해 서로 엮어서 데이터를 저장하는 것이 유리할 것이라고 생각했다.
다시 위 화면으로 돌아가서
위 화면의 userId와 subjectId를 viewmodel 내부에서 요청을 날려 이름과 주제를 가져오는 것을 구현하려고 할때 심각한 의문점이 생겼다.
- 만약 post 데이터가 10만개라고 가정을 하자.
- 각각의 post 만큼 반복문을 돌면서
2-1. userId로 userName을 가져오는 쿼리를 보내야 한다.
2-2. subjectId로 subjectName을 가져오는 쿼리를 보내야 한다.- 카테고리를 변경할 때마다 해당 과정을 계속 반복한다.
즉, posts 조회 쿼리 하나당, 2 * post 개수 만큼의 쿼리를 추가로 보내 가져와야 하는 상황이 생긴 것이다. (찾아보니 collectionGroup이라는 게 있어서 좀 더 간소화되게 보낼 수 있는 방법은 있지만, 그게 요청횟수를 줄일 수 있을거 같아 보이지는 않았다.)
아무리 생각해도 이는 너무 비효율적이서 다시 찾아보았다.
크게 3가지 정도의 해결책이 있다.
DB 쪽에서 join을 지원하지 않는 이 상황에서 읽기 성능을 최적화하고, 요청을 줄일 수 있는 방법은 역정규화밖에 없다는 생각이 들었다. 즉, userName, subjectName, comments 내부 userName 모두 추가해주는 방식이다.
이렇게 되면 원점이다. 수정, 삭제의 효율성을 따져 id를 활용한 join 설계를 했는데, 이제와서 역정규화로 name들을 중복 저장한다면, 어떻게 수정 삭제를 할수 있을 것인가라는 생각이 들어 실 사례를 찾아보았다.
읽기 성능이 더 중요한 서비스에서 역정규화를 사용하고 있었다. user 데이터 변화가 빈번한 소셜 미디어에서 사용하고 있는 것이 신기했지만, 수많은 게시물들마다 매번 테이블간 조인을 하는 것은 엄청난 부하가 걸릴 테니 당연하다는 생각도 들었다.
밑에서 설명하겠지만, 그들은 수정 삭제를 여러가지 전략을 통해 최적화하고 있었다.
읽기 성능을 높이기 위해 역정규화를 한다면, 쓰기에 부하가 걸린다.
쓰기 성능을 높이기 위해 RDB 방식으로 한다면, 읽기에 부하가 걸린다.
읽기 성능을 높이기 위한 목적으로 설계된 noSQL을 사용한다면(테이블 내 join 기능이 없다면), 역정규화를 사용한 뒤에, 쓰기, 삭제 등의 데이터 업데이트는 캐싱 혹인 비동기 전략을 구성해서 작업해야 한다는 어느정도의 결론을 내릴 수 있겠다.
이미 설계된 프로젝트고 마감이 얼마 안남았지만, 그래도 효율적으로 컨트롤할수 있는 방법을 고민해서 최대한 적용해보겠다. 기회가 되면 변경된 설계를 소개해보도록 하겠다.
역정규화를 하지 않기로 했다...
그런데, firestore가 자체 캐시를 지원한다고 한다.
persitenceEnabled 옵션을 통해 백엔드에서 수신된 모든 문서를 오프라인 액세스용으로 캐시를 하고, 자동으로 동기화를 해준다고 한다. 이로 인해 한번 받은 데이터는 캐시를 통해 가져올 수 있다.
db
.collection("cities")
.where("state", isEqualTo: "CA")
.snapshots(includeMetadataChanges: true)
.listen((querySnapshot) {
for (var change in querySnapshot.docChanges) {
if (change.type == DocumentChangeType.added) {
final source =
(querySnapshot.metadata.isFromCache) ? "local cache" : "server";
print("Data fetched from $source}");
}
}
이 샘플 코드를 통해 해당 데이터가 어디서 받아오는지를 확인할 수 있다고 하니, 이 기능을 몰랐던(나같은) 분들은 한번 확인해보기를 바란다.