리소스 설계
REST 리소스 패턴
클라이언트와 서버 간의 REST 통신 흐름
┌─────────────────────────────────────────────────────────────────┐
│ REST 요청/응답 패턴 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Client Server │
│ ┌──────────┐ ┌──────────┐ │
│ │ HTTP 메소드│ │ 처리 │ │
│ │ MIME 타입 │ ─────── Request ──────────► │ │ │
│ │ 타깃 URI │ │ │ │
│ │ │ │ │ │
│ │ │ ◄─────── Response ───────── │ │ │
│ │ 응답 코드 │ │ HTTP 코드 │ │
│ │ 표현형 │ │ MIME 타입 │ │
│ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘콘텐츠 협상 (Content Negotiation)
동일한 URI의 리소스를 여러 가지 표현형으로 제공하여 클라이언트가 원하는 형식을 선택할 수 있게 하는 메커니즘
┌─────────────────────────────────────────────────────────────────┐
│ 콘텐츠 협상 방식 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ HTTP 헤더 방식 │ │ URL 패턴 방식 │ │
│ ├─────────────────────┤ ├─────────────────────┤ │
│ │ Accept: app/json │ │ /books.json │ │
│ │ Accept: app/xml │ │ /books.xml │ │
│ │ Accept-Language: ko│ │ /books.html │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ ↓ ↓ │
│ 표준화된 방식 URL만 보고 구분 가능 │
│ │
└─────────────────────────────────────────────────────────────────┘HTTP 헤더를 이용한 콘텐츠 협상
# 요청 (클라이언트 → 서버)
GET /api/v1/books/123 HTTP/1.1
Host: api.example.com
Accept: application/json # 원하는 응답 형식
Accept-Language: ko-KR # 원하는 언어
# 응답 (서버 → 클라이언트)
HTTP/1.1 200 OK
Content-Type: application/json # 실제 응답 형식
{
"id": 123,
"title": "REST API 설계"
}주요 HTTP 헤더
| 헤더 | 방향 | 설명 |
|---|---|---|
Accept | 요청 | 클라이언트가 처리 가능한 MIME 타입과 우선순위 |
Accept-Language | 요청 | 원하는 언어 |
Content-Type | 응답 | 서버가 보낸 엔티티의 MIME 타입 |
주요 MIME 타입
┌─────────────────────────────────────────────────────────────────┐
│ 자주 사용되는 MIME 타입 │
├─────────────────────────────────────────────────────────────────┤
│ application/json │ JSON 데이터 (가장 많이 사용) │
│ application/xml │ XML 데이터 │
│ text/html │ HTML 문서 │
│ text/plain │ 일반 텍스트 │
│ image/jpeg │ JPEG 이미지 │
│ image/png │ PNG 이미지 │
│ multipart/form-data │ 파일 업로드 │
└─────────────────────────────────────────────────────────────────┘URL 패턴을 이용한 콘텐츠 협상
# JSON 형식 요청
GET /api/v2/library/books.json HTTP/1.1
# XML 형식 요청
GET /api/v2/library/books.xml HTTP/1.1참고: URL 패턴 방식은 서버에서 각 확장자별로 별도 메소드를 준비해야 한다.
POJO 기반 JSON 바인딩
자바 객체를 JSON으로 매핑하는 표준 방식. Jackson 라이브러리가 담당한다.
┌─────────────────────────────────────────────────────────────────┐
│ Jackson 매핑 흐름 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Java Object JSON String │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Coffee │ │ { │ │
│ │ - name │ ═══ serialize ═══► │ "name":.. │ │
│ │ - price │ │ "price":.. │ │
│ │ - origin │ ◄══ deserialize ══ │ "origin":..│ │
│ └──────────────┘ │ } │ │
│ └──────────────┘ │
│ │
│ ObjectMapper가 변환 담당 │
│ │
└─────────────────────────────────────────────────────────────────┘Jackson 사용 예제
// 의존성: org.fasterxml.jackson
ObjectMapper objectMapper = new ObjectMapper();
// JSON → Java Object (역직렬화)
String jsonData = "{\"name\":\"Espresso\",\"price\":4500}";
Coffee coffee = objectMapper.readValue(jsonData, Coffee.class);
// Java Object → JSON (직렬화)
Coffee coffee = new Coffee("Latte", 5000);
String json = objectMapper.writeValueAsString(coffee);
// 결과: {"name":"Latte","price":5000}API 버저닝
애플리케이션이 점진적으로 발전할 수 있도록 URI 설계 단계부터 버전을 명확히 구별하는 방법
┌─────────────────────────────────────────────────────────────────┐
│ API 버저닝의 필요성 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ v1.0 v2.0 v3.0 │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ API │ ────────► │ API │ ────────► │ API │ │
│ └─────┘ └─────┘ └─────┘ │
│ │ │ │ │
│ │ 기능 추가 │ 구조 변경 │ │
│ │ 필드 변경 │ 폐기 API │ │
│ ▼ ▼ ▼ │
│ 기존 클라이언트 신규 클라이언트 최신 클라이언트 │
│ (v1 사용) (v2 사용) (v3 사용) │
│ │
│ → 각 버전 클라이언트가 영향 없이 동작해야 함 │
│ │
└─────────────────────────────────────────────────────────────────┘버전 지정 방법 비교
| 방법 | 예시 | 장점 | 단점 |
|---|---|---|---|
| URI에 지정 | /v2/coffees/123 | 명확함, 캐시 가능 | URL이 길어짐 |
| 쿼리 파라미터 | /coffees?version=v2 | 구현 간단 | 캐시 불가 |
| Accept 헤더 | Accept: app/vnd.foo-v2+json | URL 깔끔 | 테스트 어려움 |
1. URI에 버전 지정 (권장)
# 명시적 버전 지정
GET /api/v2/coffees/1234 HTTP/1.1
# 버전 없으면 최신 버전으로
GET /api/coffees/1234 HTTP/1.1
→ 내부적으로 /api/v2/coffees/1234 처리┌─────────────────────────────────────────────────────────────────┐
│ 구버전 API 접근 처리 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Client Server │
│ │ │ │
│ │─── GET /api/v1/coffees ──────────────►│ │
│ │ │ │
│ │◄── 301 Moved Permanently ─────────────│ │
│ │ Location: /api/v2/coffees │ │
│ │ │ │
│ │─── GET /api/v2/coffees ──────────────►│ │
│ │ │ │
│ │◄── 200 OK ────────────────────────────│ │
│ │
└─────────────────────────────────────────────────────────────────┘리다이렉션 응답 코드
| 코드 | 의미 | 용도 |
|---|---|---|
| 301 Moved Permanently | 영구 이동 | 구버전 API가 새 버전으로 완전히 대체됨 |
| 302 Found | 임시 이동 | URI는 유효하지만 리소스가 일시적으로 다른 곳에 있음 |
2. 쿼리 파라미터에 버전 지정
GET /api/coffees/1234?version=v2 HTTP/1.1단점: 응답이 캐시되지 않음
3. Accept 헤더에 버전 지정
GET /api/coffees/1234 HTTP/1.1
Accept: application/vnd.foo-v2+jsonvnd: vendor-specific (벤더 고유) MIME 타입- GitHub API 등에서 사용하는 방식
HTTP 응답 코드
REST API에서 사용하는 표준 HTTP 응답 코드
┌─────────────────────────────────────────────────────────────────┐
│ HTTP 응답 코드 분류 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 2XX 성공 3XX 리다이렉션 4XX 클라이언트 에러 │
│ ┌─────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ 200 OK │ │ 301 Moved │ │ 400 Bad Request │ │
│ │ 201 Cre │ │ 302 Found │ │ 401 Unauthorized│ │
│ │ 204 No │ │ 304 Not Mod │ │ 404 Not Found │ │
│ │ 202 Acc │ └─────────────┘ │ 406 Not Accept │ │
│ └─────────┘ └─────────────────┘ │
│ │
│ 5XX 서버 에러 │
│ ┌─────────────────┐ │
│ │ 500 Internal │ │
│ │ 503 Unavailable │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘성공 응답 (2XX)
| 코드 | 이름 | 설명 | 사용 예시 |
|---|---|---|---|
| 200 | OK | 요청 성공, 데이터 포함 | GET, PUT, DELETE 성공 |
| 201 | Created | 리소스 생성됨 | POST 성공, Location 헤더 필수 |
| 202 | Accepted | 비동기 처리 수락 | 오래 걸리는 작업 요청 |
| 204 | No Content | 성공, 응답 본문 없음 | DELETE 성공 (데이터 불필요) |
# 201 Created 예시
POST /api/v1/users HTTP/1.1
Content-Type: application/json
{"name": "John", "email": "john@example.com"}
---
HTTP/1.1 201 Created
Location: /api/v1/users/456 # 생성된 리소스 위치
Content-Type: application/json
{"id": 456, "name": "John", "email": "john@example.com"}# 202 Accepted 예시 (비동기 처리)
POST /api/v1/reports/generate HTTP/1.1
---
HTTP/1.1 202 Accepted
Location: /api/v1/reports/jobs/789 # 작업 상태 조회 URL
{"message": "Report generation started", "jobId": 789}리다이렉션 응답 (3XX)
| 코드 | 이름 | 설명 |
|---|---|---|
| 301 | Moved Permanently | 리소스가 영구적으로 다른 URI로 이동 |
| 302 | Found | 리소스가 임시로 다른 위치에 있음 |
클라이언트 에러 (4XX)
| 코드 | 이름 | 설명 |
|---|---|---|
| 400 | Bad Request | 잘못된 요청 구문, 유효하지 않은 데이터 |
| 401 | Unauthorized | 인증 필요 (로그인 안 됨) |
| 403 | Forbidden | 권한 없음 (인증은 됐지만 접근 불가) |
| 404 | Not Found | 리소스를 찾을 수 없음 |
| 406 | Not Acceptable | 클라이언트가 요청한 MIME 타입으로 응답 불가 |
| 415 | Unsupported Media Type | 지원하지 않는 미디어 타입 |
┌─────────────────────────────────────────────────────────────────┐
│ 401 vs 403 차이점 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 401 Unauthorized 403 Forbidden │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ "누구세요?" │ │ "신분은 확인됐지만 │ │
│ │ │ │ 여기 들어올 권한 │ │
│ │ 인증 토큰 없음 │ │ 없어요" │ │
│ │ 토큰 만료 │ │ │ │
│ │ 토큰 유효하지 않음 │ │ 관리자 페이지 접근 │ │
│ └──────────────────────┘ │ 다른 유저 데이터 접근│ │
│ └──────────────────────┘ │
│ │
│ 해결: 로그인/재로그인 해결: 권한 상승 필요 │
│ │
└─────────────────────────────────────────────────────────────────┘서버 에러 (5XX)
| 코드 | 이름 | 설명 |
|---|---|---|
| 500 | Internal Server Error | 서버 내부 에러 (일반적) |
| 503 | Service Unavailable | 서버 점검 중 또는 과부하 |
응답 코드 선택 가이드
┌─────────────────────────────────────────────────────────────────┐
│ HTTP 메소드별 응답 코드 │
├──────────┬──────────────────────────────────────────────────────┤
│ 메소드 │ 응답 코드 │
├──────────┼──────────────────────────────────────────────────────┤
│ GET │ 200 OK (성공) / 404 Not Found (없음) │
│ │ │
│ POST │ 201 Created (생성 성공, Location 헤더 포함) │
│ │ 202 Accepted (비동기 처리) │
│ │ 400 Bad Request (잘못된 데이터) │
│ │ │
│ PUT │ 200 OK (수정 성공, 데이터 포함) │
│ │ 201 Created (새로 생성됨) │
│ │ 204 No Content (성공, 본문 없음) │
│ │ │
│ PATCH │ 200 OK (수정 성공) │
│ │ 204 No Content (성공, 본문 없음) │
│ │ │
│ DELETE │ 200 OK (삭제 성공, 데이터 포함) │
│ │ 204 No Content (삭제 성공, 본문 없음) │
│ │ 404 Not Found (이미 없음) │
└──────────┴──────────────────────────────────────────────────────┘핵심 용어 정리
| 용어 | 설명 |
|---|---|
| 콘텐츠 협상 | 클라이언트가 원하는 표현형(JSON, XML 등)을 선택하는 메커니즘 |
| 페이로드 | 헤더나 체크섬을 제외한 실제 전송 목적 데이터 |
| 퍼머링크 | permanent + link, 리소스의 영구 불변 URL |
| 크리덴셜 | 인증에 사용되는 암호학적 개인 정보 (토큰, 인증서 등) |
| MIME 타입 | 콘텐츠의 형식을 나타내는 표준 (application/json 등) |
| vnd | vendor-specific, 벤더 고유 MIME 타입 접두사 |