Skip to content

Conversation

@wontaeyoung
Copy link
Collaborator

개요

메인 데이터소스인 Firestore를 조작할 수 있는 추상화 기능을 제공하는 Firestore Service 구현했습니다.



공유사항

이전 회의에서 라이브로 공유드린 문서 디코딩 시 런타임 에러가 발생하는 이슈가 Firebase 패키지 버전 업데이트 후 해결된 부분을 확인했습니다.

#21 브랜치에서 Firebase 패키지 버전을 업데이트했습니다.

  • as-is : 10.21.0
  • to-be : 10.22.1



✏️ 작업 사항

  • GSNetowrk 모듈 추가
  • Firestore Service 인터페이스 프로토콜 정의
  • Firestore Service 구현체 LiveFirestoreService 구현



사용법

더미 데이터

let chat: Chat = .dummy
let message: Message = .dummy    



문서 생성

/// 지정된 컬렉션에 모델을 생성합니다.
/// - Parameters:
///   - in collection: 모델을 생성할 컬렉션입니다.
///   - with model: 저장할 모델의 인스턴스입니다.
func create<T: GSModel>(
    in collection: FirestoreCollection,
    with model: T
) throws


try service.create(in: .chat, with: chat)



/// 지정된 컬렉션에 모델을 생성합니다.
/// - Parameters:
///   - superCol superCollection: 상위 컬렉션입니다.
///   - superDoc superDocumentID: 상위 문서의 ID입니다.
///   - in collection: 모델을 생성할 컬렉션입니다.
///   - with model: 저장할 모델의 인스턴스입니다.
func create<T: GSModel>(
    superCol superCollection: FirestoreCollection,
    superDoc superDocumentID: String,
    in collection: FirestoreCollection,
    with model: T
) throws


try service.create(
  superCol: .chat,
  superDoc: chat.id,
  in: .message,
  with: message
)



문서 조회

/// 지정된 컬렉션의 모든 문서를 조회합니다.
/// - Parameters:
///   - from collection: 조회할 컬렉션입니다.
/// - Returns: 해당 컬렉션의 모든 모델을 포함하는 배열을 반환합니다.
func fetch<T: GSModel>(
    from collection: FirestoreCollection
) async throws -> [T]


let fetchedChats: [Chat] = try await service.fetch(from: .chat)



/// 지정된 컬렉션과 문서 ID를 사용하여 모델을 조회합니다.
/// - Parameters:
///   - from collection: 조회할 컬렉션입니다.
///   - at documentID: 조회할 문서의 ID입니다.
/// - Returns: 지정된 타입의 모델을 반환합니다.
func fetch<T: GSModel>(
    from collection: FirestoreCollection,
    at documentID: String
) async throws -> T


let fetchedChat: Chat = try await service.fetch(
    from: .chat, 
    at: chat.id
)



/// 지정된 컬렉션의 모든 문서를 조회합니다.
/// - Parameters:
///   - superCol superCollection: 상위 컬렉션입니다.
///   - superDoc superDocumentID: 상위 문서의 ID입니다.
///   - from collection: 조회할 컬렉션입니다.
/// - Returns: 해당 컬렉션의 모든 모델을 포함하는 배열을 반환합니다.
func fetch<T: GSModel>(
    superCol superCollection: FirestoreCollection,
    superDoc superDocumentID: String,
    from collection: FirestoreCollection
) async throws -> [T]


let fetchedMessages: [Message] = try await service.fetch(
  superCol: .chat, 
  superDoc: chat.id,
  from: .knock
)




/// 지정된 컬렉션과 문서 ID를 사용하여 모델을 조회합니다.
/// - Parameters:
///   - superCol superCollection: 상위 컬렉션입니다.
///   - superDoc superDocumentID: 상위 문서의 ID입니다.
///   - from collection: 조회할 컬렉션입니다.
///   - at documentID: 조회할 문서의 ID입니다.
/// - Returns: 지정된 타입의 모델을 반환합니다.
func fetch<T: GSModel>(
    superCol superCollection: FirestoreCollection,
    superDoc superDocumentID: String,
    from collection: FirestoreCollection,
    at documentID: String
) async throws -> T


let fetchedMessage: Message = try await service.fetch(
  superCol: .chat,
  superDoc: chat.id,
  from: .message,
  at: message.id
)



/// 지정된 컬렉션에서 특정 조건을 만족하는 모든 문서를 조회합니다.
/// - 단일 쿼리일 때 사용합니다. 복합 쿼리를 사용하시려면 getCollectionPath로 컬렉션 경로를 설정하고, .query 체이닝으로 원하는 조건을 설정한 뒤 .fetch를 호출해주세요.
/// - Parameters:
///   - from colRef: 조회할 컬렉션입니다.
///   - where field: 조건을 적용할 필드입니다.
///   - by query: 조건으로 적용할 쿼리 연산자입니다.
/// - Returns: 조건을 만족하는 모든 모델을 포함하는 배열을 반환합니다.
func fetch<T: GSModel>(
    from collection: FirestoreCollection,
    where field: any FirestoreFieldProtocol,
    by query: FirestoreQueryOperation
) async throws -> [T]


let fetchQueryChats: [Chat] = try await service.fetch(
  from: .chat,
  where: FirestoreField.Chat.createdDate,
  by: .orderBy(type: .descending)
)



/// 지정된 컬렉션에서 특정 조건을 만족하는 모든 문서를 조회합니다.
/// - 단일 쿼리일 때 사용합니다. 복합 쿼리를 사용하시려면 getCollectionPath로 컬렉션 경로를 설정하고, .query 체이닝으로 원하는 조건을 설정한 뒤 .fetch를 호출해주세요.
/// - Parameters:
///   - superCol superCollection: 상위 컬렉션입니다.
///   - superDoc superDocumentID: 상위 문서의 ID입니다.
///   - from colRef: 조회할 컬렉션입니다.
///   - where field: 조건을 적용할 필드입니다.
///   - by query: 조건으로 적용할 쿼리 연산자입니다.
/// - Returns: 조건을 만족하는 모든 모델을 포함하는 배열을 반환합니다.
func fetch<T: GSModel>(
    superCol superCollection: FirestoreCollection,
    superDoc superDocumentID: String,
    from collection: FirestoreCollection,
    where field: any FirestoreFieldProtocol,
    by query: FirestoreQueryOperation
) async throws -> [T]


let fetchQueryMessages: [Message] = try await service.fetch(
  superCol: .chat,
  superDoc: chat.id,
  from: .message,
  where: FirestoreField.Message.isRead,
  by: .equalTo(value: true)
)

복합 쿼리 조회

문서 조회 코드에 명시된 fetchQuery는 단일 쿼리만 가능합니다.

복합 쿼리가 필요한 경우에는 getCollectionPath으로 경로를 가져오고, 체이닝 방식으로 필요한 쿼리를 적용한 뒤 fetch로 조회할 수 있습니다.

let fetchMultiQueryChats: [Chat] = try await service
  .getCollectionPath(from: .chat)
  .query(field: FirestoreField.Chat.createdDate, operation: .orderBy(type: .ascending))
  .query(field: FirestoreField.Chat.knockContentDate, operation: .lessThan(value: Date.now))
  .query(field: FirestoreField.Chat.knockContent, operation: .in(values: ["하이", "반갑습니다"]))
  .fetch()



문서 업데이트

  • updating 파라미터 배열을 작성할 때, 첫 번째 아이템의 타입까지만 작성하고 나면 두 번째 아이템부터는 필드 타입이 추론되어서 타입 생략으로도 접근이 가능합니다.
/// 지정된 컬렉션의 모델을 업데이트합니다. 모델의 ID를 사용해서 문서를 조회합니다.
/// - Parameters:
///   - in collection: 업데이트 모델이 위치한 컬렉션입니다.
///   - at model: 업데이트할 모델입니다.
///   - updating fields: 업데이트할 필드 리스트입니다.
func update<T: GSModel, U: FirestoreFieldProtocol>(
    in collection: FirestoreCollection,
    at model: T,
    updating fields: [U]
) throws


try service.update(
  in: .chat,
  at: chat,
  updating: [FirestoreField.Chat.joinedMemberIDs,
             .createdDate,
             .lastContent,
             .unreadMessageCount]
)



/// 지정된 컬렉션의 모델을 업데이트합니다. 모델의 ID를 사용해서 문서를 조회합니다.
/// - Parameters:
///   - superCol superCollection: 상위 컬렉션입니다.
///   - superDoc superDocumentID: 상위 문서의 ID입니다.
///   - in collection: 업데이트 모델이 위치한 컬렉션입니다.
///   - at model: 업데이트할 모델입니다.
///   - updating fields: 업데이트할 필드 리스트입니다.
func update<T: GSModel, U: FirestoreFieldProtocol>(
    superCol superCollection: FirestoreCollection,
    superDoc superDocumentID: String,
    in collection: FirestoreCollection,
    at model: T,
    updating fields: [U]
) throws


try service.update(
  superCol: .chat,
  superDoc: chat.id,
  in: .message,
  at: message,
  updating: [FirestoreField.Message.imageContent,
             .isRead,
             .sentDate]
)



문서 삭제

/// 지정된 컬렉션의 문서를 찾아서 삭제합니다.
/// - Parameters:
///   - in collection: 삭제할 컬렉션입니다.
///   - at documentID: 삭제할 문서의 ID입니다.
func delete(
    in collection: FirestoreCollection,
    at documentID: String
)


service.delete(in: .chat, at: chat.id)
        


/// 지정된 컬렉션의 문서를 찾아서 삭제합니다.
/// - Parameters:
///   - superCol superCollection: 상위 컬렉션입니다.
///   - superDoc superDocumentID: 상위 문서의 ID입니다.
///   - in collection: 삭제할 컬렉션입니다.
///   - at documentID: 삭제할 문서의 ID입니다.
func delete(
    superCol superCollection: FirestoreCollection,
    superDoc superDocumentID: String,
    in collection: FirestoreCollection,
    at documentID: String
)


service.delete(
  superCol: .chat,
  superDoc: chat.id,
  in: .message,
  at: message.id
)

wontaeyoung and others added 30 commits January 17, 2024 17:53
@wontaeyoung wontaeyoung added the ✨ Feature 신규 기능 구현 label Mar 13, 2024
@wontaeyoung wontaeyoung self-assigned this Mar 13, 2024
@wontaeyoung wontaeyoung linked an issue Mar 13, 2024 that may be closed by this pull request
4 tasks
Copy link
Collaborator

@ValseLee ValseLee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

단일한 Service 객체 하나로 너무나 많은 일을 수행하고 있어서.. 다른 객체들이 Service 객체를 유연하게 가져가지 못할 것 같다는 생각이 같이 들었습니다. Service를 일부 분리해두면 어떨까 하는 제안을 드려봅니다.
논외로, Service가 있다는 건 Repository도 있다는 것을 암시하는지..? Clean을 어느 정도 차용할 것인지도 이야기를 해보면 좋을 것 같아요

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 AppInfo가 어떤 정보를 담고 있는 거였죠!?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요거 avatar_url snake_case를 camelCase로 변환하는 디코더 옵션이 있습니다.

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

이러면 코딩키 없이도 자동으로 매핑해준답니다~

struct Event: Codable, Identifiable {
let id: String
let type: String?
let actor: Actor
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actor 가 예약 키워드라서 actor로 수정해야 좋을 것 같습니다!

@@ -0,0 +1,193 @@
import FirebaseFirestore

public protocol FirestoreService {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 Super Service를 분리할 계획이 혹시 있나요?
fetchService, createService 등으로 분리하고 추후에 Abstract Factory Pattern으로 서비스를 초기화해서 필요한 View 마다 의존성으로 꽂아버리는 방식은 어떨까요?

[Feat] GitHub API 코드 마이그레이션을 진행했습니다.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 신규 기능 구현

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] Firebase 추상화 서비스 클라이언트를 구현합니다.

4 participants