프로젝트 요구사항 분석과 설계
개인 코드 편집기의 인증·파일 관리·REST 자동 저장을 기본 범위로 정하고, 실시간 협업 확장의 데이터·API 경계를 설계합니다.
지난 12장에서는 NestJS를 활용한 서버리스 아키텍처, WebSocket 실시간 통신, CQRS 패턴, 그리고 도메인 주도 설계(DDD) 같은 고급 주제를 다뤘습니다.
13장에서는 지금까지 배운 내용을 묶어 실제 웹 애플리케이션 프로젝트를 기획하고, NestJS로 구현하는 과정을 단계별로 진행합니다.
이번 절은 그 출발점으로, 프로젝트 요구사항 분석과 전반 설계에 집중합니다.
좋은 설계는 프로젝트의 성공을 좌우하며, 이후 개발 과정의 시행착오를 줄이고 유지보수성을 높이는 기반이 됩니다.
Nest · Planned architecture
협업 편집기 설계 경계
HTTP API와 Socket Gateway를 나눈 설계안입니다. 서버가 JWT·자원 권한과 업무 규칙을 확인하고 TypeORM을 통해 User·Project·File·Folder를 저장하도록 구현합니다. 브라우저의 userId만 신뢰하지 않습니다.
- 브라우저
HTTP API 호출과 Socket.IO 이벤트로 서버와 통신합니다.
- HTTP 인증
입력과 JWT를 검증하고 자원 접근 권한을 확인하도록 설계합니다.
- Socket 인증
실시간 연결과 이벤트에도 인증·권한 확인이 필요합니다.
- 도메인 서비스
프로젝트·파일·사용자 관련 업무 규칙을 조정합니다.
- 저장 계층
TypeORM과 PostgreSQL에 User·Project·File·Folder를 저장합니다.
화살표는 요청·이벤트·저장 호출의 설계 경로입니다. HTTP와 Socket의 인증·권한 구현 여부를 각각 검증해야 합니다.
프로젝트 개요: 자동 저장부터 협업 확장까지
이번 프로젝트에서는 웹 기반의 온라인 코드 에디터를 만듭니다. 먼저 로그인한 사용자가 자신의 프로젝트를 열고, 파일을 편집한 뒤 REST API로 저장하는 흐름을 연결합니다.
기본 확인 목표는 편집한 내용을 서버에서 다시 조회했을 때 같은 내용이 남아 있는 것입니다. 파일은 REST API로 미리 만들고, React 화면에서는 목록에서 선택해 편집합니다.
여러 사용자의 동시 편집은 선택 확장입니다. 뒤의 Socket.IO 클라이언트 코드는 연결 지점을 보여 주는 스케치이며, 협업 서버와 원격 변경 적용까지 완성된 예제는 아닙니다. 일부 DTO와 회원가입 화면도 간략화되어 있으므로, 각 절의 준비 사항을 채운 뒤 기본 흐름을 확인합니다.
핵심 기능- 코드 편집: 다양한 프로그래밍 언어(JavaScript, Python 등)에 대한 구문 강조(Syntax Highlighting)를 지원하는 웹 기반 코드 에디터.
- REST 자동 저장: 마지막 입력 뒤 파일 내용을 저장하고, 다시 조회해 저장 여부를 확인합니다.
- 프로젝트/파일 관리: 개인 프로젝트와 파일을 REST API로 관리합니다. React 예제는 프로젝트 생성·목록·삭제와 평탄한 파일 목록을 다룹니다. 파일 생성 버튼과 폴더 트리는 확장 과제입니다.
- 사용자 인증: 기본적인 사용자 가입 및 로그인 기능.
- 실시간 협업·버전 관리 (선택 확장): 다른 사용자의 변경 적용, 공유 권한, 충돌 처리와 변경 이력 복원을 추가합니다.
요구사항 분석
프로젝트 요구사항 분석과 설계는 사용자 시나리오, 비기능 요구사항, API 계약, 데이터 경계 기준으로 읽습니다.
프로젝트를 시작하기 전에, 어떤 기능을 만들 것인지, 누가 사용할 것인지, 어떤 제약사항이 있는지 등을 명확히 정의하는 것이 중요합니다.
예시
소유자가 /main.ts를 console.log(1);에서 console.log(2);로 바꾼다고 합시다. 먼저 “입력창의 글자가 바뀌었다”와 “서버에 남았다”를 나눕니다. 전자는 에디터 상태로 확인하고, 후자는 같은 fileId를 GET하여 content가 console.log(2);인지 확인합니다. 이어 다른 소유자의 토큰으로 같은 파일을 요청해 거부되는지도 확인해야 합니다. 화면의 저장 표시 하나만으로 저장과 권한 두 요구를 검증할 수는 없습니다.
스스로 정해 보기
두 번째 화면이 이전 내용을 계속 표시하지만 파일 GET에는 새 내용이 있습니다. 기본 REST 목표와 선택 협업 목표 중 어느 쪽을 먼저 점검해야 하나요? 판단에 필요한 관측도 하나씩 적어 보세요.
판단 근거 확인
REST의 저장·재조회 목표는 새 내용이 반환되었다는 범위에서 확인했습니다. 자동으로 상대 화면에 전달하는 일은 선택 협업의 범위입니다. 해당 모드가 켜져 있는지, 같은 파일 방에 참가했는지, 변경 이벤트를 받았는지와 화면에 적용했는지를 따로 확인합니다. 이 사례에는 두 세션이 동시에 편집하거나 저장 순서가 바뀌는 조건을 넣지 않았으므로 충돌 처리까지 성공했다고 판단하지 않습니다.
기능 요구사항
사용자가 시스템을 통해 무엇을 할 수 있어야 하는지에 대한 정의입니다.
-
사용자 관리
- 사용자는 회원가입을 할 수 있다. (이메일/비밀번호 기반)
- 사용자는 로그인/로그아웃을 할 수 있다. (JWT 기반 인증)
- 사용자는 자신의 프로필 정보를 조회할 수 있다. 닉네임 수정은 선택 확장으로 둔다.
-
프로젝트 관리
- 로그인한 사용자는 새로운 프로젝트를 생성할 수 있다.
- 사용자는 자신의 프로젝트 목록을 조회할 수 있다.
- 사용자는 자신의 프로젝트를 열고 닫을 수 있다.
- 사용자는 자신의 프로젝트 이름을 변경하거나 삭제할 수 있다.
- 프로젝트는 이름, 생성자(User ID), 생성일, 마지막 수정일 등의 속성을 가진다.
-
파일/폴더 관리
- 프로젝트 내에서 파일을 생성, 삭제, 이름 변경할 수 있다.
- 프로젝트 내에서 폴더를 생성, 삭제, 이름 변경할 수 있다.
- 파일은 이름, 경로, 내용, 타입(언어), 마지막 수정일 등의 속성을 가진다.
- 폴더는 이름, 경로 등의 속성을 가진다.
-
코드 편집
- 사용자는 파일을 열어 코드를 편집할 수 있다.
- 코드 에디터는 구문 강조, 자동 완성(간단한 수준), 들여쓰기 등을 지원한다. (프론트엔드 에디터 라이브러리 활용)
- 코드 변경 내용은 자동으로 저장된다.
-
실시간 협업 (선택 확장)
- 협업 서버와 공유 권한을 구현한 뒤 여러 사용자가 같은 파일을 편집할 수 있게 한다.
- 한 사용자의 편집 내용은 다른 모든 참여 사용자에게 실시간으로 동기화된다. (WebSocket 활용)
- 누가 어느 위치에서 편집 중인지 커서 위치를 표시한다. (선택 사항/확장)
- 사용자는 다른 사용자에게 프로젝트 공유 초대 링크를 보낼 수 있다. (간단하게 구현하거나 향후 확장)
- 초대받은 사용자의 프로젝트 참여 권한을 서버에서 확인한다. 현재 REST 예제는 프로젝트 소유자만 접근하는 모델이다.
비기능 요구사항
시스템의 품질 속성에 대한 정의입니다. 아래 응답 시간과 동시 사용자 수는 앞으로 측정할 설계 목표입니다. 이 장의 예제나 테스트가 이미 달성한 결과로 읽지 않도록 구분합니다.
-
성능
- 코드 편집 및 실시간 동기화는 100 ms 이내의 지연 시간을 가져야 한다. (WebSocket 응답 시간)
- 동시 접속 사용자 100명까지 안정적인 실시간 협업을 지원해야 한다.
- 파일 및 프로젝트 생성/조회 응답 시간은 500 ms 이내여야 한다.
-
확장성
- 향후 사용자 및 프로젝트 수 증가에 따라 시스템을 쉽게 확장할 수 있어야 한다. (클라우드 환경 고려)
-
보안
- 사용자 인증 및 권한 부여가 적절히 이루어져야 한다. (JWT)
- 민감한 사용자 정보(비밀번호)는 안전하게 저장되어야 한다.
- 모든 통신은 HTTPS/WSS를 통해 암호화되어야 한다.
-
유지보수성
- 클린 아키텍처 또는 DDD 원칙을 적용하여 모듈화되고 유지보수하기 쉬운 코드베이스를 유지한다.
- 적절한 로깅 및 모니터링 시스템을 구축한다.
-
사용 편의성
- 직관적인 사용자 인터페이스를 제공한다. (프론트엔드)
시스템 아키텍처 설계
요구사항 분석을 바탕으로 시스템의 큰 그림을 그립니다.
주요 컴포넌트, 기술 스택, 데이터 흐름 등을 정의합니다.
아키텍처 개요: 마이크로서비스 지향 모놀리식 + 실시간 서비스 분리
초기에는 개발 및 배포의 용이성을 위해 모놀리식(Monolithic) 구조로 시작하되, NestJS의 모듈 시스템을 활용하여 각 기능 도메인을 명확히 분리하는 모듈형 모놀리식(Modular Monolith) 형태로 설계합니다.
기본 흐름은 HTTP API와 데이터베이스로 시작합니다. 실시간 협업을 확장할 때 WebSocket Gateway를 별도 모듈로 추가합니다. 아래 아키텍처 그림은 이 확장까지 포함한 설계안입니다.
계층별 구성-
클라이언트 (Frontend)
- 기술 스택: React, Vue.js, Angular 중 하나 (예: React)
- 코드 에디터 라이브러리: Monaco Editor, CodeMirror 등 (예: Monaco Editor)
- WebSocket 클라이언트: Socket.IO 클라이언트 라이브러리
- 역할: 사용자 인터페이스 제공, HTTP API 호출, WebSocket을 통한 실시간 통신 처리.
-
백엔드 (Backend - NestJS)
- 기술 스택: NestJS (TypeScript)
- 웹 서버: Express.js (NestJS 기본)
- 인증: Passport.js (JWT 전략)
- 데이터베이스 ORM: TypeORM (PostgreSQL, MySQL 등)
- 실시간 통신:
@nestjs/platform-socket.io및Socket.IO -
아키텍처
- API Gateway: 클라이언트의 HTTP 요청을 처리하고, 인증/권한 부여를 담당. (NestJS 컨트롤러)
- Application Services: 비즈니스 로직 조정, 도메인 객체 사용. (NestJS 서비스)
- Domain Models: 프로젝트, 파일, 사용자 등 핵심 도메인 객체 및 비즈니스 규칙. (DDD 원칙 적용)
- Infrastructure: 데이터베이스 접근, 외부 서비스 연동 (Repository 패턴).
- WebSocket Gateway: 실시간 통신 요청 처리, Socket.IO 서버 관리.
-
데이터베이스
- 주 데이터베이스: PostgreSQL (관계형 데이터 모델)
- 사용자, 프로젝트, 파일/폴더 메타데이터 저장.
- 캐시/보조 데이터베이스 (선택 사항/확장): Redis
- WebSocket 세션 관리, 실시간 편집 내용의 임시 저장, 메시지 브로커(Socket.IO Redis Adapter).
- 주 데이터베이스: PostgreSQL (관계형 데이터 모델)
데이터베이스 스키마 설계 (간략)
User 테이블| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
id | UUID | PK, Auto-gen | 사용자 고유 ID |
email | VARCHAR | Unique, Not Null | 이메일 |
password | VARCHAR | Not Null | 해싱된 비밀번호 |
nickname | VARCHAR | Nullable | 닉네임 |
createdAt | TIMESTAMP | Not Null | 생성일 |
updatedAt | TIMESTAMP | Not Null | 마지막 수정일 |
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
id | UUID | PK, Auto-gen | 프로젝트 고유 ID |
name | VARCHAR | Not Null | 프로젝트 이름 |
ownerId | UUID | FK (User.id) | 프로젝트 소유자 |
createdAt | TIMESTAMP | Not Null | 생성일 |
updatedAt | TIMESTAMP | Not Null | 마지막 수정일 |
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
id | UUID | PK, Auto-gen | 파일 고유 ID |
name | VARCHAR | Not Null | 파일 이름 |
path | VARCHAR | Not Null | 프로젝트 내 파일 경로 |
content | TEXT | Nullable | 파일 내용 (최종 저장분) |
type | VARCHAR | Nullable | 파일 타입 (예: 'js') |
projectId | UUID | FK (Project.id) | 소속 프로젝트 ID |
createdAt | TIMESTAMP | Not Null | 생성일 |
updatedAt | TIMESTAMP | Not Null | 마지막 수정일 |
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
id | UUID | PK, Auto-gen | 폴더 고유 ID |
name | VARCHAR | Not Null | 폴더 이름 |
path | VARCHAR | Not Null | 프로젝트 내 폴더 경로 |
projectId | UUID | FK (Project.id) | 소속 프로젝트 ID |
createdAt | TIMESTAMP | Not Null | 생성일 |
updatedAt | TIMESTAMP | Not Null | 마지막 수정일 |
Collaboration (협업) 관련: 실시간 협업에서는 파일 내용이 자주 바뀌므로, 변경 내용을 매번 즉시 DB에 반영하기보다 WebSocket으로 변경 이벤트를 주고받고 주기적으로(또는 사용자가 편집을 멈췄을 때) DB에 최종 저장하는 방식을 고려합니다. (Operation Transformation(OT), Conflict-free Replicated Data Type(CRDT) 같은 복잡한 알고리즘은 이 프로젝트 범위를 넘어설 수 있으므로, 초기에는 단순한 덮어쓰기 방식으로 시작하고 이후 확장성을 고려합니다.)
주요 API 엔드포인트 및 WebSocket 이벤트 설계 (간략)
HTTP API (RESTful)POST /auth/register: 사용자 회원가입POST /auth/login: 사용자 로그인 (JWT 발급)GET /auth/profile: 내 프로필 조회POST /projects: 새 프로젝트 생성GET /projects: 내 프로젝트 목록 조회GET /projects/:projectId: 특정 프로젝트 조회 (파일/폴더 구조 포함)PATCH /projects/:projectId: 프로젝트 이름 변경DELETE /projects/:projectId: 프로젝트 삭제POST /projects/:projectId/files: 프로젝트 내 파일 생성GET /projects/:projectId/files/:fileId: 파일 내용 조회PATCH /projects/:projectId/files/:fileId/content: 파일 내용 저장PATCH /projects/:projectId/files/:fileId/rename: 파일 이름 변경DELETE /projects/:projectId/files/:fileId: 파일 삭제POST /projects/:projectId/folders: 프로젝트 내 폴더 생성PATCH /projects/:projectId/folders/:folderId/rename: 폴더 이름 변경DELETE /projects/:projectId/folders/:folderId: 폴더 삭제
아래 이벤트는 협업을 구현할 때 사용할 후보입니다. 기본 REST 확인에는 필요하지 않습니다. 서버는 토큰에서 사용자 신원을 정하고 파일 접근 권한을 검사해야 하며, 클라이언트가 보낸 userId를 그대로 신뢰하지 않습니다.
-
클라이언트 -> 서버
join_file: 특정 파일 협업 세션 참가 ({ fileId: string, userId: string })leave_file: 특정 파일 협업 세션 이탈 ({ fileId: string, userId: string })code_change: 코드 변경 이벤트 ({ fileId: string, userId: string, changes: any })cursor_change: 커서 위치 변경 이벤트 ({ fileId: string, userId: string, position: { line: number, ch: number } })
-
서버 -> 클라이언트
file_joined: 파일 협업 세션에 사용자 참가 알림 ({ fileId: string, userId: string, userName: string })file_left: 파일 협업 세션에서 사용자 이탈 알림 ({ fileId: string, userId: string, userName: string })code_update: 다른 사용자의 코드 변경 내용 전파 ({ fileId: string, userId: string, changes: any })cursor_update: 다른 사용자의 커서 위치 변경 전파 ({ fileId: string, userId: string, position: { line: number, ch: number } })file_content: 파일 내용 요청 시 파일의 전체 내용 전송 ({ fileId: string, content: string })
개발 환경 및 배포 전략 (간략)
-
백엔드 (NestJS)
- 개발: Node.js, npm/yarn, Docker Compose (PostgreSQL, Redis).
- 배포: Docker 컨테이너화. AWS Fargate (ECS) 또는 Google Cloud Run과 같은 관리형 컨테이너 서비스에 배포를 고려.
- CI/CD: GitHub Actions를 통한 자동 빌드, 테스트, 이미지 푸시, 배포.
-
프론트엔드 (React)
- 개발: Node.js, npm/yarn.
- 배포: AWS S3 + CloudFront (정적 웹 호스팅).
- 데이터베이스: PostgreSQL. (AWS RDS, GCP Cloud SQL과 같은 관리형 서비스 이용).
- 로깅/모니터링: CloudWatch Logs/Metrics 또는 ELK Stack (Docker Compose 환경).
아래 점검표로 기본 요구인 파일 저장을 HTTP 계약·소유권·구현·재조회 확인에 연결해 보세요. 협업 이벤트와 동시 편집 검증은 선택 확장을 구현한 뒤 같은 순서로 추가합니다.
사용자 요구를 실행 가능한 계약으로 추적하는 흐름
저장 요구·사용자 결과·실패 조건·HTTP 계약·소유/소속·구현/재조회 계획의 산출물 변화이며 실행 완료로 표시하지 않는다.
Nest · Requirement to evidence
요구에서 검증 계획까지
기본 목표인 파일 저장을 실패 조건·HTTP 계약·소유권·구현·재조회 확인에 연결합니다. 협업 이벤트와 Socket 검증은 선택 확장입니다. 응답 시간·동시 사용자 수는 측정할 목표입니다.
- 사용자 결과
누가 어떤 작업을 완료할 수 있어야 하는지 씁니다.
- 실패 조건
기본 경로의 권한 없음·잘못된 입력·저장 실패를 먼저 적습니다. 동시 편집 충돌은 협업 확장에 추가합니다.
- 경계 계약
파일 GET·content PATCH의 요청과 응답을 정합니다. join_file·code_change는 협업 확장의 이벤트 후보입니다.
- 데이터 불변식
User·Project·File·Folder의 식별자, 소유자와 소속 관계를 확인합니다.
- 구현과 증거
REST 저장 응답과 같은 파일의 재조회 결과를 확인합니다. Socket 검증은 협업 구현 뒤 추가합니다.
화살표는 기본 요구를 구현·확인 계획에 연결하는 순서입니다. 이 그림은 실행 성공의 증거가 아닙니다. 실제 테스트 결과와 협업·성능 검증은 별도로 남깁니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 파일 저장 요구를 사용자 결과, 실패 조건, HTTP 계약, 소유자와 소속, 구현, 응답·재조회 계획의 여섯 단계로 연결한다. 이 계획은 실행 완료의 증거가 아니다.
- 파일 저장의 사용자 결과, 실패 조건, HTTP 계약, 소유권과 소속, 구현, 응답 및 재조회 확인이 왼쪽에서 오른쪽으로 이어진다.
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 요구와 실패 조건 파일 저장 요구를 사용자 결과와 실패 조건으로 나눕니다. | 자료: 사용자 결과·실패 · 상태: 계획 | 모호한 요구가 검증 가능한 조건이 됩니다. |
| HTTP·소유 계약 HTTP와 소유자·소속을 명시합니다. | 산출물: HTTP·소유 범위 · 실행: 아직 계획 | 구현할 경계의 책임을 정합니다. |
| 구현·재조회 계획 구현과 응답·재조회 확인 계획에 연결합니다. | 산출물: 검증 계획 · 실행 완료: 증명 아님 | 요구를 후속 확인 근거까지 추적합니다. |
사용자 요구를 실행 가능한 계약으로 추적하는 흐름
저장 요구·사용자 결과·실패 조건·HTTP 계약·소유/소속·구현/재조회 계획의 산출물 변화이며 실행 완료로 표시하지 않는다.
Nest · Requirement to evidence
요구에서 검증 계획까지
기본 목표인 파일 저장을 실패 조건·HTTP 계약·소유권·구현·재조회 확인에 연결합니다. 협업 이벤트와 Socket 검증은 선택 확장입니다. 응답 시간·동시 사용자 수는 측정할 목표입니다.
- 사용자 결과
누가 어떤 작업을 완료할 수 있어야 하는지 씁니다.
- 실패 조건
기본 경로의 권한 없음·잘못된 입력·저장 실패를 먼저 적습니다. 동시 편집 충돌은 협업 확장에 추가합니다.
- 경계 계약
파일 GET·content PATCH의 요청과 응답을 정합니다. join_file·code_change는 협업 확장의 이벤트 후보입니다.
- 데이터 불변식
User·Project·File·Folder의 식별자, 소유자와 소속 관계를 확인합니다.
- 구현과 증거
REST 저장 응답과 같은 파일의 재조회 결과를 확인합니다. Socket 검증은 협업 구현 뒤 추가합니다.
화살표는 기본 요구를 구현·확인 계획에 연결하는 순서입니다. 이 그림은 실행 성공의 증거가 아닙니다. 실제 테스트 결과와 협업·성능 검증은 별도로 남깁니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 파일 저장 요구를 사용자 결과, 실패 조건, HTTP 계약, 소유자와 소속, 구현, 응답·재조회 계획의 여섯 단계로 연결한다. 이 계획은 실행 완료의 증거가 아니다.
- 파일 저장의 사용자 결과, 실패 조건, HTTP 계약, 소유권과 소속, 구현, 응답 및 재조회 확인이 왼쪽에서 오른쪽으로 이어진다.
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 요구와 실패 조건 파일 저장 요구를 사용자 결과와 실패 조건으로 나눕니다. | 자료: 사용자 결과·실패 · 상태: 계획 | 모호한 요구가 검증 가능한 조건이 됩니다. |
| HTTP·소유 계약 HTTP와 소유자·소속을 명시합니다. | 산출물: HTTP·소유 범위 · 실행: 아직 계획 | 구현할 경계의 책임을 정합니다. |
| 구현·재조회 계획 구현과 응답·재조회 확인 계획에 연결합니다. | 산출물: 검증 계획 · 실행 완료: 증명 아님 | 요구를 후속 확인 근거까지 추적합니다. |
이번 절에서는 온라인 코드 에디터의 기본 범위와 협업 확장을 나누고, 데이터 모델과 API 경계를 설계했습니다.
다음 절부터는 인증과 프로젝트·파일 REST API를 연결합니다. 먼저 기본 저장 흐름을 확인한 뒤, 협업 Gateway와 원격 변경 적용을 확장할 수 있습니다.
이 단계에서는 요구사항, 데이터 모델, API 경계, 인증 흐름, 배포 제약을 먼저 검증해야 합니다.
요구사항을 경계 사례로 바꾸기
중첩 URL의 projectId와 fileId는 각각 존재하는지만 확인해서는 부족합니다. 사용자 U가 프로젝트 A와 B를 모두 소유하고 있고 파일 F는 B에 속한다고 가정합니다. U가 /projects/A/files/F로 수정 요청을 보내면 사용자 소유권 검사는 두 프로젝트에 대해 통과할 수 있어도 F가 URL에 지정한 A의 파일이라는 조건은 거짓입니다. 승인 조건은 인증된 사용자와 프로젝트 소유자 일치, 파일의 project_id와 URL projectId 일치를 함께 포함해야 합니다. 거절 후 파일 내용이 그대로인지도 확인해야 합니다. 403 또는 404 중 어떤 응답을 쓸지는 API 계약에 정하되 이 문서의 설계만으로 특정 구현 응답이 이미 보장된다고 보지는 않습니다.
저장 버튼이나 debounce도 요청 완료 순서를 보장하지 않습니다. 내용 v1의 저장이 먼저 전송되고 v2의 저장이 나중에 전송되어도 v1 처리가 늦게 끝나면 마지막 저장 값은 v1일 수 있습니다. 수정 버전 7을 두 요청이 읽었다면, 서버가 예상 버전 7과 현재 버전 일치를 쓰기와 함께 검사하여 한 요청만 성공시키는 계약을 설계할 수 있습니다. 두 번째 요청에는 충돌을 알려 최신 내용을 비교하게 합니다. 이 버전 검사는 앞으로 구현할 협업·저장 계약이며, 지금 명세에 적힌 REST 저장만으로 구현된 기능은 아닙니다.
성능 목표도 측정 조건을 붙여야 검수할 수 있습니다. 예를 들어 동일한 조건의 동기화 지연 표본 100개가 80 ms 95개와 900 ms 5개라면, 정렬한 표본에서 ceil(0.95 × 100)번째 값을 고르는 nearest-rank p95는 80 ms이고 최대는 900 ms입니다. 따라서 p95가 100 ms 이하라는 조건은 만족하지만 모든 요청이 100 ms 이하라는 조건은 만족하지 않습니다. 이는 구성한 표본의 계산 예제이며 실제 서비스 측정이 아닙니다. 동시 사용자 수, 입력 시작과 수신 완료 시점, 측정 기간, 실패 표본 처리, 선택한 백분위수 정의를 먼저 정한 뒤 목표와 결과를 비교합니다.
요구사항의 반례를 찾는 문제
문제 1. U가 A와 B를 모두 소유하고 F가 B의 파일입니다. /projects/A/files/F 수정 요청에서 사용자 소유권만 통과하면 저장해도 되나요?
풀이. 저장하면 안 됩니다. file.project_id가 A인지도 검사해야 하며 이 경우에는 B라서 요청을 거절합니다. 거절 응답과 함께 F의 저장 내용이 변하지 않았다는 후조건을 확인합니다. 다른 사용자의 프로젝트를 거절하는 사례와 같은 소유자의 잘못된 부모 URL을 거절하는 사례를 별도로 검수합니다.
문제 2. v2가 먼저 성공한 뒤 늦은 v1 요청이 성공하여 최종 내용이 v1이 되었습니다. 클라이언트에 debounce를 적용한 것만으로 이 문제를 해결했다고 볼 수 있나요?
풀이. 볼 수 없습니다. debounce는 전송 횟수를 줄이는 정책이며 이미 보낸 요청의 완료 순서를 바꾸지 않습니다. 저장을 직렬화하거나 서버에서 예상 버전 조건을 원자적으로 적용하는 등 명시적인 충돌 계약이 필요합니다. 버전 7에서 v2가 먼저 성공해 버전 8이 되었다면, 여전히 7을 기대하는 v1 쓰기는 충돌로 처리하고 v2를 보존해야 합니다.
문제 3. 위 100개 표본으로 “100명 동시 접속에서도 모든 동기화가 100ms 이내였다”라고 보고할 수 있나요?
풀이. 보고할 수 없습니다. 예제는 동시 접속 실험을 수행한 기록이 아니고 최대 표본도 900 ms입니다. 정확한 계산 결과는 이 구성 표본의 nearest-rank p95 80 ms와 최대 900 ms입니다. 실제 100명 실험에서는 부하 조건과 관측 기간을 고정하고 성공·실패 및 지연 분포를 기록해야 합니다. p95 조건을 택했다면 그 조건과 최대 지연을 구별하여 보고합니다.