Devths에서의 API 설계는 어떤 고민을 거쳤는가? - 100-hours-a-week/9-team-Devths-WIKI GitHub Wiki

배경

팀프로젝트를 진행하며 본격적인 개발 시작 전 API를 설계했습니다.

탄탄한 설계가 뒷받침되어야 추후에 불필요한 변경이 최소화되므로 나름의 기준을 세우고 진행하고자 했습니다.

기준이라고 하면 여러 가지가 있겠지만, 저는 API하면 가장 먼저 떠오르는 ‘RESTful’이라는 키워드에 초점을 맞춰 보았습니다.

해당 페이지는 REST와 RESTful가 어떤 부분이 다른지, 우리 서비스 관점에서 RESTful API는 어떻게 작성해야 하는지 고민했던 과정을 기록해둔 문서입니다.

REST? RESTful?

API를 설계하기 전에 매번 언급되는 용어 두 가지를 간단하게 정리해 보았습니다.

REST와 RESTful은 언뜻 보기에 비슷해 서로 대치가 가능한 용어처럼 보이지만, 엄밀히 따지고 보면 사용처가 다르다는 것을 알게 되었습니다.

REST

REST는 ‘Representational State Transfer’의 약어입니다. 직역하자면 ‘표현적인 상태 전달’인데요, 어떤 상태를 어떻게 표현해서 누구에게 전달한다는 것일까요?

미국의 컴퓨터 공학자인 Roy Fielding 박사는 이렇게 정의했습니다 :

REST components communicate by transferring a representation of a resource in a format matching one of an evolving set of standard data types

REST 컴포넌트들은 지속적으로 확장·발전하는 표준 데이터 타입 집합 중 하나에 부합하는 형식으로 표현된 리소스의 표현(representation)을 전송함으로써 서로 통신한다.

The key abstraction of information in REST is a resource. Any information that can be named can be a resource: a document or image, a temporal service (e.g. "today's weather in Los Angeles"), a collection of other resources, a non-virtual object (e.g. a person), and so on.

REST에서 정보의 핵심 추상화 단위는 리소스이다. 이름을 부여할 수 있는 모든 정보는 리소스가 될 수 있다. 예를 들어 문서나 이미지와 같은 정적인 데이터, “오늘의 로스앤젤레스 날씨”와 같은 시간에 따라 변하는 서비스, 다른 리소스들의 집합, 사람과 같은 비가상 객체 등도 모두 리소스가 될 수 있다.

REST components perform actions on a resource by using a representation to capture the current or intended state of that resource and transferring that representation between components.

REST 컴포넌트들은 리소스의 현재 상태 또는 의도된 상태를 담은 표현(representation)을 사용하고, 그 표현을 컴포넌트 간에 전송함으로써 해당 리소스에 대한 동작을 수행한다.

참고 자료 링크

이 내용들을 바탕으로 REST라는 건 ‘이름을 붙일 수 있는 모든 것(심지어는 웹 서비스의 기능까지도)’을 리소스로 보고, 리소스의 ‘현재 상태’ 또는 ‘의도된 상태(=이 리소스에 부여하고 싶은 상태)’를 담은 ‘표현’을 주고 받으며 동작을 수행하는 일종의 아키텍처라고 이해할 수 있겠습니다.

현대 웹 통신의 표준인 HTTP에서는 다음과 같이 구현됩니다 :

  • 리소스 = HTTP URI
  • 표현 = HTTP Request/Response Body + Header(Metadata)
  • 상태 = Body Payload(표현) 내 ‘리소스’의 상태
  • 리소스에 대한 의도 = HTTP Method
  • 행위 = HTTP Method가 가진 의도에 따라 표현을 전송하는 것

RESTful

‘REST’가 좋은 설계를 위한 아키텍처였다면, 형용사형 어미 -ful이 붙은 ‘RESTful’은 이름에서 유추 가능하듯이 ‘REST 아키텍처 스타일의 제약 조건을 잘 지켜 설계된’이라는 뜻으로 생각해볼 수 있겠습니다.

그렇다면 RESTful API란?

간단하게 알아본 REST와 RESTful의 정의로 미루어 보았을 때, RESTful API는 말 그대로 ‘REST 아키텍처의 규칙을 잘 지켜 설계된 API’라고 보면 되겠습니다.

그렇다면 어떻게 작성해야 RESTful한 API가 될 수 있을까요? 그 규칙은 다음과 같습니다.

  1. 리소스 중심 : 행위가 아니라 리소스를 URI로 식별
  2. Uniform Interface 준수 : 상태 조작은 표현(representation), 의미 전달은 HTTP Method
  3. 상태가 표현에 담겨 전달 : 요청/응답은 항상 “이 리소스는 지금 이런 상태다” 또는 “이 리소스를 이런 상태로 만들고 싶다”
  4. HTTP 의미를 존중 : 각 HTTP Method가 가진 의도를 그대로 사용
  5. 클라이언트–서버의 독립 구조 유지 : 서버 구현 변경 ≠ 클라이언트 수정

즉, RESTful API의 본질은 HTTP가 가진 의미론을 ‘의도적으로 그대로 사용'하고, 리소스와 상태 전이를 그 의미 안에 담는 것이라고 볼 수 있겠습니다.

Devths에서의 API 설계

설계 및 작성 과정에서 ‘RESTful’에 대한 고민이 잘 드러나는 API를 정리해 보았습니다.

리소스 식별(명사화) 관점

저희 서비스에는 회원 간 팔로우 기능이 존재합니다.

팔로우는 유저 간 M:N 연관 관계를 가지므로 follows라는 매핑 테이블을 만들어 팔로우 정보를 관리하게끔 설계했습니다.

이때 ‘다른 회원을 팔로우하는 기능’의 API는 어떻게 작성해야 할까요?

회원 A가 회원 B를 팔로우하면, follower_id=a.id, following_id=b.id를 값으로 갖는 follow 엔티티 하나가 생성됩니다.

여기서 저는 URI 작성 방식에 대한 고민을 하게 됩니다.

처음에는 /api/users/{userId}/follows로 작성했다가, 경로에 동사(follow)가 포함되어 변경하기로 했습니다. RESTful 아키텍처에서는 자원을 명사로 표현해야 하는 것이 원칙이기 때문입니다.

팔로우 기능의 동작 흐름을 간단하게 체크해봤습니다.

제가 yun을 팔로우하면, yun의 ‘팔로워 목록’에 제가 추가됩니다. 즉, ‘{userId}의 팔로워 목록에 나라는 팔로워를 추가한다’는 관점으로 접근하여 /api/users/{userId}/followers로 결정했습니다.

같은 관점에서 HTTP Method는 POST로 결정했습니다. REST에서 HTTP Method는 리소스에 대한 의도를 나타내고, 팔로우 기능이 정상적으로 실행되면 회원 간의 관계를 표현하는 리소스 하나가 생성되니까요.

즉, POST /api/users/{userId}/followers는 ‘{userId}의 팔로워 목록에 나를 추가해줘!’라는 의미를 갖게 됩니다.

PUT vs. PATCH

HTTP Method를 사용해 ‘리소스에 대한 수정’의 의도를 나타낼 때 흔히 두 가지 방식 중 하나를 택합니다.

하나는 PUT, 다른 하나는 PATCH입니다.

이 둘은 수정하려는 값이 리소스의 전체냐 부분이냐를 넘어, 멱등성의 보장 여부와 리소스 상태를 바라보는 관점에서 큰 차이를 보입니다.

PUT은 항상 멱등성을 보장해야 하며, 클라이언트가 보낸 데이터가 서버 리소스의 ‘새로운 상태’가 됩니다.

PATCH는 요청 의미에 따라 멱등하지 않을 수도 있으며, 기존 리소스를 유지한 채 클라이언트가 보낸 데이터에 포함된 값만 수정합니다.

저는 ‘내 정보 수정 API’에서 둘 중 어떤 것을 써야 할지 고민이었습니다.

해당 API에서는 이런 식으로 값을 받습니다. nickname(닉네임)은 필수값이고, interests(관심 분야)는 선택값입니다.

내 정보 수정 페이지의 특성상 사용자가 입력한 화면 전체의 폼 데이터와 서버의 리소스 상태를 완전히 동기화해야 하며, 서버의 최종 상태와 사용자가 마지막으로 보낸 상태가 동일하게 유지되는 것(멱등성)이 중요하다고 생각했습니다. 현재 설계에서는 PATCH를 사용해도 멱등하게 동작하는 것처럼 보이지만 저는 HTTP Method 명세 레벨에서 보장되는(보장해야 하는!) 멱등성과 상태 동기화를 우선 순위에 두어 PUT을 사용했습니다.

PATCH에서는 interests와 같은 선택적 필드가 누락되는 경우, ‘데이터를 지우려는 의도(null 전송)’와 ‘수정하지 않으려는 의도(필드 누락)’의 구분이 모호해질 수 있습니다.

반면 PUT은 전체 상태를 동기화하므로 필드의 누락을 명확하게 ‘잘못된 요청’으로 간주할 수 있어 데이터 정합성을 유지하기에 유리하다고 판단했습니다.

이 관점에서, PUT은 리소스 전체를 전달받은 표현으로 대체하겠다는 의도를 가지고 멱등성을 항상 보장하므로 제가 설계한 API의 의도에 적절하다고 결론을 내렸습니다!

최종적으로 내 정보 수정 API는 PUT /api/users/me로 확정하게 되었습니다.

다만 interests 필드를 누락하지 않게 validation을 하고, 관심 분야가 없으면 빈 배열을 전송하도록 정의했습니다.

회고

제가 예시로 작성한 API가 Best Practice라고는 단언할 수 없습니다. 완벽한 설계란 존재하지 않고, 나름대로 고민해 내린 결론들이 다른 기획에서는 틀릴 수도 있기 때문입니다.

그러나 이 과정을 통해 RESTful하게 API를 설계한다는 게 무엇인지에 대해 근본적으로 다시 생각해볼 수 있었습니다.

결국 RESTful함이란 정답이 정해진 규격을 맞추는 것이 아니라 HTTP라는 표준 프로토콜이 가진 의미와 특성을 활용해 '누구나 이해할 수 있고 예측 가능한 인터페이스'를 만드는 과정임을 배웠습니다.

앞으로도 모든 설계가 항상 정답일 수는 없겠지만, 적어도 ‘왜 이렇게 설계했는지’를 설명할 수 있는 API를 만드는 것을 하나의 기준으로 삼고자 합니다.

참고 자료

RESTful API란 무엇인가요?

Representational State Transfer (REST)