REST API 설계가 중요한 이유
API는 서비스의 정문입니다. 방문자(클라이언트)가 건물(서버)에 들어올 때 안내 표지판이 명확해야 원하는 곳을 쉽게 찾을 수 있습니다. 잘 설계된 API는 문서를 읽지 않아도 사용법을 추측할 수 있어야 합니다.
REST(Representational State Transfer)는 HTTP 프로토콜을 활용한 아키텍처 스타일입니다. 핵심 원칙은 리소스 중심, HTTP 메서드 활용, **상태 없음(Stateless)**입니다.
URI 네이밍 규칙
기본 원칙
# 잘못된 설계 — 동사 사용, 일관성 없음
GET /getUsers
POST /createNewUser
GET /user/delete/123
POST /updateUserProfile
# 올바른 설계 — 명사, 복수형, 계층 구조
GET /users # 사용자 목록 조회
POST /users # 사용자 생성
GET /users/123 # 특정 사용자 조회
PUT /users/123 # 사용자 전체 수정
PATCH /users/123 # 사용자 부분 수정
DELETE /users/123 # 사용자 삭제
네이밍 체크리스트
| 규칙 | 올바른 예 | 잘못된 예 |
|---|
| 복수 명사 사용 | /users, /orders | /user, /order |
| 소문자 사용 | /user-profiles | /UserProfiles |
| 하이픈 사용 | /blog-posts | /blog_posts, /blogPosts |
| 동사 사용 금지 | POST /users | /createUser |
| 파일 확장자 금지 | /users/123 | /users/123.json |
| 계층 관계 표현 | /users/123/orders | /getUserOrders?id=123 |
관계형 리소스
# 사용자의 주문 목록
GET /users/123/orders
# 사용자의 특정 주문
GET /users/123/orders/456
# 주문의 결제 정보
GET /users/123/orders/456/payment
# 깊은 중첩은 피하기 (3단계 이상)
# 잘못됨: /users/123/orders/456/items/789/reviews
# 올바름: /order-items/789/reviews
HTTP 메서드와 상태 코드
메서드별 의미
| 메서드 | 용도 | 멱등성 | 안전성 | 요청 본문 |
|---|
| GET | 조회 | 예 | 예 | 없음 |
| POST | 생성 | 아니오 | 아니오 | 있음 |
| PUT | 전체 교체 | 예 | 아니오 | 있음 |
| PATCH | 부분 수정 | 아니오 | 아니오 | 있음 |
| DELETE | 삭제 | 예 | 아니오 | 없음/선택 |
상태 코드 가이드
# 성공 응답
200 OK # 일반 성공 (GET, PUT, PATCH, DELETE)
201 Created # 리소스 생성 성공 (POST)
204 No Content # 성공했지만 응답 본문 없음 (DELETE)
# 클라이언트 에러
400 Bad Request # 요청 데이터 유효성 검증 실패
401 Unauthorized # 인증 실패 (토큰 없음/만료)
403 Forbidden # 인가 실패 (권한 없음)
404 Not Found # 리소스 없음
409 Conflict # 충돌 (중복 데이터 등)
422 Unprocessable # 문법은 맞지만 의미적 오류
429 Too Many Requests # 요청 한도 초과 (Rate Limit)
# 서버 에러
500 Internal Error # 서버 내부 오류
502 Bad Gateway # 업스트림 서버 오류
503 Service Unavailable # 서비스 일시 중단
에러 응답 패턴
일관된 에러 응답 형식은 클라이언트 개발자의 생산성을 크게 높입니다.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "입력 데이터가 유효하지 않습니다.",
"details": [
{
"field": "email",
"message": "유효한 이메일 형식이 아닙니다.",
"value": "invalid-email"
},
{
"field": "age",
"message": "나이는 0 이상이어야 합니다.",
"value": -5
}
],
"timestamp": "2026-02-19T14:30:00Z",
"request_id": "req_abc123"
}
}
Express.js 에러 핸들링 구현
// 에러 응답 헬퍼 함수
class ApiError extends Error {
constructor(statusCode, code, message, details = []) {
super(message);
this.statusCode = statusCode;
this.code = code; // 에러 코드 (머신 리더블)
this.details = details; // 상세 정보 배열
}
}
// 유효성 검증 에러 생성
function validationError(details) {
return new ApiError(
400,
"VALIDATION_ERROR",
"입력 데이터가 유효하지 않습니다.",
details
);
}
// 글로벌 에러 핸들러
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
const response = {
error: {
code: err.code || "INTERNAL_ERROR",
message: err.message || "서버 내부 오류가 발생했습니다.",
details: err.details || [],
timestamp: new Date().toISOString(),
request_id: req.id // 요청 추적용 ID
}
};
// 500 에러는 상세 내용을 숨김 (보안)
if (statusCode === 500) {
console.error("서버 에러:", err);
response.error.message = "서버 내부 오류가 발생했습니다.";
response.error.details = [];
}
res.status(statusCode).json(response);
});
페이지네이션
# 커서 기반 (권장 — 대규모 데이터에 적합)
GET /users?cursor=eyJpZCI6MTAwfQ&limit=20
# 응답
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTIwfQ",
"has_more": true,
"limit": 20
}
}
# 오프셋 기반 (간단하지만 대규모에서 성능 저하)
GET /users?page=3&per_page=20
# 응답
{
"data": [...],
"pagination": {
"page": 3,
"per_page": 20,
"total": 150,
"total_pages": 8
}
}
| 방식 | 장점 | 단점 |
|---|
| 커서 기반 | 대규모 안정적, 실시간 데이터 | 총 페이지 수 모름, 임의 페이지 불가 |
| 오프셋 기반 | 구현 간단, 임의 페이지 가능 | 데이터 변경 시 중복/누락, 느림 |
버전 관리
# 방법 1: URL 경로 (가장 직관적, 권장)
GET /v1/users
GET /v2/users
# 방법 2: 쿼리 파라미터
GET /users?version=2
# 방법 3: 헤더
GET /users
Accept: application/vnd.myapi.v2+json
| 방식 | 장점 | 단점 |
|---|
| URL 경로 | 명확, 캐싱 용이 | URL 변경 |
| 쿼리 파라미터 | URL 구조 유지 | 기본값 혼란 |
| 헤더 | URL 깔끔 | 테스트 불편, 발견성 낮음 |
필터링, 정렬, 검색
# 필터링 — 쿼리 파라미터 사용
GET /users?status=active&role=admin
# 정렬 — sort 파라미터
GET /users?sort=-created_at,name
# -는 내림차순, 기본은 오름차순
# 검색 — q 또는 search 파라미터
GET /users?q=김철수
# 필드 선택 — fields 파라미터 (응답 크기 최적화)
GET /users?fields=id,name,email
# 조합 사용
GET /users?status=active&sort=-created_at&fields=id,name&limit=10
실전 팁
- 일관성이 가장 중요합니다: 네이밍, 응답 형식, 에러 구조를 프로젝트 전체에서 통일하세요. 일관성 없는 API는 문서가 아무리 좋아도 사용하기 어렵습니다.
- HATEOAS보다 실용성을 선택하세요: 이론적으로 완벽한 REST보다 개발자가 쉽게 사용할 수 있는 API가 좋은 API입니다.
- Rate Limiting을 초기부터 적용하세요:
429 응답과 Retry-After 헤더를 포함하세요. 나중에 추가하면 기존 클라이언트가 대응하기 어렵습니다.
- 요청 ID를 모든 응답에 포함하세요: 디버깅 시 클라이언트와 서버 로그를 연결하는 핵심 정보입니다.
- PUT과 PATCH를 구분하세요: PUT은 전체 교체(빠진 필드는 null), PATCH는 부분 수정(보낸 필드만 변경)입니다. 대부분의 수정 요청에는 PATCH가 적합합니다.
- OpenAPI(Swagger)로 문서화하세요: 코드에서 API 스펙을 자동 생성하면 문서와 실제 API 간의 불일치를 방지할 수 있습니다.