Skip to content

Conversation

@hsw1920
Copy link
Collaborator

@hsw1920 hsw1920 commented Nov 25, 2024

🤔 배경

기존 EventEntityStickerEntity만을 payload로 담아 통신하는 구조였습니다.
Host, Guest 간에 FrameEntity 또한 브로드캐스팅 되어야하기 때문에 이를 개선해야 했습니다.

따라서 FrameEntity를 구현하고, 관련 EventHub의 로직을 구현 및 리팩터링이 이뤄졌으며, WebRTC 통신과정에서 Decoding/Encoding 과정을 단순화하고자 EventPayload의 연관값과 분기를 활용했습니다.

비즈니스로직을 ViewUIControl Event와 연동하고자 의존성 주입을 시도하였으나 기존 코드의 많은 개선이 필요하여
화면 관련 UI 작업은 너무 길어져서 추후 PR로 남기겠습니다.

네이밍의 경우 최대한 신경썻지만 이상한 부분은 영훈님과 논의하지 않은 부분이라 임시로 처리되어 있습니다.
ex. sendToViewModel, sendToViewModelFrame

...PR 고봉밥 죄송합니다.. 리뷰노트만 확인해주시고 커밋단위로 훑어보는 정도면 크게 어렵지 않습니다..!

📃 작업 내역

  • FrameEntity 및 FrameType: 프레임 데이터 관리를 위해 추가 구현
  • EventPayload 구현: EventHub 및 통신에서 StickerFrame 데이터를 통합적으로 처리합니다.
  • EventManager 분리: EventHub에서 StickerFrame 이벤트를 각각의 매니저로 분리하여 관리할 수 있도록 리팩터링
  • EventHub 리팩터링: FrameManger가 추가되어 큐 상태와 매니저의 상태를 기반으로 이벤트 처리를 리팩터링
  • 통신 과정의 데이터 구조 개선: 단순 Data가 아닌 EventPayload 타입과 연관값을 활용한 데이터 전송 및 분기 로직을 단순화
  • 관련 UseCase, Repository 추가 및 의존성 주입

✅ 리뷰 노트

폴더링 관련

  • EventHub 내의 클래스 및 파일 분리는 컨플릭트를 고려하여 추후 진행할 예정입니다.

FrameEventManager 구현

  • 프레임변경의 경우 단발성 이벤트이므로 소유권과 관계 없이 누구나 갱신할 수 있도록 하였습니다.
  • EventType으로 .update만을 사용합니다.
  • EventHub 중앙 집중을 위해 반드시 이를 거치도록 구현했습니다.
// FrameEventManager.swift
    private func updateEvent(by frame: FrameEntity) {
        guard currentFrame?.frameType != frame.frameType else { return }
        currentFrame = frame
        broadcastSubject.send(frame)
    }
  • 의도치 않은 이벤트가 들어온다면 이전 상태를 갱신하기 위해 currentFrame을 갖습니다.
    private var currentFrame: FrameEntity?

    ...

    func work(type: EventType, with frame: FrameEntity) {
        isReadySubject.send(false)
        switch type {
        case .update:
            updateEvent(by: frame)
        default:
            debugPrint("DEBUG: 의도치 않은 상황에서 모든 참여자 전체 갱신")
            broadcastSubject.send(frame)
        }
        isReadySubject.send(true)
    }

EventHub의 EventManager 분리

    private func bind() {
        eventQueue.isEmptyPublisher
            .combineLatest(stickerEventManager.isReady, frameEventManager.isReady)
            .filter { $0 && ($1 || $2) } // MARK: (큐에 내보낼 이벤트가 있음) && (스티커 매니저가 준비됨 || 프레임 매니저가 준비됨) -> 보낸다
            .map { ($1, $2) }
            .sink { [weak self] isStickerReady, isFrameReady in
                self?.processEvent(isStickerReady: isStickerReady, isFrameReady: isFrameReady)
            }
            .store(in: &cancellables)
    }
    
    private func processEvent(isStickerReady: Bool, isFrameReady: Bool) {
        // 큐에서 내보낼 이벤트의 페이로드 타입
        guard let payloadType = eventQueue.lastEventPayload() else { return }
        
        // 페이로드 타입과 준비 상태가 일치해야함
        guard isPayloadTypeReady(type: payloadType, isStickerReady: isStickerReady, isFrameReady: isFrameReady) else { return }
        
        // 이벤트 큐에서 꺼내기
        guard let currentEvent = eventQueue.popLast() else { return }

        handleEvent(currentEvent, isStickerReady: isStickerReady, isFrameReady: isFrameReady)
    }

    private func isPayloadTypeReady(type: EventPayload, isStickerReady: Bool, isFrameReady: Bool) -> Bool {
        switch type {
        case .sticker:
            return isStickerReady
        case .frame:
            return isFrameReady
        default:
            return false
        }
    }

    private func handleEvent(_ event: EventEntity, isStickerReady: Bool, isFrameReady: Bool) {
        switch event.payload {
        case .sticker(let stickerEntity) where isStickerReady:
            stickerEventManager.work(type: event.type, with: stickerEntity)
        case .frame(let frameEntity) where isFrameReady:
            frameEventManager.work(type: event.type, with: frameEntity)
        default:
            debugPrint("handleEvent 에러 - 처리할 수 없는 이벤트")
        }
    }
  • 기존 StickerEventManager만 존재했을 때는 현재 EventQueueStickerEventManager가 준비만 된다면 따로 핸들링이 필요하지 않았습니다.
  • 하지만 FrameEventManager가 추가되어 부가적인 핸들링이 필요해졌습니다.
  • processEvent에서 준비된 이벤트큐의 Event의 타입이 Sticker일때는 StickerManager가 준비가 되어야하며 Frame일때도 상동합니다.
  • 이에 대한 여집합의 경우 EventQueuepopLast() 메서드를 호출하면 해당 EventQueue에서 Event가 방출되며 처리되지 않고 리턴되기 때문에 아래와 같은 메서드를 EventQueue에 추가하였고 processEvent에서 처리하고 있습니다.
final class EventQueue {
    ...

    func lastEventPayload() -> EventPayload? {
        return queue.last?.payload
    }
}

WebRTC sendData() 통신 시 전달할 데이터의 디코딩/인코딩

호스트가 sendData하는 경우

// MARK: HOST
        eventHub.stickerListPublisher
            .sink { [weak self] entityList in
                let payload = EventPayload.stickerList(entityList)
                guard let encodedData = try? self?.encoder.encode(payload) else { return }
                self?.clients.forEach { $0.sendData(data: encodedData)}
               ...
            }
            .store(in: &cancellables)
        
        eventHub.framePublisher
            .sink { [weak self] frameEntity in
                let payload = EventPayload.frame(frameEntity)
                guard let encodedData = try? self?.encoder.encode(payload) else { return }
                self?.clients.forEach { $0.sendData(data: encodedData) }
               ...
            }
            .store(in: &cancellables)
  • HostGuest들에게 encodingData를 송신해야하며, 이때 EventPayload에 담아 보냅니다.
// MARK: GUEST
        clients.first?.receivedDataPublisher
            .sink(receiveValue: { [weak self] data in
                guard let payload = try? self?.decoder.decode(EventPayload.self, from: data) else { return }
                
                switch payload {
                case .stickerList(let stickerList):
                    self?.receiveDataFromHost.send(stickerList)
                case .frame(let frameEntity):
                    self?.receiveDataFromHostFrame.send(frameEntity)
                default:
                    break
                }
            })
            .store(in: &cancellables)
  • Guest의 경우 해당 sendData()로 송신한 데이터를 receivedDataPublisher를 통해 수신하며, 이때 EventPayload단 1회 디코딩합니다.
  • EventPayload 및 연관값으로 감싸지않고 분기를 통해 각 [StickerEntity].selfFrameEntity.self로 디코딩을 하는 경우 불필요한 디코딩 및 실패가 일어날 수 있어 이 방식을 채택하였습니다.

🎨 스크린샷

image
  • UI를 제외한 비즈니스로직만을 구현하였습니다.
  • GuestreceiveDataPublisher, 해당 데이터의 EventPayload 디코딩Sticker/Frame 엔티티의 분기를 확인했습니다.

🚀 테스트 방법

asyncAfter를 통해 임의로 Event를 EventHub에 push하면 가능하긴 합니다..

- DomainInterface에 존재하는 FrameEntity에서 FrameType이 필요했음.
- FrameType을 DomainInterface에 두고, FrameImageGenerator에서 의존하도록 구조 변경
- Host, Guest는 EventEntity으로 변경사항을 래핑해서 EventHub에 보내주어야 합니다.
- 변경사항으로는 현재 StickerEntity의 변경, FrameType의 변경이 일어날 수 있습니다.
- 따라서 이를 분기하기 위해 StickerEntity, FrameType을 연관값으로 갖는 enum 타입의 EventType을 두었습니다.
- EntityType라는 네이밍이 너무 추상적이었습니다. 또한 연관값의 데이터를 전달하는 느낌이 나지 않았습니다.
- EventPayload라는 네이밍으로 통신을 통해 전달되는 핵심 데이터의 느낌이 나도록 변경하였습니다.
- EventType과 EventPayload와 같은 enum에 frozen 키워드를 추가하여 열거형이 변경되지 않음을 명시적으로 선언하였습니다.
- 현재 EventHub의 EventManager는 오직 StickerEntity를 담은 Event만을 관리할 수 있습니다.
- 하지만 EventHub는 StickerEntity 이외에도 FrameEntity를 담은 Event 또한 관리해야합니다.
- 현재 EventManager에서는 관련 StickerEntity를 관리하는 딕셔너리가 존재하기 때문에 해당 Manager에서 Frame 까지 함께 관리한다면 SRP에 위배됩니다.
- 따라서 현재의 EventManager를 StickerEntity를 담은 Event만을 담당하는 Manager 역할을 할 수 있도록 리팩터링 하였습니다.
- 다음 커밋에서 네이밍 변경이 있습니다.
- 책임 분리를 위해 기존 `StickerEntity`만을 담당하던 `EventManager`를 `StickerEventManager`로 네이밍 변경
- `StickerEventManger` 기준에서는 모호했던 `resultEventPublihser`라는 모호한 네이밍을 사용하지 않고 `braodcastSubject`로 컨텍스트를 강조하는 네이밍으로 변경
- `EventHub`를 바라보는 레퍼지토리의 경우 필요한 것은 `[StickerEntity]이므로 외부에서는 `stickerListPublisher`로 네이밍 변경
- 큐의 현재 상태를 명시적으로 나타내도록 변경
- 외부에 보여지는 publisher와 내부에서 사용하는 subject 분리를 통해 접근제어 수준 변경
- 기존 callEventPublisher라는 네이밍이 StickerEventManger에게 매우 모호했습니다.
- Manager가 준비가 되어있어서 이벤트를 받을 수 있다는 의미를 알리는 publisher로 `isReady`라는 네이밍을 채택하였습니다.
  - Bool 타입이기에 굳이 어미에 Publisher를 붙이지 않았습니다.
- 그외 Subject와 Publisher를 연산프로퍼티를 통해 접근제어자를 다루었습니다.
- FrameEventManager 인스턴스를 포함하여 이벤트를 할당
- 이벤트큐는 항상 가능해야하며, 두 매니저(스티커, 프레임)중 하나 이상이 준비된 경우만 필터를 통과합니다.
- 만약 이벤트큐에 준비된 최신 이벤트의 payload가 매니저의 담당과 다르다면 무시됩니다.
- 프레임은 소유권 상관 없이 브로드캐스팅 됩니다.
- 기존에는 [StickerEntity]를 그대로 담아 보내고 있었습니다.
- 추가 기능인 FrameEntity 또한 전달되어야하는 상황에서 if-else 분기문으로 decode를 반복하기보다 EventPayload에 담아 보내어 decode를 1회 반드시 성공한 이후에
switch로 분기하는 방식을 선택했습니다.
- EventPayload에 [StickerEntity] 케이스를 추가하였습니다.
- UseCase, Repository 인터페이스 / 구체타입 구현
@hsw1920 hsw1920 added ✨ feat 새로운 기능 추가 ⚙️ refactor 코드 정상화 labels Nov 25, 2024
@hsw1920 hsw1920 self-assigned this Nov 25, 2024
@hsw1920 hsw1920 linked an issue Nov 25, 2024 that may be closed by this pull request
2 tasks
@hsw1920 hsw1920 requested review from 0Hooni, Kiyoung-Kim-57 and youn9k and removed request for 0Hooni November 25, 2024 15:59
Copy link
Member

@Kiyoung-Kim-57 Kiyoung-Kim-57 left a comment

Choose a reason for hiding this comment

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

와 진짜 고민한 흔적이 많은 코드네요! 많이 배워갑니다!!

Copy link
Collaborator

@0Hooni 0Hooni left a comment

Choose a reason for hiding this comment

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

LGTM 💯

수고하셨습니다!


public func mergeSticker(type: EventType, sticker: StickerEntity) {
let sticketEvent = EventEntity(type: type, timeStamp: Date(), entity: sticker)
let sticketEvent = EventEntity(
Copy link
Collaborator

Choose a reason for hiding this comment

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

오타 나신것 같아요!

rename로 sticketEventstickerEvent로 바꿔주시면 좋을거 같습니다 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

style/#101:: 오타 수정 수정 완료했습니다!

sticket -> sticker
@hsw1920 hsw1920 merged commit 821e1a6 into develop Nov 26, 2024
1 check passed
@hsw1920 hsw1920 deleted the feat/#101-send-frame-event branch November 26, 2024 05:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat 새로운 기능 추가 ⚙️ refactor 코드 정상화

Projects

None yet

Development

Successfully merging this pull request may close these issues.

이벤트 허브 페이로드 송수신이 가능하다

5 participants