Middleware와 Events

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

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

두 가지 확장 시점

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

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

Middleware 실행 모델

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

TypeScript
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를 등록합니다.

TypeScript
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 전연결 정책, 멤버 표시 이름·아바타
joinUnitSession의 Unit Room 참여 전Room 입장과 Unit 가시성
receivePresence참여한 Session에서 Presence 수신 후, 브로드캐스트 전검증과 Presence 필터링
sendPresence각 Room 멤버에게 Presence 전송 전수신자별 Presence 필터링
TypeScript
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 단위 검증
commitChangesetAdapter 리비전 CAS·커밋 전최종 리비전 규칙과 지표
TypeScript
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()으로 프로세스 내부 리스너를 등록하고 해제 가능한 구독을 받습니다.

TypeScript
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확정된 사실일반 용도
unitCreatedUnit과 초기 데이터 생성 완료로컬 목록 캐시·파생 인덱스
changesetCommitted변경 집합 확정 및 리비전 갱신브로드캐스트, 지표, 파생 인덱스
unitsDeletedAdapter가 삭제 배치 확정프로세스 내부 관련 상태 정리
unitsRecoveredAdapter가 복구 배치 확정프로세스 내부 파생 상태 복구

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을 이러한 경계에 매핑하는 사례는 신원 확인과 권한 부여를 참고하세요.

이 가이드는 어떠셨나요?

© 2026 DreamNum Co., Ltd.