# Fragment

## Content

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

## 프래그먼트가 필요한 이유

워크북이나 복잡한 데이터 구조와 같은 대용량 문서의 경우, 각 op 적용 시마다 전체 스냅샷을 읽고 쓰면 높은 I/O 오버헤드와 성능 병목이 발생합니다.
프래그먼트를 사용하지 않을 경우 서버 측 처리 흐름은 다음과 같습니다.

1. 데이터베이스에서 전체 스냅샷을 읽음
2. 연산을 적용하여 데이터 수정
3. 수정된 전체 스냅샷을 다시 데이터베이스에 기록

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

1. 스냅샷을 독립적인 프래그먼트로 분할(예: 워크시트별 프래그먼트)
2. 연산 시 관련된 프래그먼트만 로드 및 업데이트
3. 필요할 때 프래그먼트를 병합하여 전체 스냅샷 반환

## 설계

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

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

지원되는 연산 타입:

* setCellValue: 셀 값 수정
* addWorksheet: 워크시트 추가
* removeWorksheet: 워크시트 삭제

### 프래그먼트 분할 예시

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

```typescript
{
  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](/spreadjs/docs/v19/spreadjs-collaboration-server/collaboration-framework/js-collaboration-ot/ottypes))을 확장하여 구현되며, 다음 메서드들을 추가로 제공합니다.

| 메서드 | 설명 |
| --- | --- |
| `createFragments(data: S): ISnapshotFragments;` | 초기 데이터로부터 여러 프래그먼트를 생성하여 `ISnapshotFragments`를 반환합니다. |
| `applyFragments(request: ISnapshotFragmentsRequest, op: T): Promise<void>;` | `ISnapshotFragmentsRequest`를 사용해 프래그먼트 단위로 연산을 적용합니다. |
| `composeFragments(fragments: ISnapshotFragments): S;` | 프래그먼트들을 병합하여 완전한 스냅샷을 생성합니다. |

>type=info
> 프래그먼트를 사용하는 경우 위 메서드들은 반드시 구현해야 합니다.
> [create](/spreadjs/api/v19/collaboration/js-collaboration-ot/interfaces/OT_Type#create) 및 [apply](/spreadjs/api/v19/collaboration/js-collaboration-ot/interfaces/OT_Type#apply) 메서드는 생략할 수 있습니다.

## 워크플로우

1. **문서 생성**: [createFragments](/spreadjs/api/v19/collaboration/js-collaboration-ot/interfaces/OT_Type#createfragments) 메서드를 호출하여 스냅샷을 프래그먼트로 분할하고 데이터베이스에 저장합니다.
2. **연산 적용**:
    * 연산(op)을 수신한 후 [applyFragments](/spreadjs/api/v19/collaboration/js-collaboration-ot/interfaces/OT_Type#applyfragments)와 [ISnapshotFragmentsRequest](/spreadjs/api/v19/collaboration/js-collaboration-ot/interfaces/ISnapshotFragmentsRequest)를 사용해 관련 프래그먼트만 조작합니다.
    * 영향을 받는 프래그먼트만 업데이트합니다(예: 셀 수정 시 해당 워크시트 프래그먼트만 업데이트).
3. **클라이언트에서 스냅샷 요청**: [composeFragments](/spreadjs/api/v19/collaboration/js-collaboration-ot/interfaces/OT_Type#composefragments)를 호출해 프래그먼트를 병합한 뒤 전체 스냅샷을 반환합니다.

>type=warning
> * **서버 전용 기능**: 프래그먼트는 서버 측에서만 구현되며, 클라이언트는 여전히 완전한 스냅샷을 사용합니다.
> * **OT 타입 일관성**: 클라이언트와 서버는 동일한 uri와 transform 로직을 공유해야 합니다.

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

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

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

```typescript
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 타입 구현:

```typescript
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

```typescript
/**
 * 동시 작업에서 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>;
}
```