DB 어휘 정리 에 이어지는 용어 정리 시리즈 3탄. 저번엔 DB 를 통해 테이블이 어떻게 만들어지는지 봤다. 이번 글의 API 는 그 테이블을 앱 · 브라우저 같은 외부에 어떻게 노출하는지 를 다룬다.
📍 핵심 개념(REST / 상태 코드 / 멱등성 / 페이지네이션 / 인증/인가) 은 글 끝의 playground 에서 직접 만져볼 수 있게 따로 만들어뒀다.
Table of contents
Open Table of contents
API 설계의 5가지 질문
API 를 설계할 때 우리가 답해야 하는 질문은 사실 5개 + 1개로 정리된다.
| 질문 | 어휘 | |
|---|---|---|
| 1 | 자원을 어떤 모양으로 노출하나 | REST |
| 2 | 결과를 어떻게 알려주나 | 상태 코드 |
| 3 | 같은 요청 또 와도 안전한가 | 멱등성 |
| 4 | 많은 데이터를 어떻게 나눠 주나 | 페이지네이션 |
| 5 | 누가 뭘 할 수 있나 | 인증/인가 |
| + | 브라우저가 다른 도메인 요청 막을 때 | CORS (보너스) |
하나씩 정리하면서 어디서 막막함이 풀리는지 따라가본다.
1. REST
REST API 를 한 줄로 정의 하면:
리소스를 URL 로 가리키고, 그 리소스에 할 행위는 HTTP 메서드로 표현하는 것.
말은 어려운데 사용 예시를 보면 쉽다.
| HTTP 메서드 + URL | 의미 |
|---|---|
GET /orders | 주문 목록 |
GET /orders/42 | 주문 1개 조회 |
POST /orders | 주문 생성 |
PATCH /orders/42 | 주문 수정 |
DELETE /orders/42 | 주문 삭제 |
자원의 이름 (/orders/42) 은 명사, 행위는 HTTP 메서드 (GET, POST…) 로 분리하는 게 핵심.
Stateless — 서버가 상태를 기억하지 않는다
REST 의 또 다른 특징은 stateless.
- 서버는 직전 요청을 기억하지 않는다.
- 매 요청이 독립적 으로 처리된다.
- 그래서 “누구인지” 를 매번 토큰으로 알려줘야 한다.
이게 뒤에 나올 인증/인가 와 연결되는 지점.
Supabase 의 경우 — PostgREST 가 자동 생성
Supabase 는 PostgREST 라는 도구를 써서 테이블을 만든 순간 자동으로 REST API 가 생긴다.
테이블 orders 를 만들면 /rest/v1/orders 같은 주소가 자동으로 열린다. 직접 그 URL 에 요청 보내기는 번거로우니까 SDK 를 제공한다.
const { data, error } = await supabase
.from('orders')
.select('*')
.eq('user_id', userId);
이게 내부적으론 GET /rest/v1/orders?user_id=eq.<id> 같은 REST 호출.
문제는 여러 단계가 있을 때. 서버에서 돌아야 하는 복잡하고 민감한 로직 은 자동 REST 만으로 안 된다. 직접 함수를 짜야 한다. 이게 Edge Function 이다.
Supabase Edge Function 은 이 글 에서 따로 정리했었다.
2. 상태 코드 (Status Code)
요청이 어떻게 됐는지 알려주는 3자리 숫자. 어제 했던 에러 핸들링 글 의 재시도 로직에서도 이런 숫자들이 나왔다. 대역마다 의미가 정해져 있다.
| 대역 | 의미 | 대표 코드 |
|---|---|---|
| 2xx | 성공 | 200 OK, 201 Created, 204 No Content |
| 4xx | 클라이언트 잘못 | 400 요청 형식 오류, 401 미인증, 403 권한 없음, 404 없음, 409 충돌 |
| 5xx | 서버 잘못 | 500 서버 에러, 503 서비스 불가 |
핵심:
- 4xx 가 떴을 때는 클라이언트 (호출하는 쪽) 가 뭔가 잘못한 것 — 재시도 의미 없음, 호출 코드를 고쳐야 함
- 5xx 가 떴을 때는 서버 잘못 — 일시적이면 재시도하면 풀림
3. 멱등성 (Idempotency)
같은 요청을 여러 번 보내도, 한 번 보낸 것과 결과가 같은 성질.
수학에서 온 용어인데 API 설계의 핵심 개념. HTTP 메서드마다 멱등성이 정해져 있다.
| 메서드 | 멱등? | 이유 |
|---|---|---|
GET | ✅ | 읽기만 함 |
PUT | ✅ | 전체 교체 — 같은 내용으로 100번 교체 = 1번 교체 |
DELETE | ✅ | 한 번 지우면 끝, 또 보내도 이미 없음 |
POST | ❌ | 호출마다 새로 생성 — 주문 2번 = 주문 2개 |
왜 멱등성이 중요한가
클라이언트가 요청 보냈는데 응답을 못 받으면, 재시도 한다. (네트워크 일시 끊김 같은 경우.) 그런데 POST 를 재시도하면?
- 주문이 2번 들어가고
- 결제가 2번 발생하는
불상사 가 생긴다.
멱등성 키 (Idempotency-Key)
그래서 POST 같은 비멱등 작업 엔 멱등성 키 를 붙인다.
- 클라이언트가 고유 키 를 같이 보냄 (예: UUID)
- 서버가 그 키를 본 적 있으면 재실행하지 않고, 이전 결과를 그대로 반환
Stripe, Toss 같은 결제 API 가 이걸 표준으로 강제한다. “이 요청을 두 번 보내면 사고가 나나?” 라고 자문해서 답이 “예” 면 멱등성 보장이 필요하다.
4. 페이지네이션 (Pagination)
데이터를 한 번에 다 주지 않고 조각 (페이지) 으로 나눠서 주는 것.
1만 개 데이터를 한 번에 받으면 클라이언트도 서버도 부담. 그래서 나눠 준다. 두 가지 방식이 있다.
Offset 방식 — 직관적이지만 함정 있음
GET /orders?limit=20&offset=40
“40번째부터 20개 줘” 라는 명령. 직관적이지만:
- 뒤로 갈수록 느림 — 페이지 100 이면 DB 가 앞 1980 개를 건너뛰고 와야 함
- 데이터가 추가/삭제되면 밀려서 중복 / 누락 발생 가능 — 1페이지 보고 다음 페이지 가는 사이에 누가 새 주문 넣으면 같은 항목을 두 번 보게 됨
Cursor 방식 — 빠르고 안정적이지만 점프 불가
GET /orders?limit=20&cursor=order_42_at_2026-06-18T13:00:00Z
“이 커서 뒤에서부터 20개 줘”. 이전 페이지의 마지막 ID/시각을 다음 요청에 전달.
- 빠름 — DB 가 인덱스로 바로 시작 지점을 찾음
- 데이터가 변해도 안정적 — 커서 기준점이 고정이라 새 데이터가 들어와도 안 밀림
- 단점: 임의 페이지로 점프 불가 (“100페이지로” 같은 게 안 됨)
무한 스크롤 (Twitter, Instagram 같은) 은 거의 다 cursor 방식. 페이지 번호 UI (1, 2, 3…) 는 offset 방식.
5. 인증/인가 (Authentication / Authorization)
자주 헷갈리는데 다른 개념 이다.
| 인증 (Authentication) | 인가 (Authorization) | |
|---|---|---|
| 영어 약자 | AuthN | AuthZ |
| 질문 | ”누구?" | "뭘 할 수 있어?” |
| 검증 대상 | 신원 | 권한 |
| 실패 시 상태 코드 | 401 Unauthorized | 403 Forbidden |
인증 — JWT 토큰 흐름
REST 가 stateless 라 매 요청에 누구인지 알려줘야 한다.
- 로그인 → 서버가 JWT 토큰 발급
- 이후 매 요청의 헤더 에 토큰 첨부
Authorization: Bearer eyJhbGciOiJI... - 서버가 토큰을 검증해서 “누구” 인지 확인
인가 — 소유권 / 역할
- 소유권 기반: “이 주문은 user_42 의 것이니까 user_42 만 수정 가능”
- 역할 기반 (RBAC): “admin 역할이면 모든 주문 조회 가능”
상태 코드와의 매칭이 중요:
- 401 — 토큰이 없거나 잘못됨 = 인증 실패
- 403 — 토큰은 유효한데 권한 없음 = 인가 실패
보너스. CORS (Cross-Origin Resource Sharing)
브라우저는 보안상 다른 출처 (origin) 의 요청을 막는다. 이를 Same-Origin Policy 라고 한다.
출처 =
프로토콜 + 도메인 + 포트. 하나라도 다르면 “다른 출처”.
근데 그럼 https://my-app.com 에서 https://api.my-app.com 호출 못 한다. 도메인이 다르니까. 그래서 등장한 게 CORS — 안전한 경우엔 출처가 달라도 허용해주는 기술.
누가 허용하는가 — 서버
CORS 허용은 서버가 응답 헤더로 알려주는 일 이다.
Access-Control-Allow-Origin: https://my-app.com
브라우저는 응답에 이 헤더가 있는지 확인하고, 있으면 클라이언트 코드에 응답을 넘긴다. 없으면 차단.
Preflight — 미리 물어보는 요청
위험할 수 있는 요청 (예: 본문이 있는 POST, 커스텀 헤더 등) 은 본 요청 전에 미리 OPTIONS 요청 으로 “이거 보내도 돼?” 라고 묻는다. 서버가 OK 하면 그때 본 요청을 보낸다.
이걸 preflight 라고 한다. 개발할 때 “왜 OPTIONS 요청이 두 개씩 가지?” 하는 게 이 패턴.
회고
DB → API 흐름으로 오니까 “어떻게 자원을 정의하고, 어떻게 외부에 노출할 것인가” 라는 큰 그림이 잡힌다. 이전엔 API 라고 하면 “함수 호출 같은 것” 정도로 두루뭉술했는데, 이제는:
- 자원을 URL 로 가리키고 (REST)
- 결과를 숫자로 알려주고 (상태 코드)
- 같은 요청이 두 번 와도 안전하게 만들고 (멱등성)
- 데이터를 잘라서 주고 (페이지네이션)
- 누구인지 / 뭘 할 수 있는지 확인하고 (인증/인가)
- 브라우저의 보안 정책을 통과시키는 것 (CORS)
이 6가지가 한 줄기로 엮여있다 는 게 보인다.
AI 에게 “API 만들어줘” 라고 했을 때 결과물을 검토할 때도 위 6가지를 체크리스트로 쓸 수 있을 듯. 어휘가 잡히면 검수 능력이 같이 잡힌다.
더 공부해볼 것
1. GraphQL — REST 외의 다른 길
- REST 가 “리소스 중심” 이라면 GraphQL 은 “쿼리 중심”
- 클라이언트가 필요한 필드만 골라서 받음 (오버페치 / 언더페치 해결)
- 단점: 캐싱이 어렵고, 학습 곡선 가파름
- 언제 REST vs GraphQL 을 쓰는가의 기준
- GraphQL 공식 사이트
2. tRPC — TypeScript 진영의 답
- 풀스택 TypeScript 환경에서 REST/GraphQL 대신 쓰는 RPC
- 클라이언트가 서버 함수를 타입 안전하게 호출 (스키마 정의 없이 type inference)
- Next.js 와 자주 같이 씀
- tRPC 공식
3. OpenAPI / Swagger — API 문서화 표준
- API 의 모든 엔드포인트 / 요청 / 응답을 스키마로 정의
- 스키마 → 자동 문서, 자동 클라이언트 SDK 생성
- 팀 작업에서 사실상 표준
- OpenAPI Specification
4. JWT 의 약점 + 대안
- JWT 는 한 번 발급하면 만료까지 무효화 불가 (탈취 시 위험)
- 짧은 access token + refresh token 패턴
- 세션 기반 인증 의 부활 (Lucia, Auth.js 등)
- HttpOnly Cookie vs Bearer Header 의 trade-off
5. Rate Limiting — DoS 와 비용 방어
- 한 클라이언트가 너무 많이 호출하면 차단
- Token Bucket / Leaky Bucket / Sliding Window 알고리즘
- 상태 코드 429 Too Many Requests 와
Retry-After헤더 - API 게이트웨이 (Kong, Tyk, AWS API Gateway) 에서 흔히 구현