스펙 변경에 강한 API 설계 — 1인 개발 실전 패턴 5가지

스펙이 바뀔 때마다 API를 뒤엎지 않으려면? NestJS + Prisma 모노레포에서 겪은 5가지 스펙 변경 사고와 그로부터 얻은 API 설계 원칙을 정리했다.


💡 Tip. 바쁜 현대인들을 위한 본문 요약

  • 수동 API 문서는 반드시 구버전이 된다 — Swagger 자동 생성을 SSoT로 삼아야 한다
  • 스펙 변경 시 마스터 문서 교차 검증 없이 명세서만 보면 구버전 로직이 섞인다
  • 3개 이상 컴포넌트 연동 시 데이터 흐름 전체 정의 없으면 “알아서 하겠지” 버그가 터진다
  • API 명세와 기존 컨벤션 참조가 없으면 limit vs pageSize 같은 불일치가 발생한다
  • 머지 전 10분 Swagger 검증이 머지 후 30분 핫픽스보다 싸다

🔥 증상: 스펙이 바뀌면 API가 무너진다

1인 개발이든 팀 개발이든, 스펙은 바뀐다. 문제는 “바뀌는 것” 자체가 아니라 바뀔 때마다 API 전체를 뒤엎어야 하는 구조에 있다.

NestJS + Prisma + React 모노레포로 서비스를 개발하면서, 스펙 변경 때문에 생긴 사고만 6건. 전부 “API를 처음부터 이렇게 설계했으면 안 터졌을 텐데” 싶은 것들이었다.

아래 5가지는 실제 스펙 변경으로 터졌던 사고를 증상 → 원인 → 해결 → 예방 순서로 정리한 것이다.



1. 수동 API 문서의 함정 — 문서와 코드가 갈라지는 순간

새벽까지 1. 수동 API 문서의 함정 — 문서와 코드가 갈라지는 순간 삽질하다 녹초가 된 모습
새벽까지 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 /itemsuseList목록
GET /items/:iduseOne상세 (⚠️ 자주 누락)
POST /itemsuseCreate생성
PATCH /items/:iduseUpdate수정
DELETE /items/:iduseDelete삭제

🧹 정리: 스펙 변경에 강한 API 설계 원칙

5가지 핵심 원칙

#원칙해결하는 문제
1코드에서 문서가 나오는 구조 (Swagger)문서-코드 불일치
2마스터 문서 교차 검증구버전 로직 혼입
3데이터 흐름 전체 정의컴포넌트 간 ID 단절
4컨벤션 참조 명세서필드명 불일치
5머지 전 Swagger 검증누락 엔드포인트

공통 교훈

스펙이 바뀌어도 API가 안 무너지는 건 구조의 문제다.

  • 문서는 코드에서 자동 생성되어야 한다
  • 명세서는 비즈니스 로직 + 컨벤션 참조 둘 다 있어야 한다
  • 통합은 흐름 전체를 정의해야 한다. “알아서”는 버그의 시작이다
  • 검증은 머지 전에 해야 한다. 머지 후는 핫픽스다

1인 개발이라 다행히 전부 내가 고칠 수 있었지만, 팀이었으면 각각 핫픽스 + 롤백이 필요했을 사고들이다. 설계 단계에서 이 5가지만 체크하면, 스펙 변경이 “API 전면 수정”이 아니라 “부분 확장”이 된다.