10장. API 명세 — 화면과 DB를 잇는 계약

이 장이 끝나면: 화면기획서(화면이 원하는 것)와 ERD(DB가 가진 것)를 잇는 계약인 API 명세(docs/API-Spec.md) 가 만들어져 있고, 그 명세를 설계하고 관리하는 아키텍트 스킬(/architect) 이 서 있다. 명세의 모든 필드는 ERD의 컬럼에서 나왔고, PRD·ERD·화면기획서와 세 방향으로 대조해 어긋남이 없다. REST API·엔드포인트·상태 코드 같은 용어도 이 장에서 잡는다.


10.1 개념 — 계약이 화면과 DB를 잇는다

마주 보는 양 끝이 생겼다

지금 온티켓 폴더에는 서로 마주 보는 두 문서가 있다.

하나는 docs/화면기획서.md다. 화면마다 무엇을 보여 주고 무엇을 입력받는지를 적은 문서로, 한마디로 '화면이 원하는 것' 이다. 이벤트 상세 화면은 제목·장소·일시와 등급별 가격·잔여 수량을 원하고, 예매 화면은 등급·수량·이메일을 보내 주문번호와 QR을 돌려받길 원한다.

다른 하나는 docs/ERD.md다. DBA가 확정한 테이블과 컬럼, 관계와 제약, 상태값을 적은 문서로, 'DB가 가진 것' 이다. 게다가 이쪽은 문서만 있는 것이 아니다 — Supabase에 테이블 다섯 개가 실제로 서 있고, CSV에서 부은 데이터가 들어 있다.

한쪽은 원하고, 한쪽은 가졌다. 그런데 둘 사이에는 아직 아무것도 없다. 아키텍처를 결정할 때 온티켓을 화면(web/)과 서버(api/)로 나누기로 했고 빈 골격도 세워 두었지만, 그 둘이 무엇을 어떤 이름으로 주고받을지는 어디에도 적혀 있지 않다.

다리가 없으면 각자 다르게 상상한다

이 상태에서 곧바로 "주문 기능 만들어줘"라고 구현을 시키면 무슨 일이 생길까. 서버를 만드는 AI는 구매자 이메일을 buyer_email이라는 이름으로 받기로 한다. 화면을 만드는 AI는 같은 값을 email이라는 이름으로 보내기로 한다. 잔여 수량도 한쪽은 remaining, 다른 쪽은 stock이다. 둘 다 그럴듯한 이름이고, 각자 따로 돌려 보면 잘 돈다. 그런데 둘을 붙이는 순간 화면에 값이 뜨지 않는다. 오류 메시지조차 없다 — 틀린 코드는 없고, 이름이 서로 다를 뿐이기 때문이다.

이렇게 주고받는 데이터 한 칸 한 칸을 필드(field) 라고 부른다. 필드의 이름과 형식을 양쪽이 각자 상상하게 두면 반드시 어긋난다. 그래서 상상할 틈을 없앤다 — 서버가 어떤 창구를 열고, 각 창구가 어떤 필드를 받아 어떤 필드를 돌려주는지를 문서 하나에 먼저 못 박는다. 이 문서가 API 명세다. 화면기획서와 ERD가 마주 보는 양 끝이라면, API 명세는 그 둘을 잇는 계약이다. (API가 정확히 무엇인지는 바로 아래 용어 정리에서 잡는다.)

왜 DB 설계 다음인가

계약서를 쓰려면 계약할 물건이 먼저 있어야 한다. 명세의 필드는 ERD의 컬럼에서 나온다. 테이블도 없고 컬럼도 없는 상태에서 명세를 쓰면, 필드 이름을 지어낼 수밖에 없다. 그리고 지어낸 이름은 나중에 만들어지는 DB와 어긋난다.

온티켓에 실례가 있다. 첫 버전에는 로그인이 없다(PRD의 범위 제외). 그러면 주문한 사람을 무엇으로 알아볼 것인가? 이 질문은 DB를 설계할 때 이미 답이 났다 — orders 테이블에 buyer_email(구매자 이메일) 컬럼을 두고, 과거 데이터에서 온 user_id는 비워 둘 수 있게 남긴다. docs/ERD.md에 사람이 내린 결정으로 적혀 있다. 그래서 이 장의 명세는 고민할 것이 없다. 주문 요청의 필드는 buyer_email이고, 마이티켓은 주문번호와 buyer_email로 조회한다. ERD의 결정을 그대로 받아 쓴다.

순서가 반대였다면 어땠을까. PRD만 보고 명세를 쓰는 AI는 "이메일로 주문을 받는다"는 문장에서 buyer_email이라는 필드를 지어낸다. 한참 뒤 CSV만 보고 DB를 설계하는 AI는 orders.csv에 있는 대로 user_id만 둔다. 두 문서는 각자 멀쩡해 보이지만 서로 어긋나 있고, 그 사실은 구현이 한창일 때에야 드러난다. 검토 기준으로 삼을 문장 하나를 여기서 챙겨 두자 — 명세에만 있고 ERD에 없는 값이 있으면, 어느 한쪽이 거짓말을 하는 것이다.

단, 모든 필드가 컬럼과 일대일은 아니다. 잔여 수량 remaining은 DB에 저장된 값이 아니라 총수량 − 판매수량으로 서버가 계산하는 값이고, 결제 금액 amount도 가격 × 수량으로 서버가 계산한다. 그래도 계산의 재료는 전부 ERD에 있다. ERD에 재료조차 없는 값은 명세에 적을 수 없다.

누가 무엇을 하는지도 갈라 두자. 세 문서를 읽고 명세 초안을 쓰는 것은 AI가 한다. 그 명세가 ERD·화면기획서와 어긋나지 않는지 대조 결과를 읽고, 어긋난 곳을 어느 문서 쪽으로 맞출지 정하는 것은 사람이 한다.

왜 코드보다 먼저인가

계약이 서 있으면 일의 순서가 곧게 펴진다. 먼저 백엔드가 명세(무엇을 주고받을지)와 ERD·DB (실제로 가진 값)를 보고 API 서버를 만든다. 이어서 프론트엔드가 같은 명세와 돌아가는 백엔드를 보고 화면을 붙인다. 검증하는 쪽도 같은 명세를 펴 놓고 채점한다. 만드는 시점은 달라도 보는 문서가 같으니 어긋나지 않는다. 이 이야기는 아래 'API 명세' 절에서 자세히 푼다.

지금까지의 흐름을 한 줄로 그으면 이렇다. 앞 문서가 다음 문서의 입력이 되는 사슬이다.

회의록 → PRD → 화면기획서(화면이 원하는 것) → ERD·DB(가진 것)
      → API 명세(둘을 잇는 계약) → 백엔드(명세 + DB) → 프론트(명세 + 돌아가는 백엔드)

[짚고 가기] 이 사슬이 언제나 한 방향으로만 흐르는 것은 아니다. 명세를 쓰다 보면 "화면은 이 값을 원하는데 ERD에 재료가 없다"가 발견되기도 한다. 그때 명세에 값을 지어 넣지 않고, 질문으로 남겨 ERD 쪽으로 되돌린다. 컬럼을 추가할지 화면에서 그 값을 뺄지는 사람이 정한다. 계약은 양 끝을 잇는 자리라서, 양 끝의 어긋남이 가장 먼저 보이는 자리이기도 하다.


10.2 용어 정리 — API를 처음 만나는 사람을 위해

아키텍처를 결정할 때 온티켓을 화면(프론트엔드)과 데이터 처리(서버)로 나누기로 했고, web/api/라는 빈 골격도 이미 서 있다. 그 둘이 대화하는 방법이 API다. 계약서를 쓰려면 계약서의 말부터 알아야 하니, 용어를 여기서 확실히 잡는다.

이 절은 이 책 전체에서 개발이 처음인 사람에게 가장 낯선 대목일 수 있다. 그러니 천천히, 비유를 하나씩 손에 쥐어 가며 읽자. 여기 나오는 말들 — API, REST, 엔드포인트, GET·POST, 200·404, JSON — 은 남은 과정 내내, 그리고 개발을 계속하는 한 평생 쓰인다. 지금 완벽히 외울 필요는 없다. "아, 그 식당 비유의 그거"라고 떠올릴 수 있을 정도면 충분하다.

시작하기 전에 큰 그림을 하나 다시 그려 두자. 우리가 만드는 온티켓은 두 개의 프로그램 으로 나뉘어 있다. 사용자가 눈으로 보고 클릭하는 화면(프론트엔드), 그리고 그 뒤에서 데이터를 저장하고 계산하는 서버(백엔드). 이 둘은 서로 다른 곳에서 돌아간다 — 화면은 사용자의 브라우저 안에서, 서버는 어딘가의 컴퓨터에서. 물리적으로 떨어진 두 프로그램이 협력해 하나의 서비스를 이루려면, 서로 대화할 방법이 필요하다. 그 대화의 방식과 규칙이 바로 API다. 이 절의 모든 용어는 "떨어진 두 프로그램이 어떻게 대화하는가"라는 한 가지 질문의 세부 사항이라고 생각하면 길을 잃지 않는다.

API (Application Programming Interface)

프로그램끼리 서로 기능을 요청하고 응답받는 약속된 창구다. 식당에 비유하면 정확히 맞아떨어진다.

우리가 매일 쓰는 앱이 전부 이렇게 동작한다. 날씨 앱은 기상청 API에 "오늘 서울 날씨"를 요청하고, 지도 앱은 지도 API에 "이 좌표의 지도 조각"을 요청한다. 카카오페이 결제도, 유튜브 영상 목록도 전부 API 요청과 응답이다.

이름을 한 글자씩 뜯어 보면 개념이 또렷해진다. API는 'Application Programming Interface'의 약자다. 마지막 단어 Interface(인터페이스) 가 핵심인데, 이건 '서로 다른 두 것이 맞닿아 소통하는 접점'을 뜻한다. 자동차의 운전대와 페달이 좋은 예다. 운전자는 엔진이 어떻게 폭발해 바퀴를 굴리는지 하나도 몰라도, 운전대를 돌리고 페달을 밟는 것만으로 차를 몬다. 운전대와 페달이 사람과 엔진 사이의 '인터페이스'다. 마찬가지로 API는 프로그램과 프로그램 사이의 인터페이스 — 상대가 내부적으로 어떻게 돌아가는지 몰라도, 정해진 창구로 요청만 하면 결과를 받을 수 있게 해 주는 접점이다.

식당 비유를 조금 더 늘려 보자. 이 비유는 이 장 내내 쓰이니 확실히 붙잡아 두면 좋다.

고객이 주방에 직접 들어가지 않는다는 점이 중요하다. 만약 고객이 아무 때나 주방에 들어가 냄비를 뒤집을 수 있다면 식당은 아수라장이 된다. 그래서 창구(웨이터, 곧 API)를 통해서만 소통한다. 소프트웨어에서도 화면이 서버의 데이터베이스를 직접 헤집으면 큰일 난다 — 아무나 남의 주문 정보를 지워 버릴 수도 있으니까. API라는 정해진 창구를 두고, 그 창구가 허락한 요청만 받는 것이 안전하고 질서 있는 방식이다.

[짚고 가기] API를 "프로그램끼리 대화하는 법"이라고만 하면 처음엔 여전히 추상적이다. 식당 비유 하나를 손에 쥐고 이 절 내내 그 위에 용어를 얹어 가면 길을 잃지 않는다. GET은 "메뉴 보여 달라", POST는 "주문 넣는다", 200은 "여기 음식 나왔습니다", 404는 "그런 메뉴 없는데요"… 모든 용어가 하나의 비유 안에 자리를 잡으면 머릿속에서 흩어지지 않는다.

서버와 클라이언트 — 요청하는 쪽과 응답하는 쪽

본격적인 용어에 들어가기 전에 한 쌍만 더 잡자. 클라이언트(client)서버(server) 다. API 이야기에는 늘 이 두 역할이 등장한다.

식당 비유에 얹으면 고객이 클라이언트, 주방이 서버다. 요청은 고객에게서 주방으로, 응답은 주방에서 고객으로 흐른다. 대화는 항상 클라이언트가 먼저 요청하고, 서버가 응답하는 방향으로 시작된다는 점을 기억하자. 주방이 고객을 부르지 않는다.

온티켓 예매 요청이 화면→API 서버→DB를 오가는 왕복과 주요 상태 코드

REST API와 엔드포인트

REST API는 API를 설계하는 가장 널리 쓰이는 방식이다. 핵심 아이디어는 단순하다 — 자원(데이터)마다 주소를 주고, 그 주소에 동사(메서드)로 요청한다.

엔드포인트(endpoint) 는 그 주소 하나하나를 말한다. 온티켓이라면:

https://api.onticket.example/events        ← 이벤트 목록이라는 자원의 주소
https://api.onticket.example/events/EV001  ← 특정 이벤트 하나의 주소
https://api.onticket.example/orders        ← 주문이라는 자원의 주소

'REST'라는 이름부터 풀어 보자. REST는 'REpresentational State Transfer'의 약자인데, 이 영어를 외울 필요는 전혀 없다. 대신 REST가 담고 있는 발상 하나만 붙잡으면 된다 — 세상의 모든 것을 '자원(resource)'으로 보고, 자원마다 고유한 주소를 붙인다는 발상이다. '자원'이란 그저 우리가 다루는 데이터 덩어리를 뜻한다. 온티켓에서는 이벤트가 하나의 자원, 주문이 또 하나의 자원, 입장 기록이 또 하나의 자원이다.

이것이 왜 편리한지는 도서관에 비유하면 와닿는다. 도서관의 모든 책에는 청구기호(주소)가 붙어 있다. "3층 815.5번 책"이라고 하면 누구나 그 책 하나를 정확히 찾는다. REST도 똑같다. /events는 '이벤트 목록 전체'라는 자원의 주소, /events/EV001은 'EV001이라는 이벤트 한 권'의 주소다. 주소만 알면 그 자원을 정확히 가리킬 수 있다. 규칙이 단순하고 일관돼서 사람도 AI도 예측하기 쉽다 — 이것이 REST가 세상에서 가장 널리 쓰이는 이유다.

엔드포인트(endpoint) 라는 말도 뜯어 보자. 'end(끝) + point(지점)', 곧 '끝지점'이다. 요청이 도착하는 마지막 지점, 즉 자원 하나하나의 주소를 가리킨다. 위 예에서 URL의 뒷부분 /events, /events/EV001, /orders가 각각 하나의 엔드포인트다. 식당 비유로는 메뉴판의 '항목 한 줄'에 해당한다. 앞부분 https://api.onticket.example는 '온티켓 서버가 있는 건물 주소', 뒷부분 /events는 '그 건물 안에서 찾아갈 창구'라고 생각하면 된다.

주소의 생김새에도 규칙이 있다. 여러 개를 가리킬 때는 복수형 이름을 쓰고(/events, /orders — 이벤트'들', 주문'들'), 그중 하나를 콕 집을 때는 뒤에 식별자를 붙인다 (/events/EV001 — 이벤트들 중 EV001). 이 규칙은 강제는 아니지만 거의 모든 REST API가 따르는 관례라, 주소만 봐도 "아, 목록이구나 / 특정 하나구나"를 짐작할 수 있다.

HTTP 메서드 — 주소에 붙이는 동사

같은 주소라도 "무엇을 하고 싶은지"를 메서드로 구분한다. 네 개만 알면 된다.

메서드 온티켓 예
GET 조회한다 (읽기만) GET /events — 이벤트 목록을 보여줘
POST 새로 만든다 POST /orders — 주문을 하나 생성해줘
PATCH 일부를 고친다 PATCH /orders/123 — 이 주문의 상태를 바꿔줘
DELETE 지운다 DELETE /orders/123 — 이 주문을 삭제해줘

여기서 잠깐 'HTTP'라는 말을 짚자. HTTP(HyperText Transfer Protocol) 는 웹에서 클라이언트와 서버가 대화할 때 쓰는 공용 언어이자 규칙이다. 브라우저 주소창에 http://https://가 붙는 걸 매일 보았을 것이다. 그 HTTP가 바로 이것이다. 세계의 거의 모든 웹 통신이 이 규칙 위에서 이뤄진다. 그리고 HTTP는 요청할 때 "무엇을 하려는지"를 밝히도록 정해 두었는데, 그 '무엇을'에 해당하는 동사가 메서드(method) 다.

앞서 REST의 핵심을 "주소(자원)에 동사(메서드)로 요청한다"고 했다. 이제 그 '동사'가 구체화된다. 같은 /orders라는 주소여도, 그 앞에 어떤 동사를 붙이느냐에 따라 전혀 다른 일이 된다. 이것이 처음엔 헷갈리는데, 일상 언어와 똑같다고 생각하면 쉽다. "그 서류(자원)를 보여줘", "그 서류를 새로 써줘", "그 서류를 고쳐줘", "그 서류를 버려줘" — 대상은 같은 서류지만 동사가 다르니 요청이 다르다. HTTP 메서드가 바로 그 동사다.

네 개를 하나씩, 식당 비유와 함께 새겨 보자.

참고로 PATCH와 비슷한 PUT이라는 메서드도 있다. PUT은 "전체를 통째로 바꿔치기", PATCH는 "일부만 수정"이라는 차이가 있는데, 온티켓 첫 버전에서는 PATCH만 써도 충분하니 지금은 "고칠 땐 PATCH" 정도로 기억해 두면 된다.

메서드를 이렇게 나누는 이유가 있다. 주소만 보고는 "이 요청이 데이터를 바꾸는가, 그냥 읽기만 하는가"를 알 수 없다. 그런데 메서드를 보면 한눈에 안다 — GET이면 안전하게 읽기, POST·PATCH·DELETE면 무언가를 바꾸는 요청. 이 구분은 나중에 "실수로 데이터를 바꾸는 일을 막는" 안전장치의 바탕이 된다. 예를 들어 검색 엔진의 자동 수집 로봇은 GET만 따라다니고 POST는 건드리지 않는다 — GET은 안전하다는 약속을 믿기 때문이다.

[짚고 가기] "왜 주소만으로 안 되고 동사가 필요할까"는 흔히 떠오르는 의문이다. /orders 하나로 '주문 조회'와 '주문 생성'을 어떻게 구분할지 되짚어 보면 답이 저절로 나온다. 같은 창구(주소)에 "볼게요(GET)"와 "넣을게요(POST)"라는 다른 주문을 넣는 그림을 떠올리면 확실히 잡힌다. 메서드 네 개는 억지로 외우기보다 "읽기(GET) 하나, 바꾸기 셋(POST·PATCH·DELETE)"으로 묶어 두면 부담이 줄어든다.

상태 코드 — 응답에 붙는 결과 번호

서버는 응답할 때 결과를 세 자리 숫자로 함께 알린다. 자주 만나는 것만 기억하자.

코드 온티켓 예
200 성공 (조회 등) 이벤트 목록을 잘 보냈다
201 만들었다 주문이 생성됐다
400 요청이 잘못됐다 수량에 -1을 보냈다
404 그런 자원이 없다 없는 이벤트 ID를 조회했다
409 충돌 — 지금 상태와 모순된다 잔여 1장인데 2장을 주문했다
500 서버 쪽에서 사고가 났다 서버 내부 오류

일상에서도 이미 만나고 있다 — 없는 페이지에 들어갔을 때 보는 "404 Not Found"가 바로 이것이다.

상태 코드(status code)를 왜 숫자로 주는지부터 짚자. 서버의 응답을 받는 것은 사람이 아니라 프로그램(화면)이다. 프로그램은 "주문을 잘 처리했어요, 그런데 재고가 모자라네요" 같은 문장을 일일이 읽고 해석하기 어렵다. 그래서 결과를 정해진 숫자로 딱 떨어지게 알려 준다. 화면 쪽 프로그램은 이 숫자만 보고 "아, 200이니 성공, 화면에 목록을 그리자", "409네, 재고 부족이니 안내 메시지를 띄우자"라고 곧바로 판단한다. 사람의 눈치가 필요 없는, 기계끼리의 깔끔한 신호다.

이 숫자에는 규칙이 있어서 첫 자리만 봐도 대략 무슨 일인지 안다. 우편번호 앞자리로 지역을 짐작하는 것과 비슷하다.

앞자리 큰 뜻 어감
2xx 성공 "잘 됐어요"
4xx 요청한 쪽(클라이언트)의 잘못 "당신 요청에 문제가 있어요"
5xx 응답한 쪽(서버)의 잘못 "우리 쪽에서 사고가 났어요"

이 큰 틀만 잡아도 절반은 이해한 것이다. 특히 4xx와 5xx의 구분이 중요하다 — 4로 시작하면 "요청을 보낸 쪽이 뭘 잘못했다", 5로 시작하면 "받은 서버가 고장 났다"는 책임 소재의 차이다. 이제 표의 여섯 개를 하나씩, 온티켓 상황에 얹어 새겨 보자.

'409 Conflict(충돌)'라는 이름의 어감을 한 번 더 새기자. conflict = 충돌, 모순. 요청한 내용이 서버의 현재 상태와 부딪친다는 뜻이다. "2장 주세요"라는 요청과 "지금 1장뿐" 이라는 현실이 충돌한다. 이 개념은 재고뿐 아니라 곳곳에서 쓰인다 — 이미 쓰인 아이디로 가입을 시도하거나, 두 사람이 같은 문서를 동시에 고치려 할 때도 409가 돌아온다.

[짚고 가기] 400·404·409는 가장 헷갈리기 쉬운 조합이다. "요청 형식이 틀렸으면 400, 대상이 없으면 404, 대상은 있는데 지금 상황과 안 맞으면 409"라는 세 문장으로 정리해 두면 좋다. 온티켓의 예매 하나로 셋이 다 나온다 — 수량에 문자를 보냄(400) / 없는 이벤트를 예매(404) / 있는 이벤트인데 재고 부족(409). 특히 409는 오대표의 초과 판매 트라우마와 직결된다. "이 숫자 하나가 골목 라이브 사고를 막는다"고 연결해 두면 오래 기억에 남는다.

JSON — 요청과 응답에 실리는 데이터의 모양

API가 주고받는 데이터는 대부분 JSON이라는 형식으로 쓴다. 중괄호와 따옴표로 이름-값 쌍을 나열하는, 사람도 읽을 수 있는 텍스트다.

{
  "event_id": "EV009",
  "title": "재즈 페스티벌 <리버사이드 재즈>",
  "ticket_types": [
    { "ticket_type_id": "TT020", "grade": "1일권", "price": 99000, "remaining": 1968 }
  ]
}

JSON은 'JavaScript Object Notation'의 약자로, '자바스크립트 객체 표기법'이라는 뜻이다. 원래 자바스크립트라는 언어에서 데이터를 적던 방식인데, 워낙 단순하고 읽기 편해서 이제는 언어를 가리지 않고 API 세계의 공용 데이터 양식이 되었다. 이름의 유래는 자바스크립트지만, 지금은 파이썬이든 무엇이든 다 이 형식으로 데이터를 주고받는다.

왜 이런 형식이 필요할까? 앞서 클라이언트와 서버는 떨어진 두 프로그램이라고 했다. 이 둘이 데이터를 주고받으려면, 서로가 똑같이 알아듣는 약속된 글쓰기 규칙이 필요하다. 우편으로 치면 주소를 쓰는 규칙 같은 것이다. "받는 사람은 여기, 보내는 사람은 저기"라는 양식이 정해져 있어야 우체국이 편지를 배달한다. JSON이 데이터 세계의 그 양식이다.

JSON을 읽는 법은 어렵지 않다. 규칙 몇 개만 알면 된다.

그래서 위 JSON을 사람 말로 풀면 이렇다 — "EV009라는 이벤트가 하나 있는데, 제목은 '재즈 페스티벌 리버사이드 재즈'이고, 티켓 종류로 TT020(1일권, 9만 9천 원, 잔여 1968장)이 있다." 이름표가 붙어 있으니, 개발을 모르는 사람도 무슨 데이터인지 대충 읽힌다. 이 '사람도 읽을 수 있다'는 점이 JSON이 사랑받는 이유다 — 문제가 생겼을 때 사람이 눈으로 확인하기 쉽다.

한 가지 자주 하는 실수를 미리 일러두자. JSON에서는 이름(키)을 반드시 큰따옴표로 감싸야 하고('event_id'가 아니라 "event_id"), 항목 끝의 쉼표 규칙도 엄격하다. 다행히 이 형식을 우리가 손으로 쓸 일은 거의 없다 — API가 자동으로 만들어 주고받는다. 지금은 "응답이 이런 이름표-값 모양의 텍스트로 온다"는 것만 눈에 익히면 충분하다.

[짚고 가기] JSON을 처음 보면 중괄호·대괄호·따옴표에 압도되기 쉽다. 겁먹을 것 없이 "이건 그냥 이름표 붙은 서류"라고 생각하면 된다. 브라우저 개발자 도구(F12)의 네트워크 탭을 열어 아무 사이트나 새로고침해 보면, 실시간으로 오가는 JSON 응답이 그대로 보인다. 지금 보고 있는 화면도 이렇게 생긴 데이터를 받아서 그린 것이다 — 이걸 직접 확인하면 API·JSON이 교과서 밖 현실로 다가온다.


10.3 API 명세 — 코드보다 먼저 쓰는 계약서

무엇인가

API 명세(API Spec) 는 이 서비스에 어떤 엔드포인트가 있고, 각각 무엇을 받아 무엇을 돌려주는지를 적은 문서다. 식당 비유로는 메뉴판 — 주방(서버 개발)과 홀(화면 개발)이 이 메뉴판 하나를 보고 각자 일한다.

'명세(明細)'라는 말은 '밝을 명, 자세할 세', 곧 '자세히 밝혀 적음'이라는 뜻이다. 영어 'specification'을 줄여 'spec(스펙)'이라고도 부른다. 우리가 전자제품을 살 때 보는 '사양표 (스펙)' — 화면 크기, 배터리 용량, 무게가 하나하나 적힌 표 — 와 같은 말이다. API 명세는 곧 'API의 사양표'다. 어떤 창구(엔드포인트)가 있고, 각 창구에 무엇을 넣으면(요청) 무엇이 나오는지(응답), 잘못 넣으면 어떤 오류가 나는지를 빠짐없이 밝혀 적은 문서다.

명세 한 항목의 모양은 이렇다.

### POST /orders — 주문 생성

요청(JSON):
  event_id       string  필수
  ticket_type_id string  필수
  quantity       number  필수, 1~4
  buyer_email    string  필수

응답 201(JSON):
  order_id, qr_code, amount, status("결제대기")

오류:
  400 — 필수값 누락, 수량 범위 밖
  404 — 없는 event_id / ticket_type_id
  409 — 잔여 수량 부족

이 한 항목을 앞 절의 용어로 낱낱이 읽어 보자. 용어 정리에서 익힌 것이 여기서 전부 만난다.

이 표기법 자체는 엄격한 표준이 아니라, 사람과 AI가 함께 읽기 좋게 정리한 형식이다. 실무에서는 이걸 더 엄밀한 기계용 형식(뒤에서 소개할 OpenAPI 등)으로도 쓰지만, 우리 목적에는 이렇게 사람이 읽기 쉬운 명세로 충분하다. 중요한 건 형식이 아니라 빠진 칸이 없는가 — 요청·응답·오류가 다 적혀 있는가다.

왜 코드보다 먼저 쓰는가

이 과정은 백엔드를 먼저 만들고, 그 돌아가는 백엔드에 프론트엔드를 붙인다. 프론트엔드는 명세와 돌아가는 백엔드만 있으면 무엇으로든 만들 수 있기 때문이다. 만드는 시점이 다른 두 역할이 어긋나지 않으려면, 둘이 보는 문서가 같아야 한다. 그 문서가 명세다.

첫째, 모두가 같은 문서를 보는 기준 자료가 된다. 명세 없이 "주문 API 만들어줘"라고 하면 AI가 필드 이름·형식을 마음대로 정한다. 나중에 화면을 만드는 AI는 또 다르게 정한다. 그러면 둘이 안 맞는다. 명세가 있으면 지시가 이렇게 바뀐다 — "명세의 POST /orders 그대로 구현해. 명세에 없는 필드를 추가하면 실패다." 빈칸이 사라진다. 백엔드는 이 문서를 보고 만들고, 프론트엔드는 같은 문서를 보고 붙이고, 검증을 맡는 QA도 같은 문서를 펴 놓고 검사한다.

'기준 자료(source of truth)'라는 말을 짚어 두자. 영어를 직역하면 '진실의 원천'으로, "헷갈릴 때 무엇이 맞는지 판정해 주는 단 하나의 기준" 이다. 서버 쪽 AI는 필드 이름을 buyer_email 로, 화면 쪽 AI는 email로 지었다고 하자. 둘 중 뭐가 맞나? 다툴 필요 없다 — 명세를 펼쳐 보면 된다. 명세에 buyer_email이라 적혀 있으면 그게 정답이고, 화면 쪽이 틀린 것이다. 이렇게 다툼을 끝내 주는 유일한 기준이 기준 자료다. 여러 AI(그리고 여러 사람)가 함께 일할 때, 각자 다르게 기억하고 다르게 짐작하는 것을 막아 주는 닻이다. 명세가 없으면 각자의 머릿속이 기준 자료가 되어 버리고, 그러면 반드시 어긋난다. 그리고 명세 자신의 기준 자료는 ERD다 — 명세의 buyer_email이 맞는 이름인 이유는 ERD에 그 컬럼이 있기 때문이다.

둘째, 구멍이 코드가 생기기 전에 드러난다. "같은 QR을 두 번 찍으면?" 같은 질문에 답이 없다는 사실은, 명세를 써 보면 문서 위에서 드러난다. 그때 메우면 문서 한 줄을 고치면 된다. 코드가 생긴 뒤에 드러나면 명세와 서버 코드와 화면 코드를 함께 고쳐야 한다.

셋째, 검증 기준이 된다. 완성된 API가 맞게 만들어졌는지는 명세와 대조하면 된다.

이것도 풀어 보자. 명세가 없으면 "이 API가 잘 만들어진 건가?"를 판단할 잣대가 없다. 재고 부족일 때 409를 돌려줘야 하는지 400을 돌려줘야 하는지, 무엇이 맞는지 기준이 없으니 버그인지 아닌지도 모른다. 명세에 "재고 부족이면 409"라고 적혀 있으면, 실제로 재고 부족 상황을 만들어 보고 409가 오는지 확인하면 된다. 오면 통과, 400이 오면 버그. 이렇게 명세는 '무엇이 옳은가'의 기준선을 그어 주고, 그 선과 대조하는 것만으로 검증이 된다. 시험 문제에 정답지가 있어야 채점을 하듯, API에는 명세가 있어야 검수를 한다.

세 가지를 한 문장으로 묶으면 — 명세는 시작 전엔 설계도, 만드는 중엔 약속, 끝난 뒤엔 채점 기준이다. 그래서 코드보다 먼저 쓴다.

[짚고 가기] "왜 문서를 먼저 쓰지? 코드부터 짜면 안 되나?"는 이 대목에서 누구나 품는 의문이다. 명세 없이 만든 두 AI의 필드 이름이 어긋나 화면에 값이 안 뜨는 상황을 직접 만들어 보면 답이 분명해진다. 서버는 buyer_email, 화면은 email을 쓰게 해 놓고 "왜 이메일이 안 나올까"를 되짚어 보면, 명세의 필요성이 몸으로 느껴진다. 문서는 일을 늘리는 게 아니라 나중의 삽질을 줄인다 — 이 지점에서 그 사실이 분명해진다.

실무에서 API 명세가 쓰이는 곳

이 사례들을 조금 더 구체적으로 그려 보자. 명세가 실무에서 어떤 얼굴로 나타나는지 알면, "교재용 개념"이 아니라 "현업에서 매일 쓰는 도구"로 느껴진다.

한 걸음 더 — 실무에서는 명세를 사람이 읽는 문서로만 두지 않고, 기계가 읽는 표준 형식으로도 쓴다. 대표적인 것이 OpenAPI(옛 이름 Swagger) 라는 형식이다. 이름 그대로 API를 '열어 (open)' 표준 양식으로 적어 두면, 그 문서만으로 자동으로 테스트 화면을 만들거나, 화면 쪽 연결 코드를 자동 생성하거나, 문서를 예쁘게 렌더링해 준다. 우리 온티켓 첫 버전에서는 여기까지 갈 필요 없이 사람이 읽기 좋은 마크다운 명세로 충분하지만, "명세를 잘 써 두면 그걸로 자동화할 길이 열린다"는 감각은 기억해 두면 좋다. 명세는 그저 문서가 아니라, 여러 자동화의 출발점이다.

[짚고 가기] 여유가 있으면 카카오나 토스의 공개 개발자 문서를 실제로 한 번 열어 보면 좋다. 지금 우리가 만드는 명세가 이 회사들이 공개한 문서와 같은 종류라는 걸 눈으로 확인하면, 자기 작업의 무게가 달리 느껴진다. 특히 토스 결제 문서는 뒤에서 실제로 쓰게 되니, 미리 한 번 눈에 익혀 두면 결제 붙이는 장에서 낯섦이 줄어든다.


10.4 아키텍트 역할 정의 — 네 칸과 스킬

PRD를 쓴 것은 /pm이었다. 그렇다면 API 명세도 PM에게 맡기면 될까? 그렇지 않다. PM의 기준 자료는 회의록과 PRD이고, PM이 답하는 질문은 "무엇을 만들 것인가"다. 명세가 답하는 질문은 다르다 — "화면이 원하는 것과 DB가 가진 것을 어떤 창구와 어떤 필드로 이을 것인가". 이 질문에 답하려면 화면기획서와 ERD를 나란히 펴 놓고 컬럼 하나하나를 짚어야 한다. 기준 자료도 판단 기준도 PM과 다르다. 편성의 원칙은 "자리는 필요가 증명될 때 늘린다"였다. 지금이 그때다 — PM의 판단 기준으로는 할 수 없는 일이 생겼으니, 아키텍트(architect) 라는 자리를 세운다.

역할을 '판단 기준·금지 목록·프로세스·도구 권한'의 네 칸으로 설계하는 틀은 그대로다.

내용
판단 기준 docs/화면기획서.md(화면이 원하는 것)와 docs/ERD.md(DB가 가진 것)가 근거다. 필드는 ERD의 컬럼에서 나온다. 명세에만 있고 ERD에 없는 값은 어느 한쪽의 거짓말이다
금지 목록 ERD에 없는 데이터를 지어내는 것 금지 / PRD 범위 밖 엔드포인트 금지 / 금액·잔여 계산을 클라이언트에 맡기는 설계 금지 / 코드 구현 금지(web/·api/ 수정 금지)
프로세스 세 문서 확인 → 엔드포인트 설계 → 필드를 ERD 대응표와 맞춤 → 세 방향 대조 자기 검증 → 모르는 것은 질문으로 남기고 보고
도구 권한 docs/API-Spec.md만 쓰고 고친다. 나머지 문서와 코드는 읽기만 한다

각 칸이 왜 그렇게 적혔는지 한 줄씩 풀어 두자.

이것을 스킬 파일 하나(.claude/skills/architect/SKILL.md)에 담아 두면, /architect 한마디로 이 규율이 걸린 채 대화가 시작된다. 이후 명세를 만들고, 대조하고, 고치는 모든 지시가 이 규율 위에서 돈다. 명세는 이 장에서 한 번 쓰고 끝나는 문서가 아니다. 구현하다가 계약을 바꿔야 할 일이 생기면, 그때도 코드부터 고치지 않고 /architect를 불러 명세부터 고친다.

[짚고 가기] "PM 스킬에 명세 쓰는 법을 한 줄 더 얹으면 안 되나?"라는 의문이 들 수 있다. 얹을 수는 있다. 다만 그러면 PM의 금지 목록("기술 스택·디자인 결정 금지")과 아키텍트의 일이 한 파일 안에서 부딪친다. 기준 자료가 다르고 금지가 다르면 다른 자리다 — 역할을 나눌지 말지는 일의 양이 아니라 이 둘로 판단한다.


10.5 프롬프트 작성법 — 근거는 세 문서, 지어내면 실패

명세 생성: 입력은 세 문서, 필드는 ERD에서만

프롬프트
/architect @docs/PRD.md @docs/화면기획서.md @docs/ERD.md 를 근거로
API 명세를 만들어서 docs/API-Spec.md로 저장해줘.
규칙:
- PRD의 기능 요구를 빠짐없이 반영할 것. 명세에만 있는 기능이 있으면 안 됨
- 요청·응답의 필드는 ERD의 컬럼(대응표의 영어 이름)에서만 가져올 것.
  ERD에 없는 값이 필요하면 지어내지 말고 명세 끝에 '질문'으로 남길 것
- 화면기획서의 각 화면이 필요로 하는 값이 어느 엔드포인트의 응답에 있는지 표로 정리할 것
- 계산값은 서버가 계산한다고 명시할 것: remaining = 총수량 − 판매수량, amount = 가격 × 수량
- 엔드포인트마다 요청 필드(필수 여부·형식), 성공 응답, 오류 응답(400/404/409)을 명시

첫 줄부터 보자. 입력이 문서 이다. PRD는 "어떤 기능이 있어야 하는가"를, 화면기획서는 "각 화면이 어떤 값을 원하는가"를, ERD는 "그 값이 DB에 어떤 이름으로 있는가"를 댄다. 앞 산출물을 @경로로 지목해 넘기는 것 — 이것이 이 과정에서 역할과 역할이 일을 넘겨받는 방식이다.

이어지는 규칙이 각각 무엇을 막으려는 것인지 짚어 보면, 좋은 지시가 어떻게 '사고를 미리 예방하는가'를 배운다.

약한 지시와 강한 지시를 나란히 놓으면 차이가 뚜렷하다.

흔한(약한) 지시 고친(강한) 지시 무엇을 바꿨나
온티켓 API 명세 만들어줘 @docs/PRD.md @docs/화면기획서.md @docs/ERD.md를 근거로 만들어줘 근거 문서를 파일 경로로 지목
필요한 필드 알아서 넣어줘 필드는 ERD 대응표의 이름에서만. 없으면 질문으로 남겨 지어낼 틈을 없애고 빈칸의 출구를 줌
잔여 수량도 응답에 넣어줘 remaining = 총수량 − 판매수량, 서버가 계산한다고 명시 계산값의 재료와 계산 주체를 못 박음

명세 검토: 세 방향 대조

명세는 세 문서를 근거로 썼으니, 검토도 세 문서 각각과 대조한다.

프롬프트
/architect docs/API-Spec.md를 세 문서와 대조해줘. 아직 고치지 말고 결과만 표로.
1) @docs/PRD.md ↔ 명세
   - PRD의 기능 요구 중 명세에 반영되지 않은 것
   - 명세에는 있는데 PRD에 근거가 없는 엔드포인트
2) @docs/ERD.md ↔ 명세
   - 명세에만 있고 ERD에 없는 필드 (계산값이면 재료가 되는 컬럼을 밝힐 것)
   - ERD에 있는데 어떤 응답에도 안 나오는 컬럼 중 화면이 원하는 것
3) @docs/화면기획서.md ↔ 명세
   - 화면이 원하는데 어느 응답에도 없는 값

PRD ↔ 명세는 양방향으로 본다. 왜 한 방향으로는 부족한지 생각해 보자. "PRD의 기능이 명세에 다 있나?"만 확인하면, 빠뜨린 것은 잡지만 몰래 끼어든 것은 못 잡는다. 반대로 "명세의 창구가 PRD에 근거가 있나?"만 확인하면, 군더더기는 잡지만 빠뜨림은 못 잡는다. 두 방향을 다 봐야 비로소 "빠진 것도 없고 남는 것도 없다"가 확인된다. 빠짐의 대표는 "판매 현황이 PRD에는 있는데 명세엔 조회 창구가 없다" 같은 것. 군더더기의 대표는 "쿠폰 API가 명세에 생겼는데 PRD엔 쿠폰 이야기가 없다" 같은 것. 앞의 것은 채워 넣고, 뒤의 것은 덜어 낸다.

ERD ↔ 명세의 검토 기준은 개념에서 챙겨 둔 그 문장이다 — 명세에만 있고 ERD에 없는 값이 있으면, 어느 한쪽이 거짓말을 하는 것이다. 명세가 값을 지어냈거나, ERD가 필요한 컬럼을 빠뜨렸거나 둘 중 하나다. 어느 쪽인지는 AI가 아니라 사람이 판정한다. 지어낸 값이면 명세에서 지우고, 정말 필요한 값이면 /dba에게 돌려 ERD와 DB부터 고친 뒤 명세에 넣는다. 반대 방향 (ERD에는 있는데 응답에 안 나오는 컬럼)은 전부 문제는 아니다 — DB에는 화면에 보여 줄 필요가 없는 칸도 있다. 그래서 "그중 화면이 원하는 것"으로 좁혀서 묻는다.

화면기획서 ↔ 명세는 계약의 다른 쪽 끝이다. 화면이 원하는 값이 어느 응답에도 없으면, 프론트엔드는 그 값을 지어내거나 화면에서 빼야 한다. 둘 다 지금 잡는 편이 싸다.

명세 수정: 잡힌 것을 고친다

대조로 문제를 찾았으면, 다시 지시로 고친다. 수정 지시도 구체적일수록 좋다.

프롬프트
/architect 대조 결과대로 docs/API-Spec.md를 고쳐줘.
- 빠진 것: '판매 현황 조회(GET /admin/sales)' 엔드포인트를 추가. 응답에 등급별 판매/잔여 수량 포함
- 군더더기: 근거 없는 쿠폰 API(POST /coupons)는 삭제
- ERD에 없는 필드: GET /events 응답의 thumbnail_url은 ERD에 없으므로 삭제하고,
  명세 끝의 '질문'에 "포스터 이미지가 필요한가"로 남길 것
- 고친 뒤, 세 방향 대조를 다시 해서 남은 불일치가 없는지 확인해줘

세 번째 줄을 보자. ERD에 없는 필드를 발견했을 때 할 수 있는 일은 둘이다 — 명세에서 지우거나, ERD에 컬럼을 더하거나. 위 지시는 사람이 "첫 버전에는 포스터 이미지 없이 간다"고 결정한 뒤에 쓴 것이다. AI는 결정을 문서에 옮길 뿐이다.

마지막 줄 "고친 뒤 다시 대조"가 좋은 습관이다. 한 번 고치면 또 다른 어긋남이 생기기도 하므로, 수정→재대조를 불일치가 0이 될 때까지 돌린다. 이 '고치고 다시 확인하는 루프'는 첫 지시를 내릴 때 익힌 지시→승인→확인의 리듬과 같은 것이다.

명세 구멍 메우기: 실패 경우를 묻는다

명세에서 가장 자주 비는 곳은 '잘 되는 경우'가 아니라 '어긋나는 경우'다. AI에게 실패 시나리오를 스스로 열거하게 하면 구멍이 드러난다.

프롬프트
/architect docs/API-Spec.md의 POST /orders와 POST /checkins에 대해,
고객이 잘못하거나 예외가 생길 수 있는 상황을 최대한 나열하고,
각 상황에 어떤 상태 코드로 응답해야 하는지 표로 제안해줘.
(예: 이메일 형식 오류, 재고 부족, 없는 이벤트, 이미 사용된 QR 등)

이렇게 물으면 "이미 찍은 QR을 또 찍으면?", "결제 도중 재고가 떨어지면?" 같은, 사람이 놓치기 쉬운 경계 상황이 표로 튀어나온다. 그 답을 명세에 반영하면 구멍이 메워진다. 구멍은 코드가 생기기 전에 메워야 비용이 가장 적게 든다 — 이 문장은 아래 실습에서 다시 만난다.

[짚고 가기] 프롬프트 네 종류(생성·대조·수정·구멍 메우기)를 한 흐름으로 이어서 해 보면 좋다. 세 문서로 명세를 생성하고 → 세 방향 대조로 어긋남을 잡고 → 수정 지시로 고치고 → 실패 경우를 물어 구멍을 메우는 한 사이클을 직접 돌려 보면, 각 프롬프트가 따로 노는 게 아니라 하나의 작업 리듬임을 알게 된다. "필드는 ERD에서만", "명세에만 있는 기능은 안 됨" 같은 경계선 문장이 왜 매번 들어가는지는, 그 줄을 빼고 생성한 명세를 대조해 보면 확실해진다.


10.6 실습 — 세 문서로 계약을 세운다

시작하기 전에 준비물을 확인한다. 이 장의 입력은 앞에서 만든 세 문서다.

터미널
$ ls docs/PRD.md docs/화면기획서.md docs/ERD.md

[잠깐] 세 파일이 모두 보여야 한다. 하나라도 "No such file"이 나오면 그 문서를 만든 실습으로 돌아가 먼저 끝낸다. 특히 docs/ERD.md가 없거나 Supabase에 테이블이 서 있지 않으면 이 장을 진행할 수 없다 — 명세의 필드를 가져올 곳이 없다.

docs/ERD.md를 열어 두 가지를 눈으로 찾아 둔다 — CSV의 한글 컬럼명을 영어 컬럼명으로 옮긴 대응표, 그리고 주문자 식별 결정(orders.buyer_email). 이 둘이 오늘 명세의 재료다.

따라하기 1 — 아키텍트 스킬 생성

프롬프트
.claude/skills/architect/SKILL.md 로 아키텍트 스킬을 만들어줘.
- 하는 일: docs/화면기획서.md 와 docs/ERD.md 를 근거로 API를 설계하고
  docs/API-Spec.md 를 관리한다. 코드는 구현하지 않는다
- 판단 기준: 필드는 ERD의 컬럼(대응표의 영어 이름)에서 나온다.
  명세에만 있고 ERD에 없는 값은 어느 한쪽의 거짓말이다
- 프로세스: 세 문서(PRD·화면기획서·ERD) 확인 → 엔드포인트 설계 → 필드를 ERD와 맞춤
  → 세 방향 대조 자기 검증 → 모르는 것은 질문으로 남기고 보고
- 금지: ERD에 없는 데이터를 지어내는 것, PRD 범위 밖 엔드포인트,
  금액·잔여 계산을 클라이언트에 맡기는 설계, web/·api/ 코드 수정
- 도구 권한: docs/API-Spec.md 만 쓰고 고친다. 다른 문서와 코드는 읽기만
- description에는 "API 명세를 만들거나 대조·수정할 때 사용"이라고 명시해줘.

위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.

퍼미션을 승인하면 .claude/skills/architect/SKILL.md가 생긴다. 파일을 열어 위 '아키텍트 역할 정의'의 네 칸이 다 들어갔는지 읽어 본다. 특히 "코드는 구현하지 않는다"와 "지어내지 않는다"가 빠졌다면 다시 넣게 한다. 이후 따라하기는 전부 /architect로 시작한다.

따라하기 2 — API 명세를 생성한다

다음 프롬프트를 입력한다.

프롬프트
/architect @docs/PRD.md @docs/화면기획서.md @docs/ERD.md 를 근거로
API 명세를 만들어서 docs/API-Spec.md로 저장해줘.
규칙:
- PRD의 기능 요구를 빠짐없이 반영할 것. 명세에만 있는 기능이 있으면 안 됨
- 요청·응답의 필드는 ERD의 컬럼(대응표의 영어 이름)에서만 가져올 것.
  ERD에 없는 값이 필요하면 지어내지 말고 명세 끝에 '질문'으로 남길 것
- 화면기획서의 각 화면이 필요로 하는 값이 어느 엔드포인트의 응답에 있는지 표로 정리할 것
- 계산값은 서버가 계산한다고 명시할 것: remaining = 총수량 − 판매수량, amount = 가격 × 수량
- 엔드포인트마다 요청 필드(필수 여부·형식), 성공 응답, 오류 응답(400/404/409)을 명시

위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.

열어 보기 전에 예측을 먼저 적어 두자. 주문을 만드는 창구에서 구매자를 가리키는 필드 이름은 무엇일까? email일까, user_id일까, buyer_email일까? 근거는 어느 문서의 어느 줄인가? 적었으면 생성된 docs/API-Spec.md를 열어 다음이 들어 있는지 훑는다.

이 항목들을 확인할 때, 위 용어 정리에서 익힌 눈으로 하나씩 짚어 보자. 각 항목이 어떤 동사 (메서드)와 어떤 주소(엔드포인트)로 되어 있는지, 성공하면 어떤 상태 코드가 오는지를 본다. 특히 세 곳을 눈여겨보라.

따라하기 3 — 세 방향 대조 검토

다음 프롬프트를 입력한다.

프롬프트
/architect docs/API-Spec.md를 세 문서와 대조해줘. 아직 고치지 말고 결과만 표로.
1) @docs/PRD.md ↔ 명세
   - PRD의 기능 요구 중 명세에 반영되지 않은 것
   - 명세에는 있는데 PRD에 근거가 없는 엔드포인트
2) @docs/ERD.md ↔ 명세
   - 명세에만 있고 ERD에 없는 필드 (계산값이면 재료가 되는 컬럼을 밝힐 것)
   - ERD에 있는데 어떤 응답에도 안 나오는 컬럼 중 화면이 원하는 것
3) @docs/화면기획서.md ↔ 명세
   - 화면이 원하는데 어느 응답에도 없는 값

위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.

전형적으로 이런 것이 잡힌다 — "판매 현황이 PRD에는 있는데 명세에 없다"(빠짐), "쿠폰 API가 명세에 생겼는데 PRD에 근거가 없다"(멋대로 추가), "응답에 thumbnail_url이 있는데 ERD에 그런 컬럼이 없다"(지어낸 필드), "화면은 장소를 보여 주는데 목록 응답에 장소가 없다"(화면이 원하는 값 누락).

AI가 표를 내놓으면 판정은 사람이 한다. 표 한 줄마다 "명세를 고칠 것인가, 다른 문서를 고칠 것인가"를 정한다. ERD에 없는 필드는 대부분 명세에서 지우면 끝나지만, 화면에 꼭 필요한 값이라면 명세에 슬쩍 넣지 않는다 — /dba에게 돌려 ERD와 DB를 먼저 고친다.

결정했으면 다음 프롬프트로 고친다. (항목은 자기 대조 결과에 맞게 바꾼다.)

프롬프트
/architect 대조 결과대로 docs/API-Spec.md를 고쳐줘.
- 빠진 것: '판매 현황 조회(GET /admin/sales)' 엔드포인트를 추가. 응답에 등급별 판매/잔여 수량 포함
- 군더더기: 근거 없는 쿠폰 API(POST /coupons)는 삭제
- ERD에 없는 필드: GET /events 응답의 thumbnail_url은 ERD에 없으므로 삭제하고,
  명세 끝의 '질문'에 "포스터 이미지가 필요한가"로 남길 것
- 고친 뒤, 세 방향 대조를 다시 해서 남은 불일치가 없는지 확인해줘

위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.

퍼미션 요청이 뜨면 미리보기를 훑는다. 설치 직후 익힌 "읽고 누르는" 습관이 여기서도 쓰인다 — AI가 고치려는 파일이 정말 docs/API-Spec.md 하나인지, ERD나 PRD를 건드리려 하지는 않는지 본다.

고친 뒤에는 반드시 다시 대조해 남은 불일치가 0인지 확인한다. 한 번의 수정이 새 어긋남을 만들 수 있으니, 대조 결과가 세 방향 모두 "빠짐 없음, 군더더기 없음, 지어낸 필드 없음"으로 깨끗해질 때까지 이 루프를 돈다.

따라하기 4 — 동료 교차 검토와 구멍 메우기

옆 사람과 명세를 바꿔 읽는다. 서로의 명세에 대해 질문 두 개씩만 던져 보자 — "주문할 때 이메일이 틀리면 어떻게 되나요?", "같은 QR을 두 번 찍으면요?" 답이 명세에 없다면, 그것이 명세의 구멍이다. 구멍은 지금 메워야 비용이 가장 적게 든다. (코드가 생긴 뒤에 메우면 코드도 같이 고쳐야 한다.)

동료가 던진 질문 중 명세에 답이 없는 것이 나오면, 다음 프롬프트로 그 실패 상황들을 정리시키고 명세에 반영한다.

프롬프트
/architect docs/API-Spec.md의 POST /orders와 POST /checkins에 대해,
고객이 잘못하거나 예외가 생길 수 있는 상황을 최대한 나열하고,
각 상황에 어떤 상태 코드로 응답해야 하는지 표로 제안해줘.
(예: 이메일 형식 오류, 재고 부족, 없는 이벤트, 이미 사용된 QR 등)

위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.

예컨대 "같은 QR을 두 번 찍으면?"에 대한 답이 명세에 없었다면, POST /checkins에 "이미 사용된 QR이면 409(또는 정해진 코드)로 '이미 입장 처리됨'을 알린다"를 추가한다. 용어 정리에서 새긴 상태 코드가 여기서 실전으로 쓰인다.

[짚고 가기] 교차 검토는 이 장에서 가장 얻는 게 많은 실습이다. 남의 명세에서 구멍을 찾는 건 의외로 쉽고, 그 과정에서 "아, 내 명세에도 이게 빠졌네"를 스스로 깨닫게 된다. 질문은 두 개로 제한하는 게 요령이다 — 무제한이면 산만해진다. "잘못된 입력"과 "중복/충돌" 계열에서 하나씩 뽑으면 좋은 질문이 나온다. 그렇게 찾은 구멍을 그 자리에서 명세에 반영하는 데까지 가 보면, "검토가 문서를 실제로 좋게 만든다"는 경험이 완성된다.

완료 기준 체크리스트


10.7 정리

오명운 · macro@prag-ai.com