오늘날 디지털 세상은 보이지 않는 연결망으로 촘촘히 엮여 있습니다. 스마트폰 애플리케이션이 서버의 데이터를 가져와 보여주고, 온라인 쇼핑몰의 결제 시스템이 카드사와 통신하며, 기업의 내부 서비스들이 서로 정보를 교환하는 이 모든 과정의 중심에는 API(Application Programming Interface)가 있습니다. 특히, 웹 기반의 분산 시스템 환경에서 API를 설계하는 가장 지배적인 아키텍처 스타일로 자리 잡은 것이 바로 REST(Representational State Transfer)입니다. REST는 단순한 기술이나 규격이 아니라, 웹의 기존 기술과 프로토콜(주로 HTTP)을 최대한 활용하여 확장 가능하고 유연하며 유지보수가 용이한 시스템을 구축하기 위한 하나의 철학이자 설계 원칙들의 집합입니다.
잘 설계된 REST API는 단순히 기능을 제공하는 것을 넘어, 시스템의 논리적 구조를 명확하게 드러내고, 클라이언트와 서버 간의 의존성을 낮추며, 미래의 변화에 유연하게 대처할 수 있는 강력한 기반이 됩니다. 반면, 급하게 만들어진 일관성 없는 API는 기술 부채의 주범이 되어 시스템 전체의 발전을 저해하고 개발자들에게 끊임없는 혼란을 안겨줍니다. 따라서 현대적인 백엔드 개발자에게 REST API를 올바르게 이해하고 설계하는 능력은 선택이 아닌 필수 역량이라 할 수 있습니다. 이 글에서는 REST의 근본적인 철학부터 시작하여, 좋은 API를 구성하는 핵심적인 설계 원칙들과 실무에서 마주할 수 있는 다양한 고급 주제들까지 체계적으로 탐구해보고자 합니다.
1. REST 아키텍처의 본질과 철학
REST API 설계를 논하기에 앞서, 우리는 먼저 'REST'가 무엇인지 명확히 이해해야 합니다. REST는 2000년 로이 필딩(Roy T. Fielding)의 박사학위 논문에서 처음 소개된 아키텍처 스타일입니다. 그는 월드 와이드 웹(WWW)이 어떻게 엄청난 규모로 성장하고 성공할 수 있었는지를 분석하며 그 핵심 원리들을 정리했고, 이를 REST라는 이름으로 정립했습니다. 즉, REST는 웹의 창시자들이 의도했던 설계 원칙들을 따르는 시스템을 만드는 방법론입니다.
REST의 핵심은 **자원(Resource)**의 **표현(Representation)**을 **전송(Transfer)**하는 것입니다. 여기서 각 용어는 매우 중요한 의미를 담고 있습니다.
- 자원 (Resource): API가 다루는 모든 개념적 대상을 의미합니다. 예를 들어, '사용자 정보', '게시글', '상품 목록' 등이 모두 자원이 될 수 있습니다. 자원은 데이터베이스의 테이블이나 특정 객체와 일대일로 대응될 수도 있지만, 더 추상적인 개념일 수도 있습니다. 중요한 것은 각 자원이 고유한 식별자, 즉 URI(Uniform Resource Identifier)를 통해 식별될 수 있다는 점입니다. 예를 들어,
/users/123
은 '123번 ID를 가진 사용자'라는 자원을 가리키는 URI입니다. - 표현 (Representation): 자원의 특정 시점의 상태를 나타내는 데이터입니다. 클라이언트와 서버는 자원 그 자체를 주고받는 것이 아니라, 자원의 '표현'을 주고받습니다. 이 표현은 다양한 형식(Format)으로 나타낼 수 있는데, 현대 웹에서는 대부분 JSON(JavaScript Object Notation) 형식이 사용됩니다. 예를 들어,
/users/123
이라는 자원에 대한 JSON 표현은{"id": 123, "name": "홍길동", "email": "gildong@example.com"}
과 같은 형태일 것입니다. 클라이언트는 서버에 요청할 때 자신이 이해할 수 있는 표현 형식을 (예:Accept: application/json
헤더) 명시할 수 있습니다. - 전송 (Transfer): 클라이언트와 서버가 HTTP 프로토콜을 통해 자원의 표현을 주고받는 행위를 의미합니다. 클라이언트는 HTTP 메서드(GET, POST, PUT, DELETE 등)를 사용하여 서버에 특정 자원에 대한 작업을 요청하고, 서버는 그에 대한 응답으로 해당 자원의 표현과 상태 코드(Status Code)를 반환합니다.
이러한 기본 개념 위에, 로이 필딩은 REST 아키텍처가 반드시 따라야 할 6가지 제약 조건(Architectural Constraints)을 정의했습니다. 이 조건들을 충족해야 진정한 의미의 'RESTful' 시스템이라고 할 수 있습니다.
1.1. 클라이언트-서버 (Client-Server) 구조
REST는 클라이언트와 서버의 역할을 명확하게 분리하는 것을 전제로 합니다. 클라이언트는 사용자 인터페이스(UI)와 사용자 경험(UX)에 집중하고, 서버는 데이터 저장, 비즈니스 로직 처리, 인증 등 백엔드 로직에 집중합니다. 이 둘 사이의 통신은 오직 HTTP 요청과 응답을 통해서만 이루어집니다. 이러한 분리 덕분에 클라이언트와 서버는 서로 독립적으로 개발되고 발전할 수 있습니다. 예를 들어, 서버 API가 동일하게 유지되는 한, 웹 프론트엔드(React, Vue 등)와 모바일 앱(iOS, Android)은 같은 서버를 공유하면서 각 플랫폼에 최적화된 형태로 개발될 수 있습니다.
1.2. 무상태성 (Stateless)
무상태성은 REST의 가장 중요한 특징 중 하나입니다. 서버는 클라이언트의 상태를 저장하지 않아야 합니다. 즉, 클라이언트가 보내는 각각의 요청은 그 자체로 완전한 정보를 담고 있어야 하며, 서버가 해당 요청을 이해하고 처리하는 데 필요한 모든 컨텍스트를 포함해야 합니다. 이전 요청의 내용이 다음 요청 처리에 영향을 주어서는 안 됩니다. 예를 들어, 사용자가 로그인을 했다는 상태 정보(세션)를 서버에 저장하는 대신, 클라이언트는 매 요청마다 자신이 누구인지를 증명하는 정보(예: JWT 토큰)를 요청 헤더에 포함시켜 보내야 합니다. 이러한 무상태성은 서버의 구현을 단순화시키고, 특정 서버에 대한 의존성을 없애주므로 시스템의 확장성(Scalability)과 신뢰성(Reliability)을 크게 향상시킵니다.
1.3. 캐시 가능성 (Cacheable)
웹의 성능을 향상시키는 핵심 요소인 캐싱을 REST 아키텍처에서도 적극적으로 활용해야 합니다. 서버는 HTTP 응답에 캐싱 관련 헤더(Cache-Control
, Expires
, ETag
등)를 포함하여, 해당 응답이 클라이언트나 중간 프록시 서버에 캐시될 수 있는지 여부와 유효 기간을 명시해야 합니다. 클라이언트는 캐시된 데이터를 재사용함으로써 불필요한 서버 요청을 줄이고, 이는 전체 시스템의 응답 속도를 개선하고 서버의 부하를 감소시키는 효과를 가져옵니다. 예를 들어, 자주 변경되지 않는 상품 카테고리 목록 같은 데이터는 캐시를 통해 효율적으로 제공될 수 있습니다.
1.4. 계층화 시스템 (Layered System)
클라이언트는 자신이 직접 통신하는 서버가 최종 목적지인지, 아니면 중간에 로드 밸런서, 캐시 서버, 보안 게이트웨이 등 다른 여러 계층을 거치고 있는지 알 필요가 없습니다. 시스템은 여러 계층으로 구성될 수 있으며, 각 계층은 특정 역할(보안, 부하 분산 등)을 수행합니다. 이러한 계층 구조는 시스템의 복잡도를 낮추고, 각 구성 요소의 독립적인 관리를 가능하게 하여 유지보수성과 확장성을 높여줍니다.
1.5. 균일한 인터페이스 (Uniform Interface)
균일한 인터페이스는 REST를 다른 아키텍처와 구별 짓는 핵심적인 제약 조건으로, 시스템 전체의 아키텍처를 단순화하고 구성 요소의 독립적인 진화를 가능하게 합니다. 이는 다시 네 가지 하위 제약 조건으로 나뉩니다.
- 자원의 식별 (Identification of resources): 위에서 설명했듯이, 모든 자원은 URI를 통해 고유하게 식별되어야 합니다.
- 표현을 통한 자원 조작 (Manipulation of resources through representations): 클라이언트는 자원의 표현과 필요한 메타데이터를 가지고 있으며, 이를 통해 서버에 있는 자원의 상태를 변경(생성, 수정, 삭제)할 수 있습니다. 예를 들어, 클라이언트는 사용자 정보의 JSON 표현을 서버에 보내 사용자의 프로필을 업데이트할 수 있습니다.
- 자기 서술적 메시지 (Self-descriptive messages): 각 메시지는 그 자체로 자신을 설명할 수 있어야 합니다. 클라이언트는 서버로부터 받은 JSON 데이터의 구조만 보고 '이것이 사용자 데이터구나'라고 추측하는 것이 아니라,
Content-Type: application/json
같은 헤더를 통해 메시지의 형식을 명확히 이해해야 합니다. 또한, 메시지 본문 내에 어떤 필드가 어떤 의미를 가지는지 잘 정의되어야 합니다. - 애플리케이션의 상태 엔진으로서의 하이퍼미디어 (HATEOAS: Hypermedia as the Engine of Application State): 이는 가장 중요하면서도 종종 간과되는 원칙입니다. 서버는 단순히 데이터만 응답하는 것이 아니라, 클라이언트가 다음에 할 수 있는 행동들에 대한 링크(URL)를 함께 제공해야 합니다. 예를 들어, 특정 주문 정보를 조회하는 API 응답에는 그 주문을 '취소'하거나 '배송 조회'를 할 수 있는 API의 URL을 포함시켜야 합니다. 이를 통해 클라이언트는 API의 전체 구조를 미리 알 필요 없이, 서버가 제공하는 링크를 따라가며 애플리케이션의 상태를 전이시킬 수 있게 됩니다. 이는 클라이언트와 서버 간의 결합도를 획기적으로 낮춰줍니다.
1.6. 주문형 코드 (Code-On-Demand) - 선택 사항
이는 유일하게 선택적인 제약 조건으로, 서버가 클라이언트에 실행 가능한 코드(예: JavaScript)를 전송하여 클라이언트의 기능을 일시적으로 확장할 수 있음을 의미합니다. 현대 웹의 프론트엔드 프레임워크들이 바로 이 원칙을 활용하는 대표적인 예시입니다.
이러한 REST의 철학과 제약 조건들은 이어지는 구체적인 설계 원칙들의 이론적 기반이 됩니다. 왜 URI는 명사로 설계해야 하는지, 왜 HTTP 메서드를 의미에 맞게 사용해야 하는지에 대한 '이유'가 바로 이 제약 조건들 속에 담겨 있습니다.
2. 자원(Resource) 중심의 URI 설계 원칙
REST API 설계의 첫걸음은 애플리케이션이 다루는 '자원'이 무엇인지 정의하고, 이를 어떻게 URI로 표현할지 결정하는 것입니다. URI는 API의 직관성과 사용성을 결정하는 가장 중요한 요소 중 하나이며, 잘 설계된 URI는 그 자체로 API의 구조를 설명하는 문서 역할을 합니다.
2.1. 동사가 아닌 명사를 사용하라
가장 기본적이고 중요한 원칙입니다. URI는 '자원'을 식별하기 위한 것이지, '행위'를 나타내기 위한 것이 아닙니다. 자원에 대한 행위는 HTTP 메서드(GET, POST, PUT, DELETE 등)가 표현해야 합니다. 많은 초보 개발자들이 RPC(Remote Procedure Call) 스타일로 API를 설계하는 실수를 저지릅니다.
- 나쁜 예 (RPC 스타일):
/getUsers
/createNewUser
/updateUserById/123
/deleteUser/123
- 좋은 예 (REST 스타일):
/users
(사용자 목록이라는 자원)/users/123
(ID가 123인 특정 사용자라는 자원)
좋은 예에서 /users
라는 URI는 '모든 사용자'라는 자원의 집합(Collection)을 나타냅니다. 여기에 GET
메서드를 사용하면 사용자 목록을 조회하고, POST
메서드를 사용하면 새로운 사용자를 생성하는 행위를 표현하게 됩니다. 즉, '행위'는 URI에서 분리되어 HTTP 메서드로 위임됩니다.
2.2. 자원의 계층 구조를 표현하라
자원들은 종종 다른 자원과 관계를 맺습니다. URI는 슬래시(/
)를 사용하여 이러한 계층적 관계를 직관적으로 표현할 수 있습니다. 예를 들어, 특정 사용자가 작성한 게시글 목록을 나타내고 싶다면 다음과 같이 설계할 수 있습니다.
/users/123/posts
: 123번 사용자가 작성한 모든 게시글 (Collection)/users/123/posts/45
: 123번 사용자가 작성한 45번 게시글 (Element)
이러한 구조는 /posts?authorId=123
과 같이 쿼리 파라미터를 사용하는 것보다 자원 간의 소유 또는 포함 관계를 훨씬 명확하게 보여줍니다. 하지만 너무 깊은 계층(3~4단계를 초과)은 URI를 지나치게 길고 복잡하게 만들 수 있으므로 적절한 수준에서 타협하는 것이 좋습니다.
2.3. 컬렉션에는 복수형 명사를 사용하라
API URI의 일관성을 유지하기 위해, 자원의 컬렉션을 나타내는 URI에는 복수형 명사를 사용하는 것이 일반적인 컨벤션입니다. 이는 단수형과 복수형을 혼용할 때 발생하는 혼란을 방지하고, URI가 자원의 단일 요소(Element)를 가리키는지 아니면 집합(Collection)을 가리키는지 명확하게 해줍니다.
- 일관성 있는 예 (복수형 사용):
GET /users
- 모든 사용자 목록을 조회GET /users/123
- ID가 123인 사용자를 조회POST /users
- 새로운 사용자를 생성
- 일관성이 부족한 예 (단수형/복수형 혼용):
GET /user
- 모든 사용자 목록을 조회? 아니면 특정 사용자? 모호함.GET /user/123
- ID가 123인 사용자를 조회
복수형 명사를 일관되게 사용하면 /자원컬렉션/자원ID
형태의 패턴을 유지할 수 있어 API의 예측 가능성이 높아집니다.
2.4. URI 가독성을 위한 가이드라인
URI는 개발자가 쉽게 읽고 이해할 수 있어야 합니다. 몇 가지 추가적인 가이드라인은 다음과 같습니다.
- 소문자 사용: URI의 호스트(Host) 부분은 대소문자를 구분하지 않지만, 경로(Path) 부분은 서버에 따라 대소문자를 구분할 수 있습니다. 불필요한 혼란을 피하기 위해 모든 경로 세그먼트는 소문자로 작성하는 것이 좋습니다.
- 단어 구분자로는 하이픈(-) 사용: URI 경로에 여러 단어로 구성된 자원명을 사용해야 할 경우, 언더스코어(
_
) 대신 하이픈(-
)을 사용하는 것이 일반적입니다. 이는 검색 엔진 최적화(SEO)에도 유리하며, 가독성 측면에서도 더 선호됩니다. 예를 들어,/product-categories
가/product_categories
보다 낫습니다. - 파일 확장자 미포함: URI는 자원의 표현 형식을 포함해서는 안 됩니다.
/users/123.json
대신/users/123
을 사용하고, 표현 형식은 HTTP 요청 헤더의Accept
와 응답 헤더의Content-Type
을 통해 명시해야 합니다. 이를 통해 동일한 URI가 클라이언트의 요구에 따라 JSON, XML 등 다양한 형식의 데이터를 제공할 수 있는 유연성을 확보하게 됩니다.
2.5. API 버전 관리 전략
API는 한번 배포하고 끝나는 것이 아니라 지속적으로 변화하고 발전합니다. 기존 API에 변경이 필요할 때, 하위 호환성을 유지할 수 없는 변경(Breaking Change)이 발생하면 기존 클라이언트들이 오작동할 수 있습니다. 이를 방지하기 위해 API 버전 관리는 필수적입니다. 일반적으로 사용되는 버전 관리 전략은 다음과 같습니다.
- URI에 버전 정보 포함 (가장 일반적):
/api/v1/users
이 방식은 가장 직관적이고 명확합니다. 개발자는 URI만 보고도 어떤 버전의 API를 사용하고 있는지 즉시 알 수 있으며, 브라우저에서 테스트하기도 쉽습니다. 많은 대규모 API(Google, Facebook 등)가 이 방식을 채택하고 있습니다. 하지만 REST 원칙에 엄격하게 따지자면, URI는 자원의 고유한 위치를 나타내야 하는데 버전 정보가 포함되는 것은 자원의 본질을 해친다는 비판도 있습니다.
- 요청 헤더에 버전 정보 포함:
Accept: application/vnd.myapi.v1+json
사용자 정의(Custom) 미디어 타입을 사용하여 Accept 헤더에 버전 정보를 명시하는 방식입니다. 이 방법은 URI를 버전 정보로 '오염'시키지 않아 REST 원칙에 더 부합한다는 장점이 있습니다. 하지만 일반적인 개발자에게는 덜 직관적이고, 브라우저에서 직접 테스트하기 번거롭다는 단점이 있습니다.
- 쿼리 파라미터에 버전 정보 포함:
/users?version=1
URI에 쿼리 파라미터로 버전을 명시하는 방식입니다. 구현이 비교적 간단하고 테스트하기 쉽지만, URI가 지저분해 보일 수 있고 필수 파라미터처럼 보이지 않아 누락될 가능성이 있습니다. 주로 선택적인 기능 분기나 테스트 목적으로 사용됩니다.
어떤 방식을 선택할지는 팀의 철학과 프로젝트의 특성에 따라 다릅니다. 하지만 가장 중요한 것은 **일관성**입니다. 한번 정한 버전 관리 전략은 API 전체에 걸쳐 일관되게 적용되어야 합니다.
3. HTTP 메서드의 의미론적 활용
URI가 자원을 식별하는 역할을 한다면, HTTP 메서드는 그 자원에 대해 수행할 작업을 명시하는 역할을 합니다. RESTful API는 각 HTTP 메서드가 가진 고유한 의미(Semantics)를 정확하게 존중하고 활용해야 합니다. 주요 메서드의 역할과 특징은 다음과 같습니다.
메서드 | 주요 용도 | 안전성(Safe) | 멱등성(Idempotent) | 설명 |
---|---|---|---|---|
GET | 자원 조회 | O | O | 서버의 자원 상태를 변경하지 않습니다. 여러 번 호출해도 동일한 결과를 반환합니다. |
POST | 자원 생성 (하위 자원 생성) | X | X | 새로운 자원을 생성합니다. 호출할 때마다 새로운 자원이 생성될 수 있으므로 멱등성이 없습니다. |
PUT | 자원 전체 교체/수정 | X | O | 기존 자원의 전체를 요청 본문(Payload)의 내용으로 완전히 교체합니다. 여러 번 호출해도 결과는 동일합니다. |
PATCH | 자원 부분 수정 | X | X (조건부 O) | 자원의 일부 필드만 수정합니다. 멱등성은 보장되지 않으나, 신중하게 설계하면 멱등하게 만들 수 있습니다. |
DELETE | 자원 삭제 | X | O | 특정 자원을 삭제합니다. 여러 번 호출해도 결과는 동일합니다(첫 번째 호출에서 삭제되고, 이후 호출은 '없는 자원'을 삭제하므로 결과적으로 상태는 동일). |
3.1. GET: 자원의 조회
GET
메서드는 특정 자원의 표현을 요청하는 데 사용됩니다. URI가 컬렉션(예: /users
)을 가리키면 해당 컬렉션에 속한 자원들의 목록을, 특정 요소(예: /users/123
)를 가리키면 해당 자원의 상세 정보를 반환합니다.
GET
요청은 '안전한(Safe)' 메서드입니다. 이는 GET
요청이 서버의 상태를 변경해서는 안 된다는 의미입니다. 단순히 데이터를 읽기만 해야 하며, 이 과정에서 리소스가 수정되거나 삭제되는 등의 부작용(Side Effect)이 발생해서는 안 됩니다. 또한 멱등성(Idempotent)을 가집니다. 즉, 동일한 GET
요청을 여러 번 보내도 서버의 상태는 변하지 않고 항상 같은 응답을 받게 됩니다 (물론 그 사이에 다른 요청에 의해 자원이 변경될 수는 있습니다).
예시: GET /users/123
- 123번 사용자의 정보를 조회합니다.
3.2. POST: 새로운 자원의 생성
POST
메서드는 주로 새로운 자원을 생성할 때 사용됩니다. 클라이언트는 요청 본문(Request Body)에 생성할 자원의 정보를 담아 컬렉션 URI(예: /users
)에 전송합니다. 서버는 이 정보를 바탕으로 새로운 자원을 생성하고, 보통 생성된 자원의 URI를 응답 헤더의 Location
에 담아 201 Created
상태 코드와 함께 반환합니다.
POST
는 멱등성을 가지지 않습니다. 동일한 POST
요청을 두 번 보내면, 두 개의 서로 다른 자원이 생성될 수 있습니다. 예를 들어, 게시글 작성 API에 POST /posts
를 두 번 호출하면 두 개의 게시글이 등록되는 것이 일반적입니다.
예시: POST /users
(요청 본문에 {"name": "새사용자", "email": "new@example.com"}
포함) - 새로운 사용자를 생성합니다.
3.3. PUT: 자원의 전체 교체
PUT
메서드는 특정 자원의 전체 내용을 요청 본문에 담긴 내용으로 완전히 교체(Replace)할 때 사용됩니다. URI는 반드시 교체 대상이 되는 특정 자원(예: /users/123
)을 명시해야 합니다. 만약 해당 URI에 자원이 존재하지 않는다면, 서버는 요청 본문의 내용으로 새로운 자원을 생성할 수도 있습니다.
PUT
은 멱등성을 가집니다. 예를 들어, PUT /users/123
요청을 통해 사용자 이름을 '김철수'로 변경했다고 가정해 봅시다. 이 동일한 요청을 여러 번 보내더라도 123번 사용자의 이름은 계속 '김철수'로 유지될 뿐, 새로운 변화가 발생하지 않습니다.
중요한 점은 PUT
은 '전체 교체'라는 점입니다. 만약 사용자 자원에 name
과 email
필드가 있는데, 요청 본문에 {"name": "이영희"}
만 담아 보냈다면, 기존의 email
필드는 누락되어 null이나 기본값으로 변경될 수 있습니다. 이것이 PATCH
와 PUT
의 핵심적인 차이입니다.
예시: PUT /users/123
(요청 본문에 {"name": "수정된이름", "email": "edited@example.com"}
포함) - 123번 사용자의 정보를 요청 본문의 내용으로 완전히 덮어씁니다.
3.4. PATCH: 자원의 부분 수정
PATCH
메서드는 자원의 일부만을 수정할 때 사용됩니다. PUT
이 자원 전체를 교체하는 반면, PATCH
는 요청 본문에 포함된 필드만 변경합니다. 예를 들어, 사용자의 이메일 주소만 변경하고 싶을 때 {"email": "new.email@example.com"}
이라는 내용만 담아 PATCH
요청을 보내면, 기존의 이름 정보는 그대로 유지된 채 이메일만 변경됩니다.
PATCH
의 멱등성은 보장되지 않습니다. 예를 들어, PATCH /accounts/123
요청에 {"operation": "deposit", "amount": 100}
과 같은 연산을 담아 보낸다면, 호출할 때마다 잔액이 100씩 증가하므로 멱등하지 않습니다. 하지만 '특정 필드의 값을 특정 값으로 변경'하는 작업은 멱등하게 설계될 수 있습니다.
예시: PATCH /users/123
(요청 본문에 {"email": "another.email@example.com"}
포함) - 123번 사용자의 이메일 주소만 변경합니다.
3.5. DELETE: 자원의 삭제
DELETE
메서드는 특정 자원을 삭제하는 데 사용됩니다. URI는 삭제할 자원을 명확히 지정해야 합니다 (예: /users/123
). 성공적으로 삭제된 경우, 서버는 보통 200 OK
또는 204 No Content
상태 코드를 반환합니다. 204
는 응답 본문에 아무런 내용이 없음을 의미하며, 삭제 작업에 대한 응답으로 자주 사용됩니다.
DELETE
는 멱등성을 가집니다. 동일한 DELETE /users/123
요청을 여러 번 보내도, 첫 번째 요청에서 해당 사용자는 삭제되고, 이후의 요청들은 '이미 존재하지 않는 리소스'에 대한 삭제 요청이 되므로 서버의 상태에 더 이상 변화를 일으키지 않습니다. 결과적으로 최종 상태는 동일합니다.
예시: DELETE /users/123
- 123번 사용자를 삭제합니다.
4. 명확하고 일관된 응답 설계
클라이언트가 API를 효과적으로 사용하기 위해서는 서버의 응답이 명확하고 예측 가능해야 합니다. 잘 설계된 응답은 성공 여부, 요청 처리 결과, 그리고 오류 발생 시 원인에 대한 정보를 효과적으로 전달합니다. 이는 HTTP 상태 코드와 응답 본문(Payload) 설계를 통해 이루어집니다.
4.1. HTTP 상태 코드의 정확한 사용
HTTP 상태 코드는 클라이언트에게 요청 처리 결과를 알려주는 가장 기본적인 수단입니다. 모든 상황에 200 OK
를 반환하고 응답 본문에 성공/실패 여부를 담는 것은 RESTful 하지 않은 방식입니다. 상태 코드는 그 의미에 맞게 정확하게 사용되어야 합니다.
- 2xx (Success): 요청이 성공적으로 처리되었음을 의미합니다.
200 OK
: 요청이 성공했으며, 응답 본문에 요청된 데이터가 포함됨 (GET, PUT, PATCH 성공 시).201 Created
: 요청이 성공하여 새로운 리소스가 생성됨 (POST 성공 시). 응답 헤더의Location
에 생성된 리소스의 URI를 포함하는 것이 좋습니다.202 Accepted
: 요청은 접수되었으나 처리가 아직 완료되지 않음 (비동기 처리).204 No Content
: 요청은 성공했지만 반환할 콘텐츠가 없음 (DELETE 성공 또는 내용 없는 PUT 성공 시).
- 3xx (Redirection): 클라이언트가 요청을 완료하기 위해 추가적인 조치가 필요함을 의미합니다.
301 Moved Permanently
: 요청한 리소스의 URI가 영구적으로 변경되었음.
- 4xx (Client Error): 클라이언트 측의 오류로 인해 요청을 처리할 수 없음을 의미합니다.
400 Bad Request
: 잘못된 문법 등 요청 자체가 잘못되었음 (예: 필수 파라미터 누락, 데이터 형식 오류).401 Unauthorized
: 인증되지 않은 사용자의 요청. 인증(Authentication)이 필요함.403 Forbidden
: 인증은 되었으나 해당 리소스에 접근할 권한이 없음 (Authorization).404 Not Found
: 요청한 리소스가 존재하지 않음.405 Method Not Allowed
: 요청한 URI에 대해 허용되지 않은 HTTP 메서드를 사용함.409 Conflict
: 리소스의 현재 상태와 충돌하여 요청을 처리할 수 없음 (예: 중복된 이메일로 회원가입 시도).
- 5xx (Server Error): 서버 측의 오류로 인해 요청을 처리할 수 없음을 의미합니다.
500 Internal Server Error
: 서버 내부에서 예상치 못한 오류가 발생함. 클라이언트에게 구체적인 오류 정보를 노출하지 않도록 주의해야 합니다.503 Service Unavailable
: 서버가 과부하 또는 유지보수로 인해 일시적으로 요청을 처리할 수 없음.
4.2. 일관성 있는 응답 본문 구조
HTTP 상태 코드가 요청의 전반적인 결과를 알려준다면, 응답 본문은 구체적인 데이터나 오류에 대한 상세 정보를 제공합니다. API 전체에 걸쳐 일관된 응답 본문 구조를 사용하는 것은 클라이언트 개발의 편의성을 크게 향상시킵니다.
성공 응답 (Success Response)
성공 응답의 데이터를 봉투(Envelope) 패턴으로 감싸서 추가적인 메타데이터를 제공하는 것이 좋습니다. 이는 특히 페이지네이션과 같은 기능에 유용합니다.
단일 데이터 응답 예시:
{
"data": {
"id": 123,
"name": "홍길동",
"email": "gildong@example.com"
}
}
컬렉션 데이터 응답 예시 (페이지네이션 포함):
{
"data": [
{ "id": 1, "title": "첫 번째 게시글" },
{ "id": 2, "title": "두 번째 게시글" }
],
"pagination": {
"totalItems": 100,
"totalPages": 10,
"currentPage": 1,
"itemsPerPage": 10
}
}
오류 응답 (Error Response)
오류 응답은 클라이언트 개발자가 문제를 해결하는 데 도움이 되도록 충분하고 구조화된 정보를 제공해야 합니다. 단순히 "오류 발생"이라는 문자열만 반환하는 것은 최악의 설계입니다.
구조화된 오류 응답 예시 (400 Bad Request):
{
"error": {
"code": "INVALID_INPUT",
"message": "입력값이 유효하지 않습니다.",
"details": [
{
"field": "email",
"reason": "유효한 이메일 형식이 아닙니다."
},
{
"field": "password",
"reason": "비밀번호는 최소 8자 이상이어야 합니다."
}
]
}
}
이러한 구조는 클라이언트가 프로그램적으로 오류를 파싱하여 사용자에게 필드별로 구체적인 피드백을 보여주는 것을 가능하게 합니다.
4.3. 네이밍 컨벤션
JSON 응답 본문의 필드명(Key)에 대한 네이밍 컨벤션을 정하고 일관되게 지키는 것이 중요합니다. 일반적으로 두 가지 방식이 많이 사용됩니다.
- 카멜 케이스 (camelCase):
userName
,createdAt
. JavaScript 진영에서 선호되며, 많은 프론트엔드 코드와 자연스럽게 어울립니다. - 스네이크 케이스 (snake_case):
user_name
,created_at
. Python, Ruby 등 여러 백엔드 언어 및 데이터베이스 필드명에서 선호됩니다.
어떤 것을 선택하든 상관없지만, API 전체에서 하나의 컨벤션만을 일관되게 사용해야 합니다. 두 가지를 혼용하는 것은 클라이언트 개발자에게 큰 혼란을 줍니다.
5. 고급 설계 패턴과 고려사항
기본적인 설계 원칙을 넘어, 실제 프로덕션 환경에서 API를 더욱 강력하고 유연하게 만들기 위한 여러 고급 기법들이 있습니다.
5.1. HATEOAS (Hypermedia as the Engine of Application State)
앞서 언급했듯이, HATEOAS는 REST의 핵심 원칙 중 하나입니다. 이는 응답에 데이터뿐만 아니라, 해당 자원과 관련된 다음 행동을 할 수 있는 링크(하이퍼미디어)를 포함시키는 것을 의미합니다.
HATEOAS를 적용한 응답 예시:
{
"id": 42,
"status": "shipped",
"totalPrice": 150.00,
"currency": "USD",
"_links": {
"self": { "href": "/orders/42" },
"customer": { "href": "/customers/123" },
"tracking": { "href": "/orders/42/tracking" },
"cancel": { "href": "/orders/42/cancel", "method": "POST" }
}
}
위 응답에서 클라이언트는 주문 상태가 'shipped'임을 알 수 있을 뿐만 아니라, _links
객체를 통해 이 주문을 '추적'하거나 '취소'할 수 있는 URL을 직접 얻을 수 있습니다. 만약 주문 상태가 'delivered'라면, 서버는 응답에서 'cancel' 링크를 제거할 수 있습니다. 이를 통해 클라이언트는 서버의 비즈니스 로직에 대한 의존성 없이, 서버가 제공하는 링크를 따라 상태를 전이시킬 수 있습니다. 이는 서버 API가 변경되더라도 클라이언트 코드를 수정할 필요성을 크게 줄여주는 강력한 메커니즘입니다.
5.2. 필터링, 정렬, 페이지네이션
수많은 데이터를 다루는 컬렉션 자원의 경우, 클라이언트가 원하는 데이터만 효율적으로 가져갈 수 있는 기능을 제공하는 것이 필수적입니다.
- 필터링 (Filtering): 특정 조건에 맞는 데이터만 조회할 수 있도록 쿼리 파라미터를 사용합니다.
GET /users?status=active&role=admin
- 정렬 (Sorting): 특정 필드를 기준으로 결과를 정렬할 수 있도록 합니다. 보통
sort
파라미터를 사용하며, 접두사(+
또는-
)로 오름차순/내림차순을 지정합니다.GET /posts?sort=-createdAt,title
(생성일 내림차순, 제목 오름차순으로 정렬) - 페이지네이션 (Pagination): 대량의 결과를 작은 단위(페이지)로 나누어 제공합니다.
- 오프셋 기반 (Offset-based):
GET /posts?offset=20&limit=10
(21번째부터 10개). 구현이 쉽지만 데이터가 자주 변경되는 대규모 테이블에서는 성능 이슈가 발생할 수 있습니다. - 커서 기반 (Cursor-based):
GET /posts?cursor=abcdefg&limit=10
(특정 지점 이후 10개). 이전 응답에서 받은 마지막 항목의 ID(cursor)를 기준으로 다음 페이지를 요청합니다. 성능이 뛰어나고 실시간 데이터에 적합합니다.
- 오프셋 기반 (Offset-based):
5.3. 보안: 인증과 인가
API 보안은 가장 중요한 고려사항 중 하나입니다.
- 인증 (Authentication): 사용자가 누구인지 확인하는 과정입니다. 현대 API에서는 주로 Bearer 토큰 방식, 특히 JWT(JSON Web Token)를 사용합니다. 클라이언트는 로그인 시 발급받은 JWT를 매 요청의
Authorization: Bearer <token>
헤더에 포함시켜 보냅니다. - 인가 (Authorization): 인증된 사용자가 특정 리소스에 접근할 수 있는 권한이 있는지 확인하는 과정입니다. 역할 기반 접근 제어(RBAC) 등의 모델을 사용하여, 예를 들어 'admin' 역할을 가진 사용자만 모든 사용자 목록(
GET /users
)을 조회할 수 있도록 제한할 수 있습니다. - HTTPS/TLS: 모든 API 통신은 반드시 TLS(Transport Layer Security)로 암호화된 HTTPS를 통해 이루어져야 합니다. 이를 통해 중간자 공격(Man-in-the-middle attack)으로부터 데이터를 보호할 수 있습니다.
5.4. 캐싱 및 속도 제한
- 캐싱 (Caching): 서버는
ETag
(리소스의 특정 버전을 식별하는 태그)나Last-Modified
헤더를 응답에 포함할 수 있습니다. 클라이언트는 다음 요청 시 이 값을 각각If-None-Match
,If-Modified-Since
헤더에 담아 보냅니다. 만약 서버의 리소스가 변경되지 않았다면, 서버는 데이터 본문 없이304 Not Modified
응답을 보내 네트워크 대역폭을 절약할 수 있습니다. - 속도 제한 (Rate Limiting): 악의적인 공격이나 특정 사용자의 과도한 요청으로부터 서버를 보호하기 위해 단위 시간당 요청 횟수를 제한해야 합니다. 보통 IP 주소나 사용자 계정을 기준으로 제한을 겁니다. 초과 요청에 대해서는
429 Too Many Requests
상태 코드를 반환하고,X-RateLimit-Limit
(총 허용량),X-RateLimit-Remaining
(남은 요청 수),X-RateLimit-Reset
(제한이 초기화되는 시간) 등의 헤더를 통해 클라이언트에게 현재 제한 상태를 알려주는 것이 좋습니다.
5.5. API 문서화
API는 그 자체로 하나의 제품이며, 제품에는 사용 설명서가 필요합니다. 잘 만들어진 문서는 개발자들의 API 학습 곡선을 낮추고, 잘못된 사용으로 인한 오류를 줄여줍니다. 현대적인 API 문서화의 표준은 **OpenAPI Specification(구 Swagger)**입니다. OpenAPI는 API의 엔드포인트, 파라미터, 요청/응답 스키마 등을 기계가 읽을 수 있는 형식(YAML 또는 JSON)으로 정의하는 명세입니다. 이 명세를 기반으로 다음과 같은 이점을 얻을 수 있습니다.
- 인터랙티브한 API 문서 자동 생성 (Swagger UI, ReDoc 등)
- 다양한 언어의 클라이언트 SDK 코드 자동 생성
- API 요청을 테스트하고 검증하는 도구로 활용
코드를 변경할 때마다 문서를 수동으로 업데이트하는 것은 비효율적이고 실수를 유발하기 쉽습니다. 코드 내 어노테이션 등을 통해 OpenAPI 명세를 자동으로 생성하고, 이를 CI/CD 파이프라인에 통합하여 항상 최신 상태의 문서를 유지하는 것이 바람직합니다.
결론: 좋은 API는 소통의 예술이다
지금까지 살펴본 것처럼, 잘 설계된 REST API를 만드는 것은 단순히 기능을 구현하는 것을 넘어, 시스템의 구조를 명확히 하고, 미래의 변화에 유연하게 대응하며, 무엇보다 API를 사용하는 다른 개발자들과 효과적으로 소통하는 과정입니다. REST는 엄격한 프로토콜이 아닌 아키텍처 스타일, 즉 '철학'에 가깝기 때문에 때로는 현실적인 제약과 트레이드오프 속에서 최선의 결정을 내려야 할 때도 있습니다.
가장 중요한 것은 **일관성**과 **예측 가능성**입니다. URI 구조, HTTP 메서드 활용, 응답 본문 포맷, 오류 처리 방식 등 모든 측면에서 일관된 규칙을 적용함으로써, 개발자들은 API의 일부만 보고도 나머지를 쉽게 유추할 수 있게 됩니다. 이는 개발자 경험(Developer Experience, DX)을 향상시키고, 결국 생산성의 증대로 이어집니다.
오늘날 GraphQL이나 gRPC와 같은 새로운 API 기술들이 등장하고 있지만, 웹의 근간을 이루는 HTTP를 가장 잘 활용하는 REST의 철학과 범용성은 여전히 강력한 힘을 발휘하고 있습니다. 여기에 소개된 원칙들을 나침반 삼아, 견고하고 유연하며 함께 일하는 동료들을 배려하는 API를 만들어 나간다면, 여러분은 복잡하게 얽힌 디지털 생태계의 성공적인 청사진을 그리는 핵심 설계자가 될 수 있을 것입니다.