기존 설계

저희가 핵심으로 사용하는 Multipeer Connectivity는 기본적으로 비동기로 동작합니다. 따라서 어떤 오브젝트가 어떤 시점에 사용자에게 도착할지 알 수 없습니다.

가장 처음 설계할 때는, 오브젝트 엔티티를 하나의 배열에서 관리하도록 했습니다. 이때 사용자가 특정 오브젝트를 조작하고 있을 때, MPC를 통해 해당 오브젝트에 대한 업데이트가 동시에 도착하면 data race가 발생할 가능성이 있다는 것을 발견했습니다. 따라서 저희는 문제 해결을 위해 actor를 도입하기로 했습니다.

import Foundation

public actor WhiteboardObjectSet: WhiteboardObjectSetInterface {
    private var whiteboardObjects: Set<WhiteboardObject>

    public init() {
        whiteboardObjects = []
    }

    public func contains(object: WhiteboardObject) -> Bool {
        return whiteboardObjects.contains(object)
    }

    public func insert(object: WhiteboardObject) {
        whiteboardObjects.insert(object)
    }

    public func remove(object: WhiteboardObject) {
        whiteboardObjects.remove(object)
    }

    public func removeAll() async {
        whiteboardObjects.removeAll()
    }

    public func update(object: WhiteboardObject) {
        remove(object: object)
        insert(object: object)
    }

    public func fetchObjectByID(id: UUID) -> WhiteboardObject? {
        return whiteboardObjects.first { $0.id == id }.deepCopy()
    }

    public func fetchAll() async -> [WhiteboardObject] {
        return Array(whiteboardObjects.sorted { $0.updatedAt < $1.updatedAt }.map{ $0.deepCopy) })
    }
}

하지만 테스트 코드를 작성하고 리팩토링을 하는 과정에서 object의 수정이 actor내부의 update 메소드가 아닌 외부 오브젝트 객체의 내부 메소드로 변경이 되고 있음을 발견 했습니다. 여기서 잊고 있었던 것인 actor로 구현된 오브젝트Set 내부의 오브젝트들이 class로 구현 되어 있어, actor의 외부에서도 오브젝트들을 수정하고 삭제할 수 있는 문제를 발견 했습니다.

지금 상황에서 어떻게 하면 동시적으로 해당 오브젝트에 접근을 제어할 수 있을까 고민을 해보았는데요, 생각 할 수 있었던 방법에는 다음과 같습니다.

WhiteboardObject 자체를 actor 또는 struct로 리팩토링하자!

현재는 오브젝트들인 TextObject, DrawingObject, PhotoObject, GameObject를 WhiteboardObject 를 상속하여 다형성의 장점을 잘 이용하고 있다고 생각합니다. 특히 재사용성의 상승과 코드의 확장성이 늘어났다고 생각하는데요, 같은 프로퍼티의 재사용, 메소드들의 override를 예시로 들 수 있을 것 같습니다.

상속이 불가능한 actor(또는 struct)로 리팩토링하는 것에는 큰 문제점 두가지를 생각할 수 있었습니다.

  1. 다형성의 장점으로 가져가던 것들을 잃게 된다는 문제점
  2. 구현의 마무리 단계에서 상속으로 설계되어 있던 것들을 모두 처리 해야된다는 문제

위 두가지가 매우 크다고 생각했습니다. 첫번째의 문제점으로는 위에서 설명한 내용들을 잃게 된다는 아쉬움이 있었습니다.

물론 actor로 변경 했을 경우 해당 object의 동시성문제를 어느정도 해결할 수 있다는 큰 장점이 있지만 두번째의 문제점을 본다면 현 상황에 현실적으로 모든 설계를 뜯어 고치는 리팩토링을 하기에는 불가능하다고 판단하였습니다.