[]
프래그먼트(Fragment) 메커니즘은 대용량 문서의 스냅샷 처리를 최적화하기 위해 설계된 고급 서버 측 기능입니다. 스냅샷을 여러 개의 작은 조각(프래그먼트)으로 분할함으로써, 서버는 필요한 부분만 처리하고 전체 스냅샷을 반복적으로 읽고 쓰는 작업을 피할 수 있어 성능이 향상됩니다. 이 문서는 프래그먼트의 설계, 구현, 사용 방법을 소개합니다.
워크북이나 복잡한 데이터 구조와 같은 대용량 문서의 경우, 각 op 적용 시마다 전체 스냅샷을 읽고 쓰면 높은 I/O 오버헤드와 성능 병목이 발생합니다.
프래그먼트를 사용하지 않을 경우 서버 측 처리 흐름은 다음과 같습니다.
데이터베이스에서 전체 스냅샷을 읽음
연산을 적용하여 데이터 수정
수정된 전체 스냅샷을 다시 데이터베이스에 기록
프래그먼트 메커니즘은 스냅샷을 분할하여 저장·조작함으로써 리소스 사용을 크게 줄입니다.
스냅샷을 독립적인 프래그먼트로 분할(예: 워크시트별 프래그먼트)
연산 시 관련된 프래그먼트만 로드 및 업데이트
필요할 때 프래그먼트를 병합하여 전체 스냅샷 반환
워크북을 예로 들면, 스냅샷 구조는 다음과 같다고 가정합니다.
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_Types)을 확장하여 구현되며, 다음 메서드들을 추가로 제공합니다.
메서드 | 설명 |
|---|---|
| 초기 데이터로부터 여러 프래그먼트를 생성하여 |
|
|
| 프래그먼트들을 병합하여 완전한 스냅샷을 생성합니다. |
문서 생성: createFragments 메서드를 호출하여 스냅샷을 프래그먼트로 분할하고 데이터베이스에 저장합니다.
연산 적용:
연산(op)을 수신한 후 applyFragments와 ISnapshotFragmentsRequest를 사용해 관련 프래그먼트만 조작합니다.
영향을 받는 프래그먼트만 업데이트합니다(예: 셀 수정 시 해당 워크시트 프래그먼트만 업데이트).
클라이언트에서 스냅샷 요청: composeFragments를 호출해 프래그먼트를 병합한 뒤 전체 스냅샷을 반환합니다.
서버 전용 기능: 프래그먼트는 서버 측에서만 구현되며, 클라이언트는 여전히 완전한 스냅샷을 사용합니다.
OT 타입 일관성: 클라이언트와 서버는 동일한 uri와 transform 로직을 공유해야 합니다.
아래는 워크북 예제를 기준으로 프래그먼트 미사용 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 오버헤드 감소 |
복잡도 | 구현이 단순, 전체 스냅샷만 처리 | 구현이 복잡, 분할 및 병합 로직 필요 |
적용 시나리오 | 소규모 문서 또는 낮은 동시성 환경 | 대규모 문서 또는 높은 동시성 환경 |
/**
* 동시 작업에서 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>;
}