API를 처음 설계할 때 가장 헷갈리는 것은 코드보다 기준입니다.
회원 정보를 가져오는 주소를 /getUser로 해야 할지 /users/123으로 해야 할지, 수정할 때 POST를 써도 되는지 PUT이나 PATCH를 써야 하는지, 실패하면 무조건 400을 보내면 되는지 — 처음에는 판단할 근거가 없습니다.
결론부터 말하면, 처음이라면 이 순서로 네 가지만 잡으면 됩니다.
- 무엇을 다루는 API인가 (자원 찾기)
- 그 대상에 어떤 작업을 하는가 (HTTP 메서드)
- 결과를 어떻게 알려주는가 (상태 코드)
- 성공·실패 응답을 어떤 형태로 유지하는가 (응답 일관성)
이 글에서는 회원 API 하나를 예로 이 순서대로 기준을 세웁니다. 그리고 글에 나오는 주장 중 실행으로 확인할 수 있는 것들 — 400과 422의 차이, PUT을 잘못 쓰면 데이터가 사라진다는 경고 — 은 실제로 서버를 만들어 실행한 결과로 보여드립니다.
손을 먼저 움직이며 배우고 싶다면 실습편(REST API 설계, URL부터 막힌다면: 초보자가 꼭 알아야 할 7가지 원칙)이 맞고, 이 글은 그때 내리는 선택들의 ‘기준’을 다룹니다.
시작 전에: REST는 URL 작성법의 이름이 아니다
REST는 특정 URL 규칙의 이름이 아니라, Roy Fielding의 박사 논문에서 정리된 분산 시스템의 아키텍처 스타일입니다. Client-Server, Stateless, Cache, Uniform Interface 같은 여러 제약을 함께 다루며, Fielding은 그중에서도 일관된 인터페이스를 REST를 구분 짓는 핵심으로 강조했습니다.
따라서 GET /users/123처럼 URL을 만들었다고 해서 자동으로 엄밀한 의미의 REST API가 되는 것은 아닙니다.
다만 실무에서는 REST의 생각을 가져와 자원을 중심으로 주소를 만들고, HTTP가 이미 정의해 둔 메서드와 상태 코드의 의미를 최대한 활용하는 방식이 널리 쓰입니다. 이 글도 엄밀한 REST 이론 전체가 아니라, 처음 API를 설계할 때 필요한 HTTP API 설계 기준에 집중합니다.
1. URL부터 만들지 말고 ‘자원’을 먼저 찾는다
회원 관리 API를 만든다고 해보겠습니다. 먼저 필요한 질문은 이것입니다.
우리 시스템에서 사용자가 다루게 될 대상은 무엇인가?
여기서는 user, 즉 회원이 자원입니다. 그래서 URL도 동작이 아니라 자원을 중심으로 잡습니다.
/users ← 회원 집합
/users/123 ← ID가 123인 특정 회원
반대로 /getUser, /createUser처럼 동작을 URL에 붙이기 시작하면, API가 커질수록 규칙을 예측할 수 없게 됩니다. 같은 /users/123이라는 자원에 어떤 작업을 할지는 HTTP 메서드가 표현하도록 나누는 것이 다음 단계입니다.
동사 URL이 왜 문제가 되는지, URL을 얼마나 깊게 만들어도 되는지는 실습편의 원칙 1~3에서 예제와 함께 다루므로 여기서는 반복하지 않습니다. 이 글의 관심은 그다음, 즉 메서드와 상태 코드에 이미 정의된 의미입니다.
2. HTTP 메서드는 CRUD 이름표가 아니라 의미가 있다
입문 단계에서는 흔히 이렇게 외웁니다: GET=조회, POST=생성, PUT=전체 수정, PATCH=부분 수정, DELETE=삭제. 편리하지만 정확히는 조금 다릅니다. HTTP 메서드는 데이터베이스의 CRUD 명령이 아니라 HTTP 요청의 의미를 정의하며, 그 의미는 RFC 9110 (HTTP Semantics)에 정리되어 있습니다.
회원 API 기준으로 표 하나로 정리하면 이렇습니다.
| 메서드 | RFC 9110 기준 의미 | 멱등성 | 회원 API 예시 | 흔한 오해 |
|---|---|---|---|---|
| GET | 대상 자원의 현재 표현을 조회 | O (안전) | GET /users/123 |
“조회니까 요청 바디에 조건을 담아도 된다” — GET의 요청 바디에는 표준적으로 정의된 의미가 없음 |
| POST | 대상 자원이 요청 데이터를 처리하게 함 | X | POST /users |
“생성 전용 메서드다” — 컬렉션 아래 새 자원 생성에 흔히 쓰일 뿐, 정의는 더 넓음 |
| PUT | 대상 URI의 자원을 요청 내용으로 생성하거나 전체 교체 | O | PUT /users/123 |
“수정 = PUT” — 부분 수정에 쓰면 보내지 않은 필드가 사라질 수 있음 |
| PATCH | 자원에 부분 변경을 적용 (RFC 5789) | 기본적으로 X | PATCH /users/123 |
“PUT의 축약형이다” — 별도 RFC로 정의된 다른 메서드 |
| DELETE | 대상 자원의 삭제를 요청 | O | DELETE /users/123 |
“두 번째 호출은 반드시 실패해야 한다” — 멱등성은 서버 상태 기준이며, 두 번째 호출이 404를 돌려주는 것과는 별개 |
멱등성(idempotency) 은 처음 보면 어려운 단어인데, 뜻은 단순합니다. 같은 요청을 여러 번 보내도 서버 상태에 미치는 효과가 한 번 보낸 것과 같다는 성질입니다. 네트워크 오류로 요청을 재시도해도 안전한지 판단할 때 기준이 됩니다.
“보내지 않은 필드가 사라진다”는 게 진짜일까 — 직접 확인
표에서 가장 위험한 오해가 “수정 = PUT”입니다. 정말 필드가 사라지는지, PUT을 RFC 의미 그대로(전체 교체) 구현한 서버에서 확인해봤습니다.
먼저 회원 123의 원래 상태입니다.
$ curl -i localhost:3002/users/123
HTTP/1.1 200 OK
{"id":123,"name":"Quiet","email":"quiet@example.com"}
이메일만 바꾸고 싶어서, PATCH로 변경할 데이터만 보냅니다.
$ curl -i -X PATCH -H "Content-Type: application/json" \
-d '{"email":"new@example.com"}' localhost:3002/users/123
HTTP/1.1 200 OK
{"id":123,"name":"Quiet","email":"new@example.com"}
name이 그대로 있습니다. 이번에는 같은 데이터를 PUT으로 보내보겠습니다.
$ curl -i -X PUT -H "Content-Type: application/json" \
-d '{"email":"only@example.com"}' localhost:3002/users/123
HTTP/1.1 200 OK
{"id":123,"email":"only@example.com"}
name이 사라졌습니다. PUT은 “보낸 내용으로 자원을 교체”하는 의미이므로, name을 보내지 않았으니 교체된 자원에는 name이 없는 것입니다. 재조회해도 결과는 같았습니다. 오해가 아니라 실제로 일어나는 일입니다.
물론 실제 서비스에서는 API 정책에 따라 PUT을 다르게 구현하기도 합니다. 중요한 것은 특정 패턴을 기계적으로 복사하는 것이 아니라, 우리 서비스 안에서 두 메서드의 의미를 어떻게 정의했는지 정하고 일관되게 유지하는 것입니다.
3. 상태 코드는 ‘성공/실패 숫자’가 아니다
API는 데이터만 보내는 것이 아니라 요청이 어떻게 처리됐는지도 알려줘야 합니다. HTTP는 이를 위해 상태 코드를 정의합니다.
회원 한 명을 정상 조회했다면 200 OK, 새 회원을 만들었다면 201 Created, 성공했지만 돌려줄 본문이 없다면 204 No Content를 쓸 수 있습니다.
오류도 실제 상황에 맞게 구분합니다.
400 Bad Request 요청 형식이나 내용에 문제가 있음
401 Unauthorized 인증 자격 증명이 없거나 유효하지 않음
403 Forbidden 요청을 이해했지만 허용하지 않음
404 Not Found 요청한 자원을 찾지 못함
409 Conflict 현재 자원 상태와 요청이 충돌함
422 Unprocessable Content 형식은 맞지만 내용을 의미상 처리할 수 없음
400과 422의 차이가 궁금할 수 있습니다. 말로 하면 “JSON 자체가 깨졌으면 400, JSON은 멀쩡한데 이메일 형식이 틀린 것처럼 내용을 처리할 수 없으면 422″인데, 실제로 그렇게 나오는지 회원 생성 API에 두 가지 잘못된 요청을 보내 확인해봤습니다.
케이스 1 — JSON 자체가 깨진 요청:
$ curl -i -X POST -H "Content-Type: application/json" \
-d '{"name": ' localhost:3002/users
HTTP/1.1 400 Bad Request
{"type":"https://api.example.com/problems/malformed-json","title":"Malformed JSON","status":400,"detail":"요청 본문의 JSON 형식이 깨져 있습니다.","instance":"/users"}
케이스 2 — JSON은 멀쩡한데 email 형식이 잘못된 요청:
$ curl -i -X POST -H "Content-Type: application/json" \
-d '{"name":"Bad","email":"not-an-email"}' localhost:3002/users
HTTP/1.1 422 Unprocessable Entity
{"type":"https://api.example.com/problems/invalid-email","title":"Invalid email","status":422,"detail":"email 형식이 올바르지 않습니다.","instance":"/users"}
같은 “잘못된 요청”이지만 원인이 다르고, 상태 코드가 그 차이를 표현합니다. 참고로 422의 공식 명칭은 RFC 9110에서 Unprocessable Content로 정리됐지만, 위 실행 결과에서 보듯 많은 서버 구현은 아직 예전 문구(Unprocessable Entity)를 출력합니다 — 의미는 같습니다.
반대로 흔한 실수는 내부에서 실패했는데 응답은 항상 200 OK로 보내고 본문에만 "success": false를 넣는 방식입니다. 이렇게 만들면 HTTP가 이미 제공하는 의미를 클라이언트가 활용할 수 없게 됩니다. 이 안티패턴이 실제 호출에서 어떤 차이를 만드는지는 실습편에서 직접 만들며 확인합니다.
4. 오류 응답 형식을 처음부터 통일한다
실제로 API를 쓰다 보면 성공보다 오류를 처리할 일이 많습니다. 그런데 API마다 오류 모양이 {"error": "..."}, {"message": "...", "code": 1004}, {"result": false}처럼 제각각이면, 사용하는 쪽은 매번 별도의 처리 코드를 짜야 합니다.
프로젝트 안에서는 한 가지 오류 구조를 정해두는 편이 좋습니다. RFC 9457 (Problem Details) 은 HTTP API 오류를 기계가 읽을 수 있는 형태로 표현하는 표준 형식을 정의하며, JSON에서는 application/problem+json 미디어 타입을 사용합니다.
위의 400·422 응답이 이미 이 형식이었는데, 없는 회원을 조회했을 때의 응답으로 구조를 한 번 더 보겠습니다. 이번에는 응답 헤더까지 함께 봅니다.
$ curl -i localhost:3002/users/999
HTTP/1.1 404 Not Found
Content-Type: application/problem+json; charset=utf-8
{"type":"https://api.example.com/problems/user-not-found","title":"User not found","status":404,"detail":"id 999 회원을 찾을 수 없습니다.","instance":"/users/999"}
type: 오류 종류를 나타내는 식별자 (프로그램이 분기 처리에 사용)title/detail: 사람이 읽는 요약과 상세status: 상태 코드,instance: 문제가 발생한 위치
서버 쪽 구현도 어렵지 않습니다. 검증에 쓴 서버(실습편의 Express 서버와 같은 방식)에서 핵심 부분만 보면 이렇습니다.
const problem = (res, status, type, title, detail, instance) =>
res.status(status).type('application/problem+json')
.json({ type: `https://api.example.com/problems/${type}`, title, status, detail, instance });
// 사용 예: email 형식 오류
if (!EMAIL_RE.test(email)) {
return problem(res, 422, 'invalid-email', 'Invalid email',
'email 형식이 올바르지 않습니다.', '/users');
}
꼭 RFC 9457이어야 좋은 API가 되는 것은 아닙니다. 더 중요한 것은 클라이언트가 오류의 종류와 원인을 예측 가능한 방식으로 처리할 수 있도록 응답 형태를 일관되게 유지하는 것입니다.
5. 버전은 문자열이 아니라 호환성 정책이다
버전을 배울 때 흔히 /api/v1/users부터 시작합니다. 이 방법 자체가 잘못은 아닙니다. 하지만 중요한 것은 /v1이라는 문자열이 아니라, 이미 API를 쓰고 있는 클라이언트를 변경이 깨뜨리지 않는 것입니다.
기존 응답의 name 필드를 어느 날 nickname으로 바꿔버리면 기존 클라이언트가 동작을 멈출 수 있습니다. 그래서 버전 표기보다 먼저 정해야 할 것은 정책입니다.
- 기존 필드를 변경·제거해야 할 때 어떻게 할 것인가
- 새 버전이 필요할 정도의 breaking change 기준은 무엇인가
- 오래된 버전을 언제까지 지원할 것인가
이 세 가지에 답이 있으면 URL 버전이든 헤더 버전이든 표기 방식은 따라옵니다. 답이 없으면 /v1을 붙여도 같은 문제를 겪습니다.
6. 문서는 나중에 쓰지 말고 설계와 함께 기록한다
엔드포인트가 몇 개 없을 때는 기억할 수 있지만, API가 늘어나면 경로·파라미터·응답·오류·인증 여부를 코드만 보고 파악하기 어려워집니다.
이때 OpenAPI Specification을 사용할 수 있습니다. HTTP API를 언어에 종속되지 않는 형태로 설명하는 표준이며, 명세를 기반으로 문서화·클라이언트 생성·테스트 도구에 활용할 수 있습니다.
처음부터 거대한 문서 시스템을 갖출 필요는 없습니다. 최소한 각 API에서 이 정도는 확인할 수 있어야 합니다: 메서드 + URL, 요청 데이터, 성공 응답, 오류 응답, 인증 여부, 간단한 설명.
회원 API를 하나로 정리해보면
지금까지의 기준으로 설계한 회원 API의 실제 동작을 한 표로 모으면 이렇습니다. 아래 상태 코드는 전부 이 글을 쓰며 실행해 받은 실제 응답입니다.
| 요청 | 실제 응답 |
|---|---|
GET /users/123 |
200 OK — 회원 정보 |
GET /users/999 (없는 회원) |
404 Not Found — problem+json |
POST /users (정상 데이터) |
201 Created — 생성된 회원 |
POST /users (깨진 JSON) |
400 Bad Request — problem+json |
POST /users (email 형식 오류) |
422 Unprocessable — problem+json |
PATCH /users/123 (email만) |
200 OK — name 유지됨 |
PUT /users/123 (email만) |
200 OK — name 사라짐 (전체 교체) |
오류 본문은 서비스 전체에서 같은 형태(Problem Details)로 유지했습니다. 이 정도만 일관되게 적용해도, 기능이 생길 때마다 /getUser, /modifyUserInfo처럼 새 규칙을 만들어내는 API보다 훨씬 예측하기 쉬워집니다.
마무리: 기준이 있으면 논쟁이 짧아진다
API 설계에서 시간을 잡아먹는 것은 코드가 아니라 “이럴 땐 뭘 써야 하지?”라는 반복되는 논쟁입니다. 자원·메서드·상태 코드·오류 형식·호환성 정책 — 이 다섯 가지에 기준이 있으면, 새 기능이 생길 때마다 논쟁하는 대신 기준에 대입하면 됩니다.
새 API를 설계할 때 확인할 질문으로 정리하면:
- 무엇이 자원인가? URL만 봐도 어떤 대상인지 알 수 있는가?
- HTTP 메서드의 실제 의미와 요청 목적이 맞는가?
- 성공과 실패에 적절한 상태 코드를 쓰고 있는가?
- 오류 응답 형식이 API마다 달라지지 않는가?
- 기존 클라이언트를 깨뜨릴 변경을 어떻게 관리할 것인가?
- 다른 개발자가 명세만 보고 사용할 수 있는가?
다음 단계
읽기만 하면 기준은 남지 않습니다. 지금 할 수 있는 것 세 가지입니다.
- 최근에 만들었거나 사용한 API 하나를 위 질문 6개로 점검해보세요. 하나라도 막히는 질문이 있다면 그것이 그 API의 약한 고리입니다.
- 회원 API 명세를 직접 1페이지로 작성해보세요 — 메서드+URL, 요청, 성공 응답, 오류 응답, 인증 여부. 이 글의 표를 참고하면 됩니다.
- 손으로 확인하고 싶다면 실습편(REST API 설계, URL부터 막힌다면: 초보자가 꼭 알아야 할 7가지 원칙)에서 이 글의 상태 코드들이 실제로 동작하는 서버를 처음부터 만들어봅니다.
참고 자료·검증 환경
- RFC 9110 — HTTP Semantics (메서드·상태 코드의 표준 의미): https://www.rfc-editor.org/rfc/rfc9110
- RFC 5789 — PATCH Method for HTTP: https://www.rfc-editor.org/rfc/rfc5789
- RFC 9457 — Problem Details for HTTP APIs: https://www.rfc-editor.org/rfc/rfc9457
- Roy Fielding — Architectural Styles and the Design of Network-based Software Architectures (REST 원 논문): https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
- OpenAPI Specification: https://spec.openapis.org/oas/latest.html
- MDN — HTTP 상태 코드: https://developer.mozilla.org/ko/docs/Web/HTTP/Status
이 글의 모든 요청·응답은 2026-08-14에 Node.js v22.22.2, Express 5.2.1 환경에서 실제 실행해 확인했습니다.