리소스 설계

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+jsonURL 깔끔테스트 어려움

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+json
  • vnd: 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)

코드이름설명사용 예시
200OK요청 성공, 데이터 포함GET, PUT, DELETE 성공
201Created리소스 생성됨POST 성공, Location 헤더 필수
202Accepted비동기 처리 수락오래 걸리는 작업 요청
204No 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)

코드이름설명
301Moved Permanently리소스가 영구적으로 다른 URI로 이동
302Found리소스가 임시로 다른 위치에 있음

클라이언트 에러 (4XX)

코드이름설명
400Bad Request잘못된 요청 구문, 유효하지 않은 데이터
401Unauthorized인증 필요 (로그인 안 됨)
403Forbidden권한 없음 (인증은 됐지만 접근 불가)
404Not Found리소스를 찾을 수 없음
406Not Acceptable클라이언트가 요청한 MIME 타입으로 응답 불가
415Unsupported Media Type지원하지 않는 미디어 타입
┌─────────────────────────────────────────────────────────────────┐
│                   401 vs 403 차이점                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   401 Unauthorized                   403 Forbidden              │
│  ┌──────────────────────┐          ┌──────────────────────┐    │
│  │ "누구세요?"           │          │ "신분은 확인됐지만   │    │
│  │                      │          │  여기 들어올 권한    │    │
│  │ 인증 토큰 없음        │          │  없어요"            │    │
│  │ 토큰 만료            │          │                      │    │
│  │ 토큰 유효하지 않음    │          │ 관리자 페이지 접근   │    │
│  └──────────────────────┘          │ 다른 유저 데이터 접근│    │
│                                    └──────────────────────┘    │
│                                                                 │
│  해결: 로그인/재로그인             해결: 권한 상승 필요         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

서버 에러 (5XX)

코드이름설명
500Internal Server Error서버 내부 에러 (일반적)
503Service 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 등)
vndvendor-specific, 벤더 고유 MIME 타입 접두사