프로젝트 요구사항 분석과 설계
개인 코드 편집기의 인증·파일 관리·REST 자동 저장을 기본 범위로 정하고, 실시간 협업 확장의 데이터·API 경계를 설계합니다.
12장에서는 서버리스, 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의 인증·권한 구현 여부를 각각 검증해야 합니다.
위 그림은 이 프로젝트의 설계안입니다. 브라우저는 HTTP API와 Socket Gateway를 각각 호출하고, 두 경로 모두 도메인 서비스와 TypeORM 저장 계층을 거쳐 PostgreSQL의 User, Project, File, Folder 데이터로 이어집니다. 서버는 브라우저가 보낸 userId를 믿지 않고 JWT와 자원 권한, 업무 규칙을 직접 확인하며, 두 경로의 인증은 각각 따로 구현하고 확인해야 한다는 점이 그림 위에 적혀 있습니다.
프로젝트 개요: 자동 저장부터 협업 확장까지
이번 프로젝트에서는 웹 기반의 온라인 코드 에디터를 만듭니다. 먼저 로그인한 사용자가 자신의 프로젝트를 열고, 파일을 편집한 뒤 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);인지로 확인합니다. 이어서 다른 사용자의 토큰으로 같은 파일을 요청해 거부되는지도 확인해야 합니다. 화면의 저장 표시 하나만으로 저장과 권한이라는 두 요구를 모두 확인할 수는 없습니다.
기능 요구사항
사용자가 시스템으로 무엇을 할 수 있어야 하는지 정의합니다.
- 사용자 관리
- 사용자는 이메일과 비밀번호로 회원 가입을 할 수 있다.
- 사용자는 JWT 기반으로 로그인하고 로그아웃할 수 있다.
- 사용자는 자신의 프로필 정보를 조회할 수 있다. 닉네임 수정은 선택 확장으로 둔다.
- 프로젝트 관리
- 로그인한 사용자는 새 프로젝트를 만들 수 있다.
- 사용자는 자신의 프로젝트 목록을 조회하고, 프로젝트를 열고 닫을 수 있다.
- 사용자는 자신의 프로젝트 이름을 바꾸거나 삭제할 수 있다.
- 프로젝트는 이름, 생성자(User ID), 생성일, 마지막 수정일을 가진다.
- 파일과 폴더 관리
- 프로젝트 안에서 파일과 폴더를 만들고, 지우고, 이름을 바꿀 수 있다.
- 파일은 이름, 경로, 내용, 타입(언어), 마지막 수정일을 가지고, 폴더는 이름과 경로를 가진다.
- 코드 편집
- 사용자는 파일을 열어 코드를 편집할 수 있다.
- 코드 에디터는 프런트엔드 라이브러리로 구문 강조, 간단한 자동 완성, 들여쓰기를 지원한다.
- 바꾼 내용은 자동으로 저장된다.
- 실시간 협업(선택 확장)
- 협업 서버와 공유 권한을 구현한 뒤, 여러 사용자가 같은 파일을 편집할 수 있게 한다.
- 한 사용자의 편집 내용은 참여한 다른 사용자에게 WebSocket으로 실시간 동기화된다.
- 누가 어느 위치를 편집하는지 커서를 표시한다(선택).
- 다른 사용자에게 프로젝트 공유 초대를 보낼 수 있다(간단히 구현하거나 향후 확장).
- 초대받은 사용자의 참여 권한은 서버에서 확인한다. 지금의 REST 예제는 프로젝트 소유자만 접근하는 모델이다.
비기능 요구사항
시스템의 품질 속성을 정의합니다. 아래 응답 시간과 동시 사용자 수는 앞으로 측정할 설계 목표이며, 이 장의 예제나 테스트가 이미 달성한 결과가 아닙니다.
- 성능
- 코드 편집과 실시간 동기화의 지연은 100ms 이내를 목표로 한다.
- 동시 접속 사용자 100명까지 안정적인 실시간 협업을 지원한다.
- 파일과 프로젝트의 생성·조회 응답 시간은 500ms 이내를 목표로 한다.
- 확장성: 사용자와 프로젝트가 늘어도 클라우드 환경에서 쉽게 확장할 수 있어야 한다.
- 보안
- 사용자 인증과 인가가 JWT로 적절히 이루어져야 한다.
- 비밀번호 같은 민감한 정보는 안전하게 저장한다.
- 모든 통신은 HTTPS와 WSS로 암호화한다.
- 유지보수성
- 클린 아키텍처나 DDD 원칙으로 모듈을 나누어 유지보수하기 쉬운 코드를 유지한다.
- 적절한 로깅과 모니터링을 구성한다.
- 사용 편의성: 직관적인 사용자 화면을 제공한다.
시스템 아키텍처 설계
요구사항을 바탕으로 주요 구성 요소, 기술 스택, 데이터 흐름 같은 큰 그림을 그립니다. 이 절에서 정하는 것은 세 가지입니다. 처음에 애플리케이션을 몇 개의 배포 단위로 나눌지, 각 계층에 어떤 기술을 쓸지, 그리고 요청과 편집 내용이 어느 구성 요소를 거쳐 흐르는지입니다. 이 결정은 다음 글부터 만드는 백엔드 모듈과 프런트엔드 구조의 기준이 되므로, 앞의 기능 요구사항과 비기능 요구사항에서 하나씩 이유를 끌어냅니다.
모듈형 모놀리스로 시작하기
처음에는 개발과 배포를 쉽게 하기 위해 하나의 애플리케이션(모놀리스)으로 시작하되, NestJS의 모듈로 기능 영역을 분명히 나누는 모듈형 모놀리스(modular monolith)로 설계합니다. 기본 흐름은 HTTP API와 데이터베이스로 시작하고, 실시간 협업을 확장할 때 WebSocket Gateway를 별도 모듈로 더합니다. 위의 아키텍처 그림은 이 확장까지 포함한 설계안입니다.
계층별 구성은 다음과 같습니다.
- 클라이언트(프런트엔드)
- 기술 스택: React
- 코드 에디터: Monaco Editor
- WebSocket 클라이언트: Socket.IO 클라이언트
- 역할: 사용자 화면 제공, HTTP API 호출, WebSocket 실시간 통신
- 백엔드(NestJS 12)
- 웹 서버: Express(NestJS 기본)
- 인증: Passport의 JWT 전략
- ORM: TypeORM 1.x와 PostgreSQL
- 실시간 통신:
@nestjs/platform-socket.io와 Socket.IO - 내부 구성: HTTP 요청을 받고 인증과 인가를 처리하는 controller, 업무 흐름을 조율하는 애플리케이션 service, 프로젝트·파일·사용자의 규칙을 담은 도메인 모델, 데이터베이스와 외부 연동을 맡는 인프라 계층(Repository), 실시간 통신을 처리하는 WebSocket Gateway
- 데이터베이스
- 주 데이터베이스: PostgreSQL에 사용자, 프로젝트, 파일·폴더 메타데이터를 저장합니다.
- 캐시·보조 저장소(선택 확장): Redis에 WebSocket 세션, 실시간 편집 내용의 임시 저장, Socket.IO Redis adapter의 메시지를 둡니다.
데이터베이스 스키마 설계
User 테이블
| 컬럼명 | 타입 | 제약 조건 | 설명 |
|---|---|---|---|
id | UUID | PK, 자동 생성 | 사용자 고유 ID |
email | VARCHAR | Unique, Not Null | 이메일 |
password | VARCHAR | Not Null | 해시한 비밀번호 |
nickname | VARCHAR | Nullable | 닉네임 |
createdAt | TIMESTAMP | Not Null | 생성일 |
updatedAt | TIMESTAMP | Not Null | 마지막 수정일 |
Project 테이블
| 컬럼명 | 타입 | 제약 조건 | 설명 |
|---|---|---|---|
id | UUID | PK, 자동 생성 | 프로젝트 고유 ID |
name | VARCHAR | Not Null | 프로젝트 이름 |
ownerId | UUID | FK (User.id) | 프로젝트 소유자 |
createdAt | TIMESTAMP | Not Null | 생성일 |
updatedAt | TIMESTAMP | Not Null | 마지막 수정일 |
File 테이블
| 컬럼명 | 타입 | 제약 조건 | 설명 |
|---|---|---|---|
id | UUID | PK, 자동 생성 | 파일 고유 ID |
name | VARCHAR | Not Null | 파일 이름 |
path | VARCHAR | Not Null | 프로젝트 안의 파일 경로 |
content | TEXT | Nullable | 파일 내용(최종 저장본) |
type | VARCHAR | Nullable | 파일 타입(예: javascript) |
projectId | UUID | FK (Project.id) | 속한 프로젝트 ID |
createdAt | TIMESTAMP | Not Null | 생성일 |
updatedAt | TIMESTAMP | Not Null | 마지막 수정일 |
Folder 테이블
| 컬럼명 | 타입 | 제약 조건 | 설명 |
|---|---|---|---|
id | UUID | PK, 자동 생성 | 폴더 고유 ID |
name | VARCHAR | Not Null | 폴더 이름 |
path | VARCHAR | Not Null | 프로젝트 안의 폴더 경로 |
projectId | UUID | FK (Project.id) | 속한 프로젝트 ID |
createdAt | TIMESTAMP | Not Null | 생성일 |
updatedAt | TIMESTAMP | Not Null | 마지막 수정일 |
실시간 협업에서는 파일 내용이 자주 바뀌므로, 바뀔 때마다 바로 DB에 반영하기보다 WebSocket으로 변경 이벤트를 주고받고, 주기적으로 또는 사용자가 편집을 멈췄을 때 DB에 최종 저장하는 방식을 고려합니다. OT(Operational Transformation)나 CRDT(Conflict-free Replicated Data Type) 같은 알고리즘은 이 프로젝트의 범위를 넘을 수 있으므로, 처음에는 단순한 덮어쓰기 방식으로 시작하고 이후에 확장합니다.
주요 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: 폴더 삭제
아래 WebSocket 이벤트는 협업을 구현할 때 쓸 후보이며, 기본 REST 흐름에는 필요하지 않습니다. 서버는 토큰에서 사용자 신원을 정하고 파일 접근 권한을 검사해야 하며, 12장에서 본 것처럼 클라이언트가 보낸 userId를 그대로 믿지 않습니다.
- 클라이언트에서 서버로
join_file: 특정 파일의 협업 세션 참가({ projectId: string, fileId: string })leave_file: 현재 참가한 파일의 협업 세션 이탈(값 없음)code_change: 코드 변경({ projectId: string, fileId: string, changes: any })cursor_change: 커서 위치 변경({ projectId: string, fileId: 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 24와 npm으로 개발하고, Docker Compose로 PostgreSQL과 Redis를 띄웁니다. 11장처럼 Docker 이미지로 만들어 AWS Fargate(ECS)나 Google Cloud Run 같은 관리형 컨테이너 서비스에 배포하고, GitHub Actions로 빌드·테스트·이미지 게시·배포를 자동화합니다.
- 프런트엔드(React): Node.js와 npm으로 개발하고, AWS S3와 CloudFront 같은 정적 웹 호스팅에 배포합니다.
- 데이터베이스: PostgreSQL을 AWS RDS나 GCP Cloud SQL 같은 관리형 서비스로 운영합니다.
- 로깅과 모니터링: CloudWatch Logs와 지표, 또는 Docker Compose 환경의 로그 수집 스택을 씁니다.
요구사항을 확인할 수 있는 형태로 바꾸기
사용자 요구를 실행 가능한 계약으로 추적하는 흐름
저장 요구·사용자 결과·실패 조건·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·소유 범위 · 실행: 아직 계획 | 구현할 경계의 책임을 정합니다. |
| 구현·재조회 계획 구현과 응답·재조회 확인 계획에 연결합니다. | 산출물: 검증 계획 · 실행 완료: 증명 아님 | 요구를 후속 확인 근거까지 추적합니다. |
위 그림은 “파일을 저장할 수 있다”는 요구를 여섯 단계로 이어 확인 가능한 형태로 바꾸는 흐름입니다. 사용자가 얻을 결과에서 시작해, 권한 오류나 저장 오류 같은 실패 조건, GET과 PATCH의 HTTP 형식, 소유자와 소속 같은 데이터 규칙, module과 service의 구현, 응답과 재조회로 결과를 확인하는 계획까지 차례로 정합니다. 이 그림은 확인 계획일 뿐 실행을 마쳤다는 표시가 아니며, 협업 이벤트와 동시 편집의 확인은 선택 확장을 구현한 뒤 같은 순서로 더합니다.
요구사항은 극단적인 사례(edge case)로 바꾸어 보면 빠진 조건이 드러납니다. 중첩 URL의 projectId와 fileId는 각각 존재하는지만 확인해서는 부족합니다. 파일이 실제로 URL에 적힌 프로젝트에 속하는지도 함께 확인해야 하며, 403과 404 가운데 어떤 응답을 쓸지는 API 규칙으로 미리 정합니다.
저장 버튼이나, 마지막 입력 뒤 잠시 기다렸다가 한 번만 저장하는 디바운스(debounce)도 요청이 끝나는 순서를 보장하지 않습니다. 내용 v1을 먼저 보내고 v2를 나중에 보내도, v1의 처리가 늦게 끝나면 마지막에 남는 값은 v1일 수 있습니다. 두 요청이 같은 수정 버전 7을 읽었다면, 서버가 “예상 버전 7이 지금 버전과 같을 때만 쓰기”를 원자적으로 검사해 한 요청만 성공시키고, 다른 요청에는 충돌을 알려 최신 내용을 비교하게 할 수 있습니다. 이 버전 검사는 앞으로 구현할 저장 규칙이며, 지금 명세의 REST 저장만으로 이미 구현된 기능은 아닙니다.
성능 목표도 측정 조건을 붙여야 확인할 수 있습니다. 예를 들어 같은 조건에서 모은 동기화 지연 표본 100개가 80ms 95개와 900ms 5개라면, 정렬한 표본에서 ceil(0.95 × 100)번째 값을 고르는 nearest-rank 방식의 p95는 80ms이고 최댓값은 900ms입니다. 따라서 “p95가 100ms 이하”라는 조건은 채우지만 “모든 요청이 100ms 이하”라는 조건은 채우지 못합니다. 이는 설명을 위해 만든 표본의 계산이며 실제 측정이 아닙니다. 동시 사용자 수, 입력 시작과 수신 완료의 시점, 측정 기간, 실패한 표본의 처리, 백분위수의 정의를 먼저 정한 뒤 목표와 결과를 비교합니다.
정리
- 이 프로젝트의 기본 범위는 인증, 프로젝트·파일 관리, REST 자동 저장이며, 실시간 협업은 선택 확장입니다.
- 요구사항은 기능 요구와 성능·보안 같은 품질 요구로 나누고, 성능 수치는 측정할 목표로만 적습니다.
- 처음에는 NestJS 모듈로 기능을 나눈 모듈형 모놀리스로 시작하고, 협업이 필요해지면 WebSocket Gateway를 별도 모듈로 더합니다.
- 서버는 HTTP와 WebSocket 모두에서 토큰으로 신원을 정하고, 클라이언트가 보낸
userId를 믿지 않습니다. - 요구사항은 실패 조건, HTTP 형식, 데이터 규칙, 재조회 확인까지 이어 확인할 수 있는 형태로 바꿉니다.
연습 문제
1. 화면과 서버 중 어디를 먼저 볼까
두 번째 사용자의 화면은 이전 내용을 계속 보여 주지만, 파일 GET 요청에는 새 내용이 들어 있습니다. 기본 REST 목표와 선택 협업 목표 가운데 어느 쪽을 먼저 점검해야 할까요? 판단에 필요한 확인 항목도 하나씩 적어 보세요.
해설 보기
REST의 저장과 재조회 목표는 새 내용이 돌아왔으므로 이미 확인되었습니다. 다른 사용자의 화면에 자동으로 전달하는 일은 선택 협업의 범위입니다.
협업 모드가 켜져 있는지, 같은 파일의 방에 참가했는지, 변경 이벤트를 받았는지, 받은 내용을 화면에 적용했는지를 따로 확인합니다. 이 상황에는 두 세션이 동시에 편집하거나 저장 순서가 바뀌는 조건이 없으므로, 충돌 처리까지 성공했다고 판단하지는 않습니다.
2. 같은 소유자의 다른 프로젝트
사용자 U가 프로젝트 A와 B를 모두 소유하고, 파일 F는 B에 속합니다. U가 /projects/A/files/F로 수정 요청을 보냈을 때, 사용자의 소유권 검사만 통과하면 저장해도 될까요?
해설 보기
저장하면 안 됩니다. U는 A의 소유자이므로 소유권 검사는 통과하지만, F가 URL에 적힌 A의 파일이라는 조건은 거짓입니다. 그래서 파일의 projectId가 A인지도 함께 검사해야 하고, 이 경우에는 B이므로 요청을 거부합니다.
거부한 응답과 함께 F의 저장된 내용이 바뀌지 않았는지도 확인합니다. 다른 사용자의 프로젝트를 거부하는 사례와, 같은 소유자의 잘못된 부모 URL을 거부하는 사례는 따로 검증합니다.
3. 늦게 끝난 이전 저장
v2의 저장이 먼저 성공한 뒤, 늦게 도착한 v1의 저장이 성공해 최종 내용이 v1이 되었습니다. 클라이언트에 debounce를 적용한 것만으로 이 문제가 해결되었다고 볼 수 있을까요?
해설 보기
볼 수 없습니다. debounce는 요청을 보내는 횟수를 줄일 뿐, 이미 보낸 요청이 끝나는 순서를 바꾸지 않습니다.
저장 요청을 한 번에 하나씩 보내도록 직렬화하거나, 서버에서 예상 버전 조건을 원자적으로 검사하는 충돌 규칙이 필요합니다. 버전 7에서 v2가 먼저 성공해 버전 8이 되었다면, 여전히 버전 7을 기대하는 v1의 쓰기는 충돌로 처리하고 v2를 지켜야 합니다.
다음 글 백엔드 API 구현에서는 이 설계를 바탕으로 인증과 프로젝트·파일 REST API를 NestJS로 구현합니다.