[CRDT 구현하기] 2차 시도: 연결 리스트 (한계)

HBSPS·2023년 12월 15일
0

AlgoITNi

목록 보기
12/13

위의 과정들을 통해 충돌을 해결할 수 있었다.
하지만 병합 과정에서 커서가 이동하는 문제가 발생했다.

커서 고정

글자는 잘 병합이 되는데 커서가 이동하는 문제를 해결하기 위해서 커서의 위치 정보를 기억해야 한다고 생각했다.
기존 커서의 위치를 갖고 있으며 병합 이후 해당 정보를 바탕으로 커서의 위치를 조정하는 방식이다.

여러 가지 방법이 있지만 가장 먼저 시도한 방법은 가장 마지막에 삽입된 요소 바로 뒤에 커서를 위치 시키는 것이다.
이를 위해 가장 최근에 삽입된 글자를 가지고 있어야 했으며 기존 CRDT 클래스에 Node 타입의 cursorPosition을 추가했다.

export default class CRDT {
    id: string;
    head: Node | null;
    length: number;
    #cursorPosition: Node | null;

    constructor(id: string) {
        ...
        this.#cursorPosition = this.head;
    }

    insert(letter: string, index: number, wordLength = 1) {
        ...
        this.#cursorPosition = newNode;
    }
    ...
}

이제 CRDT 객체는 마지막에 입력된 글자의 정보를 갖고 있다.
하지만, cursorPosition은 Node 타입이므로 Node를 기반으로 index를 구할 수 있어야 했다.

getIndexByNode(node: Node) {
    let current = this.head;
    let count = 0;

    while (current !== null) {
        if (
            current.letter === node.letter &&
            current.identifier.id === node.identifier.id &&
            current.identifier.time === node.identifier.time
        ) return count;

        count++;
        current = current.nextNode;
    }

    return null;
}

get cursorPosition(): number | null {
    if (!this.#cursorPosition) return null;

    return this.getIndexByNode(this.#cursorPosition); //해당 요소의 index를 찾아온다.
}

위와 같이 구성하여 외부에서는 getter를 이용해 변환된 index를 가져갈 수 있도록 구성했다.

여기서 발생할 수 있는 또 다른 문제점이 있었다.
마지막 글자의 정보를 기억하고 있는데 원격 피어가 해당 글자를 삭제하는 경우를 해결해야 했다.
마지막 글자의 정보를 토대로 커서의 위치를 찾지만 마지막 글자 정보와 일치하는 글자가 없을 것이다.

set cursorPosition(index: number) {
    const prevNode = this.searchNodeByIndex(index);

    this.#cursorPosition = prevNode;
}

setter를 하나 추가하여 커서의 위치를 설정할 수 있도록 구성했다.
이는 마지막 글자 정보를 갖는 것의 단점을 보완하기 위한 것으로, 만약 마지막 글자가 제거된다면 앞 글자로 설정하기 위함이다.
예를 들어 test | 와 같이 t 뒤에 커서가 있는 경우에 원격 피어가 마지막 t를 지우게 되면 tes | 와 같이 s의 뒤로 이동하는 것이다.

위와 같이 커서의 위치를 조정하고 조정 결과를 병합시에 사용하여 최종적으로 커서 위치를 조정해주었다.

const handleRecieveCodeMessage = (event: MessageEvent<string>) => {
    const remoteCRDT = JSON.parse(event.data);

    const prevCursorPosition = crdt.current.cursorPosition as number; //마지막에 입력한 요소
    const prevCursorCandidateNode = crdt.current.searchNodeByIndex(prevCursorPosition - 1); //삭제되는 경우 고려
    crdt.current.merge(remoteCRDT);

	//로컬에서 마지막으로 입력한 요소가 삭제되지 않음
    if (crdt.current.cursorPosition) setCursorPosition(crdt.current.cursorPosition);
    
	//로컬에서 마지막으로 입력한 요소가 삭제된 경우
    else {
        const cursorPosition = crdt.current.getIndexByNode(prevCursorCandidateNode);
        crdt.current.cursorPosition = cursorPosition;
        setCursorPosition(cursorPosition); //text입력하는 cursor 조정
    }

    setPlainCode(crdt.current.toString());
};

(WebRTC의 DataChannel을 통해 원격 피어의 연결 리스트가 수신되는 경우)
위와 같이 구성하여 커서의 위치가 조정되는 CRDT 알고리즘을 구현할 수 있었다.

결과

앞의 과정을 통해 제대로 병합하고 커서의 위치도 조정되는 공동 편집 기술을 만들어볼 수 있었다.

CRDT를 직접 구현하는 과정에서 이 기술이 왜 필요하며, 알고리즘이 어떻게 구현되어야 효율적인지, 그에 따라 어떤 자료구조를 선택해야 할 지 생각해볼 수 있었다.

한계

직접 만든 CRDT의 효율이 좋지 못한 탓인지 실시간성을 완벽히 만족할 수 없었다.
중간에 글자가 갑자기 깨지거나 깨졌다가 다시 100ms 내외의 짧은 시간안에 되돌아오는 경우가 있었다.
이를 해결하기 위해 많은 시간을 들여 원인을 분석했으나 현재로서는 정확하게 어떤 문제인지 진단할 수 없었다.

profile
대체로 맑음

0개의 댓글