1. Bibliotecă
  2. HTTP와 웹
  3. API와 웹 서비스

Actualizat acum 1 lună

REST는 외워야 할 규칙 목록이 아닙니다. 하나의 내기입니다. API를 자원 중심으로 구성하고 HTTP를 설계된 방식대로 사용하면, 개발자들은 문서를 읽기 전에 이미 API가 어떻게 작동하는지 짐작할 수 있습니다.

이 내기는 항상 통합니다. 여러분의 API를 한 번도 본 적 없는 개발자도 GET /users/123이 사용자를 조회하고, DELETE /users/123이 삭제하며, POST /users가 생성한다는 것을 짐작할 수 있습니다. 문서를 읽지 않았습니다. 그냥 알 수 있었습니다.

이것이 바로 핵심 보상입니다. 나머지 모든 것—명명 규칙, 상태 코드, 응답 형식—은 이 예측 가능성을 지키기 위해 존재합니다.

절차가 아닌 자원

REST의 근본적인 전환은 무언가에 무슨 작업을 할 수 있는지가 아니라, 그것이 무엇인지를 중심으로 생각하는 것입니다.

절차적 사고는 /getAllUsers, /createNewOrder, /updateUserEmail 같은 엔드포인트를 만들어냅니다. 자원 중심 사고는 /users, /orders, /users/123을 만들어냅니다. HTTP 메서드가 동사 역할을 하고, URI가 명사를 식별합니다.

이건 단순한 미학의 문제가 아닙니다. 모든 엔드포인트가 각각의 절차라면, 개발자들은 하나하나 따로 익혀야 합니다. 엔드포인트가 자원이라면, 패턴을 한 번만 익히면 어디서든 적용할 수 있습니다.

여러분의 도메인에서 명사를 찾으세요. 사용자, 주문, 상품, 댓글, 결제—이것들이 바로 자원입니다. 명사로 표현하기가 어렵다면, 다른 방식으로 처리해야 할 절차를 노출하려는 것일 수 있습니다.

의미 있는 계층 구조

자원에는 관계가 있습니다. 사용자에게는 주문이 있고, 주문에는 항목이 있으며, 게시물에는 댓글이 있습니다.

이 관계를 중첩으로 표현하세요. /users/123/orders는 사용자 123의 주문을 반환하고, /posts/456/comments는 게시물 456의 댓글을 반환합니다. URI 자체가 관계를 말해줍니다.

하지만 이런 경고 신호를 조심하세요. /users/123/orders/456/items/789/modifiers/012 같은 URI를 만들고 있다면 멈추세요. 깊은 중첩은 보기 안 좋을 뿐만 아니라, 모델에 뭔가 잘못됐다는 신호입니다. 보통은 직접 접근 가능해야 할 자원이 불필요한 상위 자원을 거치도록 강제되고 있다는 뜻입니다.

이렇게 자문해 보세요. 누군가 /items/789에 직접 접근해야 할 때가 있나요? 그렇다면 /items/789에서 직접 접근할 수 있게 하세요. /orders/456/items의 계층 구조는 직접 접근과 함께 공존할 수 있습니다.

약속으로서의 URI

잘 설계된 URI는 거기서 무엇을 찾을 수 있는지에 대한 약속입니다.

컬렉션에는 복수 명사를. /users는 컬렉션이고, /users/123은 사용자 한 명입니다. 이 일관성 덕분에 개발자들은 /user를 써야 할지 /users를 써야 할지 고민할 필요가 없습니다.

소문자와 하이픈을. /user-profiles/UserProfiles/user_profiles보다 읽기 좋습니다. 더 중요한 것은, 대소문자 구분 혼란을 피할 수 있다는 점입니다. 일부 시스템은 /Users/users를 다른 경로로 취급합니다.

구현 세부사항을 노출하지 마세요. /users.php, /api/rest/users.json, /v1/userController/getAll은 클라이언트가 알 필요 없는 구현 정보를 드러냅니다. PHP에서 Node로 전환해도 URI는 바뀌지 않아야 합니다.

짧고 직관적으로. 개발자가 URI를 짐작할 수 없다면, 명명이 실패한 겁니다. /api/v1/customers/123/orders는 괜찮습니다. /api/v1/customer-management-system/customer-resource/123/order-processing-module/order-collection은 아닙니다.

HTTP 메서드는 의미가 있습니다

HTTP 메서드는 그냥 붙이는 레이블이 아닙니다. 각각 고유한 의미가 있으며, 그 의미를 어기면 내기는 깨집니다.

GET은 신성합니다. GET은 절대 아무것도 변경해서는 안 됩니다. 부수 효과도, 상태 변경도, 아무것도 없어야 합니다. 크롤러가 GET 엔드포인트를 호출해도 데이터가 삭제되거나 이메일이 발송되면 안 됩니다. GET /users/123이 때때로 사용자를 생성한다면, 웹의 근본을 망가뜨린 겁니다.

POST는 생성합니다. POST /users는 사용자를 생성합니다. 서버가 ID를 할당하고 반환합니다. POST는 멱등성이 없는 유일한 메서드입니다. 두 번 호출하면 두 개의 자원이 생성됩니다.

PUT은 교체합니다. PUT /users/123은 사용자 전체를 교체합니다. {"name": "John"}만 보내면 다른 필드는 지워져야 합니다. PUT은 멱등성이 있습니다. 같은 PUT을 반복해서 보내도 결과는 동일합니다.

PATCH는 수정합니다. PATCH /users/123{"email": "new@example.com"}을 보내면 이메일만 변경됩니다. 다른 필드는 그대로 유지됩니다. 정밀하게 업데이트하고 싶을 때 PATCH를 쓰세요.

DELETE는 삭제합니다. DELETE /users/123은 사용자를 삭제합니다. DELETE는 멱등성이 있습니다. 이미 삭제된 것을 다시 삭제해도 오류가 아니라 그냥 아무 일도 일어나지 않습니다.

이 메서드들을 올바르게 사용하면, 클라이언트는 안전한 가정을 세울 수 있습니다. GET은 안전하게 재시도할 수 있다는 것을 압니다. 네트워크 오류가 나도 PUT과 DELETE는 안전하게 재시도할 수 있다는 것을 압니다. POST는 그렇지 않을 수 있다는 것도 압니다. 이 가정들 덕분에 개발자들은 문서를 읽지 않고도 더 견고한 연동을 구축할 수 있습니다.

가르쳐주는 응답

모든 응답은 클라이언트를 도울 기회입니다.

생성한 것을 반환하세요. POST로 자원을 생성하면 그것을 반환하세요. 클라이언트가 부분 데이터를 보냈고, 여러분은 ID, 타임스탬프, 기본값을 추가했습니다. 완전한 결과를 돌려주세요. 새 자원을 가리키는 Location 헤더도 포함하세요.

변경한 것을 반환하세요. PUT이나 PATCH로 업데이트하면 업데이트된 자원을 반환하세요. 서버 측 로직이 다른 필드를 수정하거나, 데이터를 정규화하거나, 타임스탬프를 갱신했을 수 있습니다. 클라이언트가 추측하게 두지 마세요.

일관성을 유지하세요. /users/123{"id": "123", "name": "John", "email": "..."}를 반환한다면, 모든 사용자 엔드포인트가 그 구조를 반환해야 합니다. 비일관성은 클라이언트가 특수 케이스를 처리하도록 강제합니다.

컬렉션에는 맥락이 필요합니다. 전체 1,500명 중 50명의 목록은 맥락 없이는 의미가 없습니다. 페이지네이션 메타데이터를 포함하세요.

{
  "data": [...],
  "total": 1500,
  "page": 2,
  "per_page": 50,
  "has_more": true
}

도움이 되는 오류

오류 처리가 부실한 곳에서 API는 개발자의 신뢰를 잃습니다.

상태 코드는 선택이 아닙니다. 200 OK와 함께 {"success": false, "error": "..."}를 반환하는 건 HTTP 위반입니다. 클라이언트는 상태 코드를 먼저 확인합니다. 클라이언트 실수에는 4xx, 서버 오류에는 5xx를 쓰세요.

무엇이 잘못됐는지 알려주세요. "잘못된 요청"은 아무 도움이 안 됩니다. "이메일 주소 형식이 올바르지 않습니다"는 도움이 됩니다. "이메일에는 @ 기호가 포함되어야 합니다"는 더 도움이 됩니다.

안정적인 오류 코드를 제공하세요. 사람이 읽는 메시지는 바뀝니다. 오류 코드는 바뀌지 않습니다. INVALID_EMAIL_FORMAT을 쓰면 클라이언트가 영문 문장을 파싱하지 않고도 특정 오류를 프로그래밍 방식으로 처리할 수 있습니다.

유효성 검사 오류는 한꺼번에 반환하세요. 이메일도 틀리고 나이도 미달이라면, 둘 다 한 번에 알려주세요. 오류 하나를 고치고 다시 제출하고, 또 다른 오류를 발견하고 다시 제출하게 만드는 건 개발자에게 불친절한 겁니다.

{
  "error": "validation_error",
  "fields": {
    "email": "Invalid email format",
    "age": "Must be at least 18"
  }
}

내부를 절대 노출하지 마세요. 스택 트레이스, SQL 오류, 파일 경로—이것들은 공격자에게 도움을 주고 개발자를 혼란스럽게 합니다. 서버 측에 기록하고 클라이언트에는 일반적인 메시지를 반환하세요.

페이지네이션: 트레이드오프 선택하기

응답 하나에 사용자 10만 명을 담을 수는 없습니다. 페이지네이션은 필수입니다. 어떤 방식을 선택할지가 문제입니다.

오프셋 페이지네이션은 직관적입니다. ?page=2&per_page=50 또는 ?offset=100&limit=50. 개발자들이 바로 이해합니다. 하지만 약점이 있습니다. 페이지 요청 사이에 항목이 추가되거나 삭제되면 중복이 생기거나 항목을 놓칠 수 있습니다. 변화가 느린 데이터에는 보통 문제가 없지만, 실시간 피드에는 문제가 됩니다.

커서 페이지네이션은 불투명한 마커를 사용합니다. ?cursor=abc123&limit=50. 응답에 다음 커서가 포함됩니다. 데이터가 변해도 올바르게 처리됩니다. 항목을 놓치거나 중복으로 보는 일이 없습니다. 하지만 47페이지로 바로 이동하는 것은 불가능합니다. 앞으로만 이동할 수 있습니다(이전 커서로 뒤로 가는 것은 가능).

어느 쪽이 항상 더 낫지는 않습니다. 오프셋은 사용자가 페이지를 자유롭게 오가는 관리자 대시보드에 적합합니다. 커서는 항상 앞으로 진행하는 무한 스크롤 피드에 적합합니다.

어느 것을 선택하든 기본 제한을 설정하세요. 50만 건을 반환하는 무제한 GET /users는 서버와 클라이언트 모두를 다운시킵니다.

엔드포인트 확산 없는 필터링

필터링 없이는 /active-users, /admin-users, /users-created-this-week 같은 엔드포인트가 계속 늘어납니다. 이건 확장되지 않습니다.

쿼리 파라미터는 확장됩니다.

GET /users?role=admin&active=true
GET /posts?author=123&published=true&created_after=2024-01-01

정렬도 같은 방식입니다.

GET /users?sort=name           # 오름차순
GET /users?sort=-created_at    # 내림차순 (- 접두사)
GET /users?sort=last_name,first_name  # 여러 필드

필드 선택으로 페이로드 크기를 줄일 수 있습니다.

GET /users?fields=id,name,email

패턴은 일관됩니다. URI가 자원을 식별하고, 쿼리 파라미터가 원하는 방식을 조정합니다.

버전 관리: 변화에 대비하기

API는 진화합니다. 기존 동작에 의존하는 클라이언트는 새 버전을 출시해도 깨지지 않아야 합니다.

URI 버전 관리는 명시적입니다. /v1/users/v2/users가 공존할 수 있습니다. 개발자들은 모든 요청에서 버전을 확인할 수 있습니다. 단점은 북마크, 문서, 클라이언트 코드 전반에 버전 정보가 퍼진다는 것입니다.

헤더 버전 관리는 URI를 깔끔하게 유지합니다. Accept: application/vnd.api.v2+json. 단점은 버전이 보이지 않는다는 것입니다. URL만으로는 버전을 알 수 없습니다.

대부분의 팀은 단순하다는 이유로 URI 버전 관리를 선택합니다. 어느 것을 선택하든 지원 종료 정책을 마련하세요. 수명 종료 날짜를 미리 공지하고, 응답에 지원 중단 경고를 포함하고, 클라이언트에게 마이그레이션할 시간을 충분히 주세요.

보안은 선택이 아닙니다

어디서나 HTTPS를. 로그인뿐만 아니라 모든 것에 적용하세요. API 키, 토큰, 데이터는 전송 중에 암호화되어야 합니다. 2024년에 평문 HTTP를 쓸 변명은 없습니다.

모든 것을 검증하세요. 클라이언트 데이터는 검증될 때까지 위험하다고 가정하세요. 유형, 길이, 형식, 범위를 확인하고, 잘못된 입력은 명확한 오류와 함께 거부하세요.

인증하세요. 누가 호출하는지 파악하세요. API 키, OAuth 토큰, JWT—사용 사례에 맞는 방법을 선택하세요.

인가하세요. 인증은 누구인지를 알려주고, 인가는 무엇을 허용할지를 결정합니다. 사용자 123은 자신의 주문은 조회할 수 있지만 사용자 456의 주문은 조회할 수 없습니다.

요청 수를 제한하세요. 제한이 없으면 오작동하는 클라이언트 하나가 모든 사람의 서비스를 다운시킬 수 있습니다. Retry-After 헤더와 함께 429 Too Many Requests를 반환하세요.

내기를 다시 말하며

REST는 예측 가능성에 대한 내기입니다. 여기서 설명한 모든 원칙—절차보다 자원, 올바른 HTTP 메서드, 일관된 응답, 도움이 되는 오류—은 그 내기를 위해 존재합니다.

이기면, 개발자들은 며칠이 아니라 몇 시간 만에 여러분의 API와 연동합니다. 문서를 뒤지는 대신 올바르게 짐작합니다. 기대한 대로 동작하기 때문에 여러분의 API를 신뢰합니다.

이 원칙들을 어기면, 단순히 관례를 위반하는 것이 아닙니다. 개발자들이 효율적으로 일할 수 있게 해주는 가정들을 깨뜨리는 겁니다. 모든 예외는 따로 익히고 기억해야 할 특수 케이스가 됩니다.

이 규칙들은 임의적이지 않습니다. API를 예측 가능하게 만드는 것이 무엇인지에 대한 집단적 지혜입니다. 따르면, 그 예측 가능성이 저절로 따라옵니다.

RESTful 설계에 관한 자주 묻는 질문

PUT과 PATCH는 언제 써야 하나요?

자원 전체를 교체할 때—기존 내용을 완전히 덮어쓸 전체 표현을 보낼 때—PUT을 쓰세요. 다른 필드는 그대로 두고 특정 필드만 수정할 때는 PATCH를 쓰세요. 클라이언트가 주로 필드 하나씩 업데이트한다면 PATCH가 더 편리합니다. 항상 전체 객체를 보낸다면 PUT이 더 명확합니다.

CRUD에 맞지 않는 작업은 어떻게 처리하나요?

주문 취소, 게시물 보관, 초대 재발송 같은 작업은 명사보다 동사처럼 느껴집니다. 두 가지 방식이 효과적입니다. 작업을 하위 자원으로 모델링하거나(POST /orders/123/cancellation), 상태 변경으로 모델링하는 것입니다(PATCH /orders/123{"status": "cancelled"}). 작업에 자체 데이터나 이력이 있다면 하위 자원 방식이 더 명확합니다.

UUID와 순차 ID 중 어느 것을 써야 하나요?

순차 ID는 단순하고 짧지만 정보를 노출합니다(경쟁사가 사용자 수를 추정할 수 있습니다). 또한 분산 데이터베이스에서 핫스팟이 생길 수 있습니다. UUID는 불투명하고 분산 환경에 적합하지만 길고 구두로 전달하기 불편합니다. 많은 팀이 내부적으로는 순차 ID를 유지하면서 외부에는 UUID를 씁니다.

파괴적 변경과 비파괴적 변경의 버전 관리는 어떻게 하나요?

새 필드, 새 엔드포인트, 새 선택적 파라미터를 추가하는 것은 파괴적이지 않습니다. 기존 클라이언트는 이해하지 못하는 것을 무시합니다. 필드를 제거하거나, 필드 유형을 변경하거나, 동작 방식을 바꾸는 것은 파괴적이며 버전을 올려야 합니다. 판단 기준: 기존 클라이언트 코드가 수정 없이 여전히 작동하는가?

A fost utilă această pagină?

😔
🤨
😃