사내 API나 직접 만든 서비스는 문서가 없는 경우가 흔합니다.
목록은 되는데 개별 조회가 안 될 때, 경로를 추측해가며 찾아야 합니다.
오늘 그 작업을 했고, 중간에 엉뚱한 결론을 내릴 뻔했습니다. 그 과정을 적습니다.
결론부터
| 단계 | 하는 일 |
|---|---|
| 1 | 응답 형태로 "누가 404를 줬는지" 구분한다 |
| 2 | REST 관례대로 경로를 순서대로 찍어본다 |
| 3 | 쿼리 파라미터로 필드가 늘어나는지 본다 |
| 4 | 페이지네이션 메타를 먼저 확인한다 ← 안 하면 오판한다 |
| 5 | 그래도 없으면 없는 것이다. 추측을 멈춘다 |
1. 404를 누가 줬는지 구분한다
프록시나 게이트웨이를 거치는 구조라면, 404가 두 군데서 나올 수 있습니다.
// 프록시가 준 것 — 경로가 중계 대상이 아님
{"error":"not_found","hint":"this relay only forwards /api/xxx/*"}
// 상류 서버가 준 것 — 중계는 됐는데 서버에 경로가 없음
{"error":"not found"}
언더스코어(not_found)와 공백(not found), hint 유무가 달랐습니다.
이걸 구분하지 못하면 "프록시 설정이 잘못됐나" 하고 엉뚱한 곳을 고치게 됩니다.
프록시 로그를 볼 수 있으면 더 확실합니다.
[relay] GET /api/xxx/documents/2 -> 404 ← 상류가 404를 줬다는 뜻
상류가 응답했다는 건 중계는 정상이라는 뜻입니다.
2. 경로를 순서대로 찍어본다
REST 관례를 따르는 순서로 시도합니다.
B=https://example.com/api/resource
for p in "items/2" "items/2/content" "items/2/raw" "items/2/markdown" \
"item/2" "detail/2" "view/2" "2"; do
printf " %-24s " "$p"
curl -s "$B/$p" | head -c 60
echo
done
식별자가 여럿이면 그것도 바꿔봅니다.
# id / slug / 사용자 지정 키
curl -s "$B/items/2"
curl -s "$B/items/my-document-slug"
curl -s "$B/items/by-key/my-stable-key"
쓰기 경로를 먼저 확인하면 힌트가 됩니다. 업로드 스크립트가 있다면 거기서
어느 경로로 POST하는지 보면 리소스 이름이 드러납니다.
grep -n "fetch(\|method:" upload.js
# 107: await fetch(`${BASE}/api/resource/items`, {
# 108: method: 'POST',
→ POST /api/resource/items 가 있으니 GET /api/resource/items/:id 를 기대하게 됩니다.
다만 기대일 뿐, 실제로 구현돼 있다는 보장은 없습니다.
3. 쿼리 파라미터로 필드가 늘어나는지
목록 API가 요약만 준다면, 파라미터로 전체를 요청할 수 있는지 봅니다.
base=$(curl -s "$B/items" | python3 -c "
import sys,json;d=json.load(sys.stdin)
print(max(len(str(v)) for v in d['items'][0].values() if isinstance(v,str)))")
echo "기준 최장 필드: ${base}자"
for q in "include=content" "fields=content" "full=true" "expand=content" "raw=1"; do
n=$(curl -s "$B/items?$q" | python3 -c "
import sys,json;d=json.load(sys.stdin)
print(max(len(str(v)) for v in d['items'][0].values() if isinstance(v,str)))")
printf " %-20s %s자%s\n" "$q" "$n" "$([ "$n" -gt "$base" ] && echo ' ← 늘었다')"
done
길이를 비교하는 게 핵심입니다. 응답이 200이어도 파라미터를 무시하고 같은 걸
돌려줄 수 있습니다. 눈으로 보면 "뭔가 왔네" 하고 넘어가게 됩니다.
⚠️ 4. 페이지네이션 — 여기서 오판했습니다
목록을 받아서 "총 20건이구나" 하고 작업을 진행했습니다.
그런데 검색을 해보니 목록에 없던 문서가 나왔습니다. 분명 목록에서 못 봤는데
검색에는 걸리는 겁니다.
응답을 다시 보니 이랬습니다.
{
"items": [ ...20개... ],
"total": 46, ← 전체는 46건
"page": 1,
"size": 20 ← 한 번에 20건씩
}
items 길이만 세고 total 을 안 봤습니다. 실제는 46건인데 20건으로 알고
"로컬에 원본이 있는 건 2건뿐"이라는 결론까지 냈습니다. 절반만 보고 내린 판단이었습니다.
# 전체를 가져오려면
curl -s "$B/items?size=200" # 또는 limit / per_page — 서버마다 다르다
파라미터 이름도 서버마다 다릅니다. 세 개를 시도해보니 size 만 먹혔습니다.
limit=200 → 20건 (무시됨)
per_page=200 → 20건 (무시됨)
size=200 → 46건 ✅
목록 API를 만나면 응답의 메타 필드부터 봅니다.
total,count,has_more,next,page같은 것들입니다.
없으면 없다는 것도 확인해야 합니다.
5. 없으면 없는 것이다
경로 13종, 파라미터 12종을 시도해도 안 나오면 그 기능이 없는 것입니다.
더 추측하는 건 시간 낭비입니다. 그때는 방향을 바꿉니다.
- 서버 코드를 본다 — 라우트 정의가 가장 확실합니다
- 클라이언트가 쓰는 경로를 본다 — 웹 UI가 보여준다면 어딘가로 요청하고 있습니다.
브라우저 개발자도구 네트워크 탭에 그대로 나옵니다 - 만든 사람에게 묻는다
- 없으면 추가한다 — 자체 서비스라면 이게 가장 깔끔합니다
저는 마지막을 택했습니다. 엔드포인트 하나를 추가하는 게, 없는 길을 계속 찾는 것보다 빨랐습니다.
정리
- 404를 누가 줬는지 구분하세요. 프록시인지 상류인지에 따라 고칠 곳이 다릅니다
- 쓰기 경로에서 리소스 이름 힌트를 얻을 수 있습니다
- 파라미터 시험은 응답 길이를 비교하세요. 200이어도 무시됐을 수 있습니다
total을 보세요.items길이는 전체가 아닙니다- 파라미터 이름(
size/limit/per_page)은 서버마다 다릅니다 - 충분히 찾았는데 없으면 없는 겁니다. 추측을 멈추고 방향을 바꾸세요