스펙 변경에 강한 API 설계 — 1인 개발 실전 패턴 5가지
📚 모노레포 아키텍처 결정기 시리즈 (4편)
스펙이 바뀔 때마다 API를 뒤엎지 않으려면? NestJS + Prisma 모노레포에서 겪은 5가지 스펙 변경 사고와 그로부터 얻은 API 설계 원칙을 정리했다.
💡 Tip. 바쁜 현대인들을 위한 본문 요약
- 수동 API 문서는 반드시 구버전이 된다 — Swagger 자동 생성을 SSoT로 삼아야 한다
- 스펙 변경 시 마스터 문서 교차 검증 없이 명세서만 보면 구버전 로직이 섞인다
- 3개 이상 컴포넌트 연동 시 데이터 흐름 전체 정의 없으면 “알아서 하겠지” 버그가 터진다
- API 명세와 기존 컨벤션 참조가 없으면
limitvspageSize같은 불일치가 발생한다- 머지 전 10분 Swagger 검증이 머지 후 30분 핫픽스보다 싸다
🔥 증상: 스펙이 바뀌면 API가 무너진다
1인 개발이든 팀 개발이든, 스펙은 바뀐다. 문제는 “바뀌는 것” 자체가 아니라 바뀔 때마다 API 전체를 뒤엎어야 하는 구조에 있다.
NestJS + Prisma + React 모노레포로 서비스를 개발하면서, 스펙 변경 때문에 생긴 사고만 6건. 전부 “API를 처음부터 이렇게 설계했으면 안 터졌을 텐데” 싶은 것들이었다.
아래 5가지는 실제 스펙 변경으로 터졌던 사고를 증상 → 원인 → 해결 → 예방 순서로 정리한 것이다.
1. 수동 API 문서의 함정 — 문서와 코드가 갈라지는 순간

🧨 증상
API를 구현하고 문서(docs/api/*.md)도 같이 작성해뒀다.
2주 뒤, 문서 보고 “이 API 아직 미구현”이라고 판단해서 구현을 지시했다.
결과: 이미 구현된 API를 또 만들라고 지시했다.
문서에는 /items CRUD 4개로 적혀있었는데, 실제 코드에는 5개 엔드포인트가 있었다.
문서가 구현 후 업데이트되지 않았던 거다.
🔍 원인
수동 API 문서의 근본 문제:
1차: API 설계 → docs/api/items.md 작성 ✅
2차: 구현 → 컨트롤러에 엔드포인트 추가 ✅
3차: 문서 업데이트 ❌ ← 여기서 갈라진다
수동 문서는 계획서 역할은 하지만, 구현 후에는 거짓말쟁이가 된다. 구현할 때 엔드포인트가 하나 더 필요해져서 추가했는데, 문서에는 반영을 안 한다. 이게 반복되면 문서는 “현재 상태”가 아니라 “과거 계획”이 된다.
✅ 해결: Swagger를 Single Source of Truth로
수동 문서를 폐기하고, Swagger 자동 생성을 SSoT로 삼았다.
// 컨트롤러에 데코레이터만 추가하면 문서가 자동 생성된다
@ApiOperation({ summary: '아이템 상세 조회' })
@ApiResponse({ status: 200, type: ItemResponseDto })
@Get(':id')
findOne(@Param('id') id: string) {
return this.service.findOne(id);
}
핵심은 코드에서 문서가 나오는 구조를 만드는 것이다.
| 방식 | 코드와 동기화 | 유지 비용 | 신뢰도 |
|---|---|---|---|
| 수동 문서 (Markdown) | ❌ | 높음 | 낮음 |
| Swagger (자동 생성) | ✅ | 낮음 | 높음 |
| Postman Collection | ❌ | 중간 | 중간 |
🛡️ 예방
API 관련 질문이 생기면:
□ 수동 문서 → 무시
□ Swagger /api/docs → 확인
□ 코드 grep → 최종 확인
수동 문서가 없어도 불안하지 않다. Swagger가 곧 문서이고, 컨트롤러 코드가 곧 명세다.
2. 마스터 문서 vs 명세서 — 어떤 걸 믿을 것인가
🧨 증상
서비스 v3.0으로 업데이트하면서 진단 기능의 동작이 바뀌었다.
- v2.0: 진단 결과로 사용자 레벨 자동 배치
- v3.0: 진단은 지표 수집만, 레벨은 커리큘럼 기반
문제는 명세서가 v2.0 기준으로 작성되어 있었다는 것. 명세서만 보고 “진단 결과로 레벨 배치하는 API를 만들어라”고 지시했다.
v3.0 마스터 문서에는 이미 진단 = 레벨 배치 무관이라고 적혀있었다.
🔍 원인
마스터 문서 (v3.0) ← Single Source of Truth
↕ 불일치
명세서 (작성 시점 = v2.1 기준)
→ 명세서만 보면 구버전 로직이 섞인다
명세서는 작성 시점의 스냅샷이다. 마스터 문서가 업데이트되어도 명세서는 자동으로 따라가지 않는다.
✅ 해결: 명세서 작성 시 마스터 교차 검증
지시 전 필수 체크리스트:
□ 마스터 문서 관련 섹션 먼저 확인
□ 명세서와 마스터 문서 불일치 여부 검토
□ 불일치 시 → 명세서 수정 후 지시
□ 핵심 비즈니스 로직은 반드시 마스터 교차 검증
🛡️ 예방
- 마스터 문서 = SSoT, 명세서 = 작업 가이드
- 핵심 로직 변경 시 명세서에 “마스터 섹션 X.X 참조” 링크 포함
- 기존 로직과 달라진 부분은 명세서에 diff 형태로 명시
## 변경사항 (v2.1 → v3.0)
- ~~진단 결과 → 레벨 배치~~ (제거)
- 진단 결과 → 지표 수집만 (신규)
3. “알아서 하겠지” — 데이터 흐름 미정의 사고
🧨 증상
서버(NestJS) → 클라이언트(Unity) → 웹뷰(React) 3개 컴포넌트가 연동되는 구조였다.
서버가 attemptId를 발급해서 클라이언트에 전달하면,
클라이언트가 웹뷰를 로드하면서 그 ID를 넘겨야 했다.
결과: 웹뷰는 서버의 attemptId를 모른다. 자체적으로 sessionId를 만들어서 사용했다.
결과 저장 시 서버의 어떤 레코드에 저장해야 할지 식별할 수 없었다.
🔍 원인
서버: attemptId 발급 ✅
클라이언트: attemptId 수신 ✅
클라이언트 → 웹뷰: attemptId 전달 ❌ ← 여기서 끊겼다
웹뷰: 자체 sessionId 생성 ❌
“클라이언트가 알아서 전달하겠지” — 이 가정이 버그의 시작이었다.
3개 이상 컴포넌트가 연동될 때, 데이터 흐름을 명시적으로 정의하지 않으면 각 컴포넌트가 자기 기준으로 동작한다.
✅ 해결: 데이터 흐름 다이어그램 필수화
통합 설계 시 반드시 4가지를 정의:
□ 데이터 생성 주체 (누가 만드는가?)
→ attemptId는 서버가 생성
□ 데이터 전달 경로 (어떻게 전달되는가?)
→ 서버 → 클라이언트 → 웹뷰 → 클라이언트 → 서버
□ 데이터 저장 주체 (누가 저장하는가?)
→ 클라이언트가 서버 API 호출하여 저장
□ 데이터 식별자 매핑 (ID가 어디서 오는가?)
→ attemptId = 서버에서 발급, 전체 흐름에서 동일 ID 사용
🛡️ 예방
- 컴포넌트가 3개 이상이면 데이터 흐름 다이어그램을 먼저 그린다
- 각 컴포넌트의 입력/출력을 명세에 명시
- “알아서 하겠지”는 문서에 적지 않는 것과 같다
4. 컨벤션 가이드 없는 명세서 — limit vs pageSize
🧨 증상
새로운 리소스의 CRUD API를 구현했다.
페이지네이션 필드를 limit으로 만들었는데, 프론트엔드에서 400 에러가 터졌다.
원인: 기존 20개 이상의 DTO가 pageSize를 쓰고 있었다.
프론트엔드의 DataProvider가 pageSize로 요청하는데, 새 API만 limit을 기대하고 있었던 거다.
🔍 원인
명세서에 “페이지네이션을 구현하라”고만 적혀있었다. 기존 컨벤션 참조가 없었다.
명세서: "목록 조회 API를 만들어라. 페이지네이션 포함."
→ 구현자: "limit/offset으로 하자" (일반적인 패턴)
→ 기존 코드: "pageSize/page로 되어있는데?" (프로젝트 컨벤션)
→ FE: 400 Bad Request
✅ 해결: 명세서에 컨벤션 참조 포함
## 페이지네이션
- 기존 컨벤션: `page` + `pageSize` (UserQueryDto 참조)
- 정렬: `sortBy` + `sortOrder` (기존 패턴 동일)
## 에러 응답
- 기존 패턴: `throw new NotFoundException(...)` 참조
## DTO 구조
- 기존 유사 DTO 3개 이상 grep 후 필드명/타입 통일
🛡️ 예방
명세서 작성 시 체크리스트:
□ 페이지네이션 → "기존 컨벤션: page + pageSize" 명시
□ 에러 응답 → "기존 패턴 참조" 명시
□ DTO 구조 → "유사 DTO grep 후 통일" 지시
□ 가드/데코레이터 → "기존 패턴 참조" 명시
명세서 = 비즈니스 로직 SSoT, 기존 코드 = 구현 컨벤션 SSoT. 둘 다 있어야 일관된 API가 나온다.
5. 머지 전 검증 생략 — “완료 보고 = 실제 완료” 착각
🧨 증상
백엔드 구현 완료 보고를 받았다. “GET/POST/PATCH/DELETE 4개 엔드포인트 구현 완료.”
머지 후 프론트엔드가 착수했는데, GET /items/:id (상세 조회)가 404.
보고에는 “4개”라고 되어있었지만, 실제로는 목록 조회만 있고 상세 조회가 빠져있었다.
React Admin(Refine) 기반 프론트엔드에서는 getOne 없이는 편집 화면이 동작하지 않는다.
🔍 원인
보고: "GET, POST, PATCH, DELETE 4개 완료" ✅
실제: GET(list), POST, PATCH, DELETE = 4개
누락: GET(detail) = getOne ❌
Refine 필수 엔드포인트: getList + getOne + create + update + delete = 5개
→ getOne 누락으로 404
보고만 믿고 머지한 것이 원인이다. Swagger에서 10분만 확인했으면 머지 전에 잡을 수 있었다.
✅ 해결: 머지 전 Swagger 교차 검증
완료 보고 수신 시:
□ Swagger /api/docs 확인
- 보고된 엔드포인트 수 == Swagger 실제 수?
- Refine 리소스면: getList + getOne + create + update + delete = 5개 필수
□ 각 엔드포인트 요청/응답 DTO 확인
□ 문제 발견 시: 머지 전 수정 지시
🛡️ 예방
머지 전 10분 검증 > 머지 후 핫픽스 30분.
Refine 기반 프론트엔드라면 반드시 5 endpoints:
| 엔드포인트 | Refine hook | 역할 |
|---|---|---|
GET /items | useList | 목록 |
GET /items/:id | useOne | 상세 (⚠️ 자주 누락) |
POST /items | useCreate | 생성 |
PATCH /items/:id | useUpdate | 수정 |
DELETE /items/:id | useDelete | 삭제 |
🧹 정리: 스펙 변경에 강한 API 설계 원칙
5가지 핵심 원칙
| # | 원칙 | 해결하는 문제 |
|---|---|---|
| 1 | 코드에서 문서가 나오는 구조 (Swagger) | 문서-코드 불일치 |
| 2 | 마스터 문서 교차 검증 | 구버전 로직 혼입 |
| 3 | 데이터 흐름 전체 정의 | 컴포넌트 간 ID 단절 |
| 4 | 컨벤션 참조 명세서 | 필드명 불일치 |
| 5 | 머지 전 Swagger 검증 | 누락 엔드포인트 |
공통 교훈
“스펙이 바뀌어도 API가 안 무너지는 건 구조의 문제다.”
- 문서는 코드에서 자동 생성되어야 한다
- 명세서는 비즈니스 로직 + 컨벤션 참조 둘 다 있어야 한다
- 통합은 흐름 전체를 정의해야 한다. “알아서”는 버그의 시작이다
- 검증은 머지 전에 해야 한다. 머지 후는 핫픽스다
1인 개발이라 다행히 전부 내가 고칠 수 있었지만, 팀이었으면 각각 핫픽스 + 롤백이 필요했을 사고들이다. 설계 단계에서 이 5가지만 체크하면, 스펙 변경이 “API 전면 수정”이 아니라 “부분 확장”이 된다.
📚 모노레포 아키텍처 결정기 시리즈 (4편)
- 1. NestJS + React 모노레포 구성법 — pnpm workspace 실전기
- 2. pnpm workspace 의존성 삽질기 — 모노레포 5가지 함정
- 3. 스펙 변경에 강한 API 설계 — 1인 개발 실전 패턴 5가지
- 4. 1인 개발 CI/CD 파이프라인 삽질기 — 수동 배포에서 완전 자동화까지