서버 개발자 입장에서 바라본 모바일 API 디자인

이 글은 지극히 서버 개발자 입장에서 생각한 모바일 API 디자인 방법이다. 모바일 앱과의 효과적인 인터페이스를 설계함에 있어서 그 동안의 경험을 공유하고자 한다. 모르는 부분은 개발하면서 찾아다녔지만 일부는 여전히 잘못된 정보일수 있음을 감안하고 읽어주기 바란다.

이름짓기

"Man Gave Names to All the Animals"

이름 만드는 것은 쉽지 않은 일이다. API 이름을 정하는데도 일정한 규칙없이 주먹구구로 만들다보면 개발자 스스로도 헷갈려서 중복된 API를 만들고 있는 상황이 발생한다.

인터넷의 많은 글에서 REST API를 만들때 리소스 기준을 얘기한다. 리소스 이름은 명사만 사용하고 동사는 사용해서는 안된다.

예를 들어 사용자 정보 조회 기능을 만든다고 생각해 보자. get_users 라는 API를 쉽게 떠올릴 것이다. 그러나 동사 get과 명사 users로 구성된 API는 동사를 사용했기 때문에 REST API에 합당하지 않다. 우리는 명사 users만 사용해야 한다.

명사로 이름지은 API를 만들기 위해서는 서버 자원에 대한 추상화를 잘 해야한다. 데이터베이스를 제대로 설계한다면 이를 그대로 API 이름으로 매칭할 수 있다. user 테이블을 만들었다면 user API를 사용하는 것이다.

우리는 get_users, delete_user, update_user 로 하나의 리소스에 대해 CRUD 작업을 해왔다. 그럼 그동안 우리가 사용했던 동사인 get, delete, update는 어떻게 해야 하는가? **HTTP 상태코드(status code)**를 활용할 수 있다.

웹 페이지의 폼을 구현할때 POST와 GET 메소드를 사용해 왔다.

<form method="post" action="create_user()">
  <input type="text" name="user_name" />
</form>

대부분 Ajax를 사용하는 요즘의 웹 환경에서는 이러한 형식의 폼을 사용하지는 않지만 분명 POST와 GET을 사용해 왔다. 사실 이외에도 HTTP 스펙에는 PUT, DELETE, OPTIONS 등 다양한 메소드가 정의되어 있다. 모바일 API에서는 아래 4가지 메소드만 사용한다.

  • POST: 리소스 추가
  • GET: 리소스 조회
  • PUT: 리소스 변경
  • DELETE: 리소스 삭제

동사 역할을 하는 메소드명과 명사역할을 하는 리소스명은 Method /ResoureName 형태의 API로 만들수 있다 user 리소스는 기본적으로 아래 5가지 API를 만들 수 있다.

  • POST /users: 유저 추가
  • GET /users: 유저 목록 조회
  • GET /users/:id: 유저 1개 조회
  • PUT /users/:id: 유저 1개 수정
  • DELETE /users/:id: 유저 1개 삭제

POST /users

유저 리소스를 추가하는 API다. POST 요청이기 때문에 바디(Body)를 사용할수 있다. 생성할 유저 데이터는 이 바디를 통해 서버로 전달될 것이다.

유저를 1개만 만드는 것인데 왜 user 단수를 사용하지 않고 복수 users를 사용할까? REST API에서는 복수 네이밍을 권장한다.

리소스 생성에 성공하면 201(Created) 상태코드를 리턴한다. 만약 서버에 이미 중복된 리소스가 있을 경우 409(Conflic)를 반환한다. 리소스가 중복되는 경우는 서비스 특성에 따라 다르다. 유저의 핸드폰 번호일 수도 있고 이메일 주소일 수도 있다. 데이터베이스에 UNIQUE 속성을 설정한 상태에서 중복된 리소스가 추가될 경우, 데이터베이스는 에러 실패를 발생시킬 것이다. API 로직에서는 이 에러를 받아서 409 상태코드를 반환하면 된다.

생성된 리소스는 응답 바디에 담아 보내는 것이 좋다. 클라이언트 입장에서 생성된 리소스를 사용해야할 경우가 많은데 달랑 생성된 ID만 응답해 준다면 한번 더 조회 API 호출해야 하기 때문이다.

GET /users, GET /users/:id

단수가 필요한 경우 리소스 식별자인 ID로 구분할 수 있다. 유저 리소스는 고유의 ID를 가지고 있는데 /users/:id로 식별할수 있다. 유저 목록을 조회할 때는 GET /users, 특정 ID를 가진 유저 1개를 조회할 때는 GET /users/:id를 사용하는 것이다.

PUT /users/:id

ID로 유저를 찾아 리소스를 수정할때 PUT /users/:id를 사용한다. 수정할 데이터는 바디에 담아 요청한다. 수정이 완료된 데이터는 POST와 동일한 이유로 바디에 담아 응답해 준다.

DELETE /users/:id

특정 유저 리소스를 삭제할때 DELETE /users/:id를 사용한다. 삭제에 성공했다는 데이터를 {result: "success"} 형식으로 보낼 수도 있지만 204 (No Content) 응답코드를 보내는 것이 더 심플하다. 헤더에 담긴 상태코드에 이미 삭제 성공 메세지가 들어가있기 때문이다.

예외는 없는가?

물론 있다. 여러번의 API 요청을 하나로 만들고 싶을 때가 있다. 여러명의 유저 데이터를 한번에 변경해야하는 상황이 그렇다. 네이밍 일관성을 깨뜨릴 수 밖에 없다. 여러 개 ID를 변경할 데이터와 함께 배열로 바디에 담아 요청한다.

{
  "URL": "PUT /users",

  "BODY": {
    "ids": [1, 2, 3],
    "names": ["Chris", "Marcus", "Daniel"]
  }
}

쿼리스트링을 활용하자

GET /users에 대해 생각해보자. 이것은 보통 모바일 보다는 백오피스에서 사용할 가능성이 크다. 서비스 가입자(users)를 조회할 경우 사용할 것이다.

그럼 이것만으로 충분할까? 백오피스에서 사용자를 조회하는데 10,000명의 유저가 있다면 한 페이지에 모두 보여줄 건가? 만약 검색을 해야한다면 새로운 API를 만들 것인가? 정렬을 해야한다면 ... ?

요즘 나오는 웹프론트 자바스크립트 라이브러리를 사용해도 이러한 기능을 어느정도 구현할 수 있다. 정말 어느정도만 구현할수 있다. 데이터가 너무 많으면 한정된 브라우저에서 다루기에는 버겁다. 서버단에서 최대한 데이터를 가공한 뒤 응답해 주는 것이 전체 서비스 성능에 좋다.

쿼리스트링을 사용하면 하나의 API에 대해 다양한 조건을 설정할수 있다.

  • query: 검색어
  • offset, limit: 페이지네이션
  • sort: 정렬
  • fields: 필드값

이 값에 따라 서버에서는 요청한 만큼의 데이터를 정리해서 보내줄 수 있다.

GET /users?sort=-createdAt&offset=20&limit=10

무슨 의미인지 감이 오는가? 유저를 조회하는데 생성시간 내림차순으로 정렬하되 기존의 20개는 제외하고 10개 유저만 요청한다는 의미다. 하나 더 살펴보자

GET /users?query=chris&fields=name,id

이건 검색 쿼리다. chris로 검색하고 name과 id 필드만 리턴받겠다는 의미다. 이 쿼리스트링은 서버에서 파싱되어 각 각 아래와 같은 디비 쿼리문으로 사용될 것이다.

SELECT *
FROM users
ORDER BY cratedAt
DESC LIMIT 20, 10;
SELECT name, id
FROM users
WHERE name like "%chris%";

상태코드를 활용하자

HTTP는 상당수의 정의된 상태코드가 있다. 모바일 API의 응답 바디에는 실제 데이터를 담고, 헤더의 상태코드는 API 응답의 부가정보를 담는 역할로 사용할 수 있다.

크게보면 2xx, 3xx, 4xx, 5xx가 있다.

  • 2xx: 성공
  • 3xx: 미사용
  • 4xx: 요청 오류
  • 5xx: 서버 오류

2xx

API 요청에 대해 정상적으로 로직이 수행된다면 대부분 200번 코드로 응답한다.

  • 201(Created): 서버에 신규 리소스 생성시 사용한다.
  • 204(No Content): 응답 바디가 없을 경우 사용한다. 리소스 삭제시 사용.

4xx

클라이어트 단에서 요청 파라매터 등의 오류가 있을 경우 400번 코드로 응답한다.

  • 401: Unauthrized
  • 403: Forbbiden

401과 403은 비슷한 성격의 코드다. 인증이 필요한 API에 대해 인증되지 않은 요청일 경우 401을 응답한다. OAuth를 사용할 때 엑세스 토큰(access token)이 유효하지 않을 경우다. 403은 인증시(로그인시) 사용한다. 로그인 정보가 일치하지 않을 경우 403으로 응답한다.

  • 404: Not found. 없이 페이지 뿐만 아니라 없는 리소스일 경우에도 사용한다.
  • 409: Conflict. 자원 추가시 기존 자원과 중복일 경우 사용한다. 위에서 이미 설명함.

5xx

모든 서버 에러에 대해서는 500번 코드를 사용한다. HTTP 스펙에는 다양한 코드들이 정의되어 있지만 서비스 에러코드로 사용하기에는 부족하다. 아니 성격이 좀 다르다. 서버에러가 발생할 경우 상태코드 500을 헤더에 넣고 자세한 에러정보는 바디에 담아 응답한다.

{
  "error": {
    "errorCode": "error code",
    "message": "error message",
    "stack": "stack trace"
  }
}

버저닝

모든 API를 만들었고 모바일 서비스도 출시했다. 앱을 다운받은 사용자들도 있다. 서버단에 기능 추가를 해서 API 로직을 변경해야만 하는 상황을 생각해보자. 서버쪽에서 일방적으로 로직을 변경해버리면 이미 배포된 모바일 앱에서는 크래쉬날 가능성이 크다. 경험상 대부분 크래쉬다.

따라서 API에 대한 버전관리가 필요한다. 기존에는 get_user_v1, get_user_v2로 관리했었고 이렇게 해도 동작하는데 문제없다. 그러나 우리의 포인트는 체계적인 관리다.

  • GET v1/users
  • GET v2/users

이렇게 정리하면 버전별로 계층구조가 생겨 쉽게 관리할 수 있다. 구 버전 앱에서는 GET v1/users를 여전히 사용하고, 새로 업데이트하는 버전의 앱에서는 GET v2/users를 사용하면 된다.

참고