# Middleware와 Events

> 범용 Middleware와 Events로 Transport, Endpoint, Service, 협업 확장 모듈을 확장합니다.

- Human documentation: [https://docs.univer.ai/ko-KR/server/collaboration/middleware-and-events](https://docs.univer.ai/ko-KR/server/collaboration/middleware-and-events)

- Agent Markdown: [https://docs.univer.ai/ko-KR/server/collaboration/middleware-and-events.md](https://docs.univer.ai/ko-KR/server/collaboration/middleware-and-events.md)

- Requested language: `ko-KR`

- Content language: `ko-KR`

- Documentation version: `1.0.4`

- Source: [collaboration/middleware-and-events.ko-KR.mdx](https://github.com/dream-num/documentation/blob/dev/content/server/collaboration/middleware-and-events.ko-KR.mdx)

---

Middleware와 Events는 Collaboration SDK의 범용 확장 지점입니다. 신원 확인과 권한 부여는 Middleware로 통합합니다. 작업 전 검증·제한에는 Middleware를, 상태 확정 후 변경 기록·후속 작업에는 Events를 사용합니다. 둘 다 로깅, 요청 추적, 지표, 파생 상태 업데이트를 지원할 수 있습니다.

## 두 가지 확장 시점

|               | Middleware                  | Event                      |
| ------------- | --------------------------- | -------------------------- |
| 실행 시점         | 작업 중                        | 상태 변경 확정 후                 |
| 작업 참여 또는 거부   | 가능                          | 불가                         |
| 호출 Context 보강 | 가능                          | Event가 전달하는 Context와 결과 읽기 |
| 리스너 실패        | 현재 작업 실패 가능                 | 이미 결정된 도메인 결과와 격리          |
| 일반 용도         | 신원 확인, 권한 부여, 검증, 로깅, 시간 측정 | 파생 인덱스, 캐시, 지표, 프로세스 내부 알림 |

작업의 계속 진행 여부를 결정하려면 Middleware를, 상태 변경이 이미 일어났음을 관찰하려면 Event를 사용합니다.

## Middleware 실행 모델

같은 Action의 Middleware는 등록 순서대로 비동기 체인을 형성합니다. `await next()`가 다음 Middleware로 진입하며 SDK 작업은 가장 안쪽에서 실행됩니다.

```ts
service.use("submitChangeset", async (context, next) => {
  const startedAt = performance.now();
  context.customData.startedAt = startedAt;

  try {
    await next();
  } finally {
    metrics.observe("collaboration.submit.duration", performance.now() - startedAt);
  }
});
```

Middleware는 다음을 할 수 있습니다.

* Action별 요청 읽기
* `customData`로 호출 내부 데이터 공유
* `next()` 전에 입력 검사 또는 애플리케이션 시스템 조회
* `next()` 반환 후 소요 시간이나 결과 기록
* `CollabError`를 던져 예상된 협업 작업 거부

Transport HTTP Middleware는 `next()` 호출 없이 HTTP 응답을 종료할 수 있습니다. Service와 Endpoint Middleware는 조용히 `next()`를 생략하지 말고 명시적인 오류로 거부해야 합니다.

## Transport Middleware

`transport.use()`는 일반 HTTP Middleware를 등록하며 등록 순서대로 실행합니다. `await next()`를 호출해 다음 처리를 진행하거나 응답을 직접 종료할 수 있습니다. `transport.useUpgrade()`는 WebSocket 핸드셰이크를 처리합니다.

다음 예제는 HTTP 소요 시간을 기록하고, 인증 후 신뢰할 수 있는 `userID`를 설정한 다음 Collaboration Endpoint를 등록합니다.

```ts
transport.use(async (context, next) => {
  const start = performance.now();
  context.response.once("finish", () => {
    recordHttpDuration({
      method: context.incomingMessage.method,
      route: context.route?.path ?? "unmatched",
      status: context.response.statusCode,
      durationMs: performance.now() - start,
    });
  });
  await next();
});

transport.use(async (context, next) => {
  const user = await authenticate(context.incomingMessage);
  if (!user) {
    context.response.statusCode = 401;
    context.response.end("Authentication required");
    return;
  }
  context.userID = user.userId;
  await next();
});

transport.register(collaborationEndpoint);
```

`recordHttpDuration`과 `authenticate`는 애플리케이션에서 제공합니다. Endpoint는 앞선 Middleware가 설정한 `userID`를 사용합니다. 관측 Middleware를 인증보다 먼저 등록하므로 인증 거부도 기록됩니다.

`context.route?.path`는 일치한 전체 경로 템플릿이며 처리가 Endpoint에 도달하면 사용할 수 있습니다. response의 `finish` 이벤트로 응답 완료까지의 시간을 측정합니다. 경로 매칭 전의 거부와 알 수 없는 경로는 `unmatched`로 기록됩니다.

## Endpoint Middleware

Endpoint Middleware는 실시간 Session과 Presence 작업에 참여합니다.

| Action            | 실행 시점                                 | 일반 용도               |
| ----------------- | ------------------------------------- | ------------------- |
| `connect`         | 티켓 소비·Session 생성 후, HELLO 전           | 연결 정책, 멤버 표시 이름·아바타 |
| `joinUnit`        | Session의 Unit Room 참여 전               | Room 입장과 Unit 가시성   |
| `receivePresence` | 참여한 Session에서 Presence 수신 후, 브로드캐스트 전 | 검증과 Presence 필터링    |
| `sendPresence`    | 각 Room 멤버에게 Presence 전송 전             | 수신자별 Presence 필터링   |

```ts
endpoint.use("receivePresence", async (context, next) => {
  if (JSON.stringify(context.payload).length > 8_000) {
    throw new CollabError("INVALID_REQUEST", "Presence payload is too large");
  }
  await next();
});
```

Endpoint의 `session.userID`, `session.customData`는 Session Ticket을 발급한 HTTP 요청에서 옵니다. Endpoint가 현재 WebSocket Session에 부여한 `memberID`는 재연결 시 변경됩니다.

## Service Middleware

Service Middleware는 서버 기준 협업 데이터의 수명 주기에 참여합니다.

| Action            | 경계                       | 일반 용도               |
| ----------------- | ------------------------ | ------------------- |
| `readUnitData`    | 스냅샷·블록·변경 집합 읽기 전        | 읽기 정책, 테넌트 검사, 감사   |
| `createUnit`      | 원자적 Unit 생성 전            | 유형 검사, 생성 정책, 업무 규칙 |
| `deleteUnits`     | 삭제 배치 쓰기 전               | 삭제 정책과 Unit별 검사     |
| `recoverUnits`    | 복구 배치 쓰기 전               | 복구 정책과 감사           |
| `submitChangeset` | 논리 제출이 멱등성·OT에 들어가기 전    | 편집 정책, 전체 크기 제한, 추적 |
| `applyChangeset`  | 변환된 변경 집합을 후보 상태에 적용하기 전 | mutation 단위 검증      |
| `commitChangeset` | Adapter 리비전 CAS·커밋 전     | 최종 리비전 규칙과 지표       |

```ts
service.use("createUnit", async (context, next) => {
  await creationPolicy.assertAllowed({
    userID: context.userID,
    unitID: context.request.snapshot.unitID,
    type: context.request.snapshot.type,
  });
  context.customData.creationPolicyChecked = true;
  await next();
});
```

`submitChangeset`는 논리 제출마다 한 번 실행됩니다. 리비전 경합으로 `applyChangeset`, `commitChangeset`가 새 `attempt`로 다시 실행될 수 있습니다. 이 단계의 Middleware는 재시도에 안전해야 하며 되돌릴 수 없는 외부 메시지 전송이나 일회성 과금을 직접 수행하면 안 됩니다.

## customData 범위

`customData`는 애플리케이션이 쓰기 가능한 호출 내부 객체이며 다음에 적합합니다.

* 테넌트 ID, 추적 ID, 요청 로거
* 현재 호출에서 재사용할 ACL 조회 결과
* 시간 측정 시작점, 감사 레이블
* 한 Middleware 체인이 공유하는 업무 객체

일반 HTTP 요청, Service 호출, WebSocket Session은 수명 주기가 다릅니다. `customData`는 자동 저장·브라우저 전송·로그 기록되지 않습니다. 장기 데이터는 애플리케이션 스토리지에 저장합니다.

## Event 실행 모델

Service는 `on()`으로 프로세스 내부 리스너를 등록하고 해제 가능한 구독을 받습니다.

```ts
const subscription = service.on("changesetCommitted", async (event) => {
  metrics.increment("collaboration.changeset.committed", {
    unitType: String(event.changeset.type),
  });
  localCache.delete(event.changeset.unitID);
});

subscription.dispose();
```

같은 Event의 리스너는 등록 순서대로 실행되며 완료를 기다립니다. 리스너 오류는 기록되고 이미 결정된 도메인 결과에서 격리됩니다. 예를 들어 `changesetCommitted` 리스너 실패는 Adapter가 저장한 변경 집합을 롤백하지 않습니다.

따라서 Events는 프로세스 내부, 복구 가능 또는 비핵심 후속 작업에 적합합니다. DB 커밋과 공유하는 트랜잭션 경계가 아니며 외부 시스템 전달을 보장하지 않습니다.

## 핵심 Service Events

| Event                | 확정된 사실             | 일반 용도              |
| -------------------- | ------------------ | ------------------ |
| `unitCreated`        | Unit과 초기 데이터 생성 완료 | 로컬 목록 캐시·파생 인덱스    |
| `changesetCommitted` | 변경 집합 확정 및 리비전 갱신  | 브로드캐스트, 지표, 파생 인덱스 |
| `unitsDeleted`       | Adapter가 삭제 배치 확정  | 프로세스 내부 관련 상태 정리   |
| `unitsRecovered`     | Adapter가 복구 배치 확정  | 프로세스 내부 파생 상태 복구   |

Endpoint는 같은 Service의 `changesetCommitted`를 듣고 현재 프로세스에서 Unit에 참여한 클라이언트로 확정 변경 집합을 브로드캐스트합니다. 명시적 나가기, 연결 해제, Endpoint 해제에는 `memberLeftUnit`도 제공합니다.

## 확장 모듈의 독립 확장 지점

History, Thread Comment, Worktree는 핵심 Collaboration Service의 Middleware나 Events를 상속하지 않습니다.

* History는 `getHistoryList`, `listHistoryCreators`, `getHistoryChangesets` 읽기 Middleware를 제공합니다.
* Comment는 추가, 답글, 편집, 삭제, 해결, 목록 Middleware와 `commentCommitted`를 제공합니다.
* Worktree는 수명 주기와 초안 read/submit/commit Middleware, 생성·상태·병합·초안 커밋 Events를 제공합니다.

활성화한 모듈마다 규칙을 설치하세요. 핵심 Service의 로깅·테넌트·권한 Middleware가 자동 적용되지는 않습니다.

기존 사용자와 ACL을 이러한 경계에 매핑하는 사례는 [신원 확인과 권한 부여](https://docs.univer.ai/ko-KR/server/collaboration/identity-and-authorization.md)를 참고하세요.
