[]
        
(Showing Draft Content)

Fragment

프래그먼트(Fragment) 메커니즘은 대용량 문서의 스냅샷 처리를 최적화하기 위해 설계된 고급 서버 측 기능입니다. 스냅샷을 여러 개의 작은 조각(프래그먼트)으로 분할함으로써, 서버는 필요한 부분만 처리하고 전체 스냅샷을 반복적으로 읽고 쓰는 작업을 피할 수 있어 성능이 향상됩니다. 이 문서는 프래그먼트의 설계, 구현, 사용 방법을 소개합니다.

프래그먼트가 필요한 이유

워크북이나 복잡한 데이터 구조와 같은 대용량 문서의 경우, 각 op 적용 시마다 전체 스냅샷을 읽고 쓰면 높은 I/O 오버헤드와 성능 병목이 발생합니다.

프래그먼트를 사용하지 않을 경우 서버 측 처리 흐름은 다음과 같습니다.

  1. 데이터베이스에서 전체 스냅샷을 읽음

  2. 연산을 적용하여 데이터 수정

  3. 수정된 전체 스냅샷을 다시 데이터베이스에 기록

프래그먼트 메커니즘은 스냅샷을 분할하여 저장·조작함으로써 리소스 사용을 크게 줄입니다.

  1. 스냅샷을 독립적인 프래그먼트로 분할(예: 워크시트별 프래그먼트)

  2. 연산 시 관련된 프래그먼트만 로드 및 업데이트

  3. 필요할 때 프래그먼트를 병합하여 전체 스냅샷 반환

설계

워크북을 예로 들면, 스냅샷 구조는 다음과 같다고 가정합니다.

interface IWorkbookSnapshot {
  activeSheetId: string;
  sheets: IWorksheetSnapshot[];
}
interface IWorksheetSnapshot {
  sheetId: string;
  dataTable: { [row: number]: { [col: number]: { value: string } } };
}

지원되는 연산 타입:

  • setCellValue: 셀 값 수정

  • addWorksheet: 워크시트 추가

  • removeWorksheet: 워크시트 삭제

프래그먼트 분할 예시

다음과 같은 워크북 스냅샷이 있을 때:

{
  activeSheetId: 'sheet1',
  sheets: [
    { sheetId: 'sheet1', dataTable: { 0: { 0: { value: 'Hello' } } } },
    { sheetId: 'sheet2', dataTable: { 0: { 0: { value: 'World' } } } }
  ]
}

아래와 같은 프래그먼트로 분할할 수 있습니다.

프래그먼트 이름

프래그먼트 데이터

workbook

{ activeSheetId: 'sheet1' }

sheet_sheet1

{ sheetId: 'sheet1', dataTable: { 0: { 0: { value: 'Hello' } } } }

sheet_sheet2

{ sheetId: 'sheet2', dataTable: { 0: { 0: { value: 'World' } } } }

OT 타입 지원

프래그먼트 메커니즘은 서버 측 OT 타입(OT_Types)을 확장하여 구현되며, 다음 메서드들을 추가로 제공합니다.

메서드

설명

createFragments(data: S): ISnapshotFragments;

초기 데이터로부터 여러 프래그먼트를 생성하여 ISnapshotFragments를 반환합니다.

applyFragments(request: ISnapshotFragmentsRequest, op: T): Promise<void>;

ISnapshotFragmentsRequest를 사용해 프래그먼트 단위로 연산을 적용합니다.

composeFragments(fragments: ISnapshotFragments): S;

프래그먼트들을 병합하여 완전한 스냅샷을 생성합니다.

프래그먼트를 사용하는 경우 위 메서드들은 반드시 구현해야 합니다.

createapply 메서드는 생략할 수 있습니다.

워크플로우

  1. 문서 생성: createFragments 메서드를 호출하여 스냅샷을 프래그먼트로 분할하고 데이터베이스에 저장합니다.

  2. 연산 적용:

    • 연산(op)을 수신한 후 applyFragmentsISnapshotFragmentsRequest를 사용해 관련 프래그먼트만 조작합니다.

    • 영향을 받는 프래그먼트만 업데이트합니다(예: 셀 수정 시 해당 워크시트 프래그먼트만 업데이트).

  3. 클라이언트에서 스냅샷 요청: composeFragments를 호출해 프래그먼트를 병합한 뒤 전체 스냅샷을 반환합니다.

  • 서버 전용 기능: 프래그먼트는 서버 측에서만 구현되며, 클라이언트는 여전히 완전한 스냅샷을 사용합니다.

  • OT 타입 일관성: 클라이언트와 서버는 동일한 uri와 transform 로직을 공유해야 합니다.

프래그먼트 미사용/사용 OT 타입 비교

아래는 워크북 예제를 기준으로 프래그먼트 미사용 OT 타입과 사용 OT 타입의 구현 비교입니다.

  • 프래그먼트를 사용하지 않는 OT 타입 구현:

const workbook_ot_type: OT_Type = {
    uri: 'workbook-ot-type',
    create: (data: IWorkbookSnapshot) => data,
    apply: (data: IWorkbookSnapshot, op: IOp) => {
        if (op.type === 'addWorksheet') {
            data.sheets.push(op.sheetSnapshot);
        } else if (op.type === 'removeWorksheet') {
            data.sheets = data.sheets.filter(sheet => sheet.sheetId !== op.sheetId);
        } else if (op.type === 'setCellValue') {
            const sheetSnapshot = data.sheets.find(sheet => sheet.sheetId === op.sheetId);
            sheetSnapshot.dataTable[op.row][op.col].value = op.value;
        }
        return data;
    },
    transform: (op1, op2, side) => {
        // 충돌 처리
        return op1;
    }
}
  • 프래그먼트를 사용하는 OT 타입 구현:

const workbook_ot_type = {
    uri: 'workbook-ot-type',
    createFragment: (data: IWorkbookSnapshot) => {
        const fragments = {};

        // 워크시트별 프래그먼트 생성
        for (const sheet of data.sheets) {
            const sheetFragmentName = 'sheet_' + sheet.sheetId;
            fragments[sheetFragmentName] = sheet.dataTable;
        }

        // 나머지 워크북 정보를 프래그먼트로 생성
        fragments['workbook'] = { ...data, sheets: undefined };
        return fragments;
    },
    applyFragments: async (request: ISnapshotFragmentsRequest, op) => {
        if (op.type === 'createSheet') {
            await request.createFragment(op.sheetId, op.sheetSnapshot);
        } else if (op.type === 'deleteSheet') {
            await request.deleteFragment(op.sheetId);
        } else if (op.type === 'setCellValue') {
            const sheetSnapshot = await request.getFragment(op.sheetId) as IWorksheetSnapshot;
            sheetSnapshot.dataTable[op.row][op.col].value = op.value;
            await request.updateFragment(op.sheetId, sheetSnapshot);
        }
    },
    composeFragments: (fragments: ISnapshotFragments) => {
        const data = JSON.parse(JSON.stringify(fragments['workbook'])) as IWorkbookSnapshot;

        // 모든 워크시트 프래그먼트 병합
        for (const fragmentName in fragments) {
            if (fragmentName.startsWith('sheet_')) {
                data.sheets.push(fragments[fragmentName] as IWorksheetSnapshot);
            }
        }

        return data;
    },
    transform: (op1, op2, side) => {
        // 충돌 처리
        return op1;
    }
};

항목

프래그먼트 미사용 OT 타입

프래그먼트 사용 OT 타입

처리 방식

op 적용 시 전체 스냅샷을 읽고 씀

op 적용 시 관련 프래그먼트만 읽고 씀

성능

대용량 문서에서 성능 저하, 높은 I/O 오버헤드

대용량 문서에서 높은 성능, I/O 오버헤드 감소

복잡도

구현이 단순, 전체 스냅샷만 처리

구현이 복잡, 분할 및 병합 로직 필요

적용 시나리오

소규모 문서 또는 낮은 동시성 환경

대규모 문서 또는 높은 동시성 환경

API

/**
 * 동시 작업에서 OT(Operational Transformation) 동작을 커스터마이즈하기 위한 인터페이스 정의
 * @template S 스냅샷 데이터 타입
 * @template T 연산(op) 데이터 타입
 */
export interface OT_Type<S = unknown, T = unknown> {
    /**
     * 문서 타입을 식별하는 URI
     */
    uri: string;
    /**
     * 초기 데이터로부터 스냅샷을 생성
     */
    create?(data: S): S;
    /**
     * 스냅샷 데이터를 프래그먼트로 생성
     */
    createFragments?(data: S): ISnapshotFragments;
    /**
     * 프래그먼트로부터 스냅샷을 병합
     */
    composeFragments?(fragments: ISnapshotFragments): S;
    /**
     * 연산 충돌 해결을 위한 변환 로직
     */
    transform(op1: T, op2: T, side: 'left' | 'right'): T;
    /**
     * 전체 스냅샷에 연산 적용
     */
    apply?(snapshot: S, op: T): S;
    /**
     * 프래그먼트에 연산을 비동기로 적용
     */
    applyFragments?(request: ISnapshotFragmentsRequest, op: T): Promise<void>;
}

export type ISnapshotFragments<S = unknown> = { [key: string]: S };

export interface ISnapshotFragmentsRequest<S = unknown> {
    getFragment(id: string): Promise<S | null>;
    createFragment(id: string, data: S): Promise<void>;
    updateFragment(id: string, data: S): Promise<void>;
    deleteFragment(id: string): Promise<void>;
}