'REST' 를 보다 'RESTful' 하게 API 만들기

'REST' 를 보다 'RESTful' 하게 API 만들기

REST API

인증 파트가 어느정도 마무리되고 써볼 글은 API 에 대한 이야기다. 가장 처음, REST(Representational State Transfer) API 에 대한 이야기를 해보고자 한다. REST API 는 SPA(Single Page Application) 방식으로 개발된 프론트엔드에서 백엔드의 데이터를 가져올 때 가장 많이 사용되는 자원(Resource) 처리방식이다. REST API 를 따르는 API 가 세상에는 많이 존재하고 있으므로 몇 가지 규칙만 알고있으면 다른 서비스의 API 를 사용하는 것에 거부감을 느끼지는 않을 것이다.

 

REST API 의 가장 두 가지 특징으로는 URI자원(Resource)을 요청하여 특정 형태로 표현(Representation)한다는 것과 HTTP Method 를 적극적으로 활용하여 행위(Verb)를 나타낸다는 점이다. 예를 들어 GET /users 는 모든 user 의 정보를 응답으로 달라는 이야기가 되며 DELETE /user/1user id: 1 에 해당하는 user 를 제거하라는 의미가 된다. REST API 의 요청으로 나오는 응답은 대체로 JSON(Javascript Object Notation)으로 표현되므로 따라서 사람이 읽기 쉽다.

 

REST API 는 무상태(Stateless) 환경에서 동작하는 것을 전제로 한다. 따라서 보안 및 인증에 대해서는 JWT(JSON Web Token), OAuth 와 같은 토큰 인증이 사용되며 상태 유지를 위한 세션(Sessions)은 사용하지 않는다. 또한 보안을 위해서 HTTPS 를 사용하는 것을 잊어서는 안 된다. HTTP 요청/응답의 단순한 사이클을 거치기 때문에 프론트엔드가 어떤 환경에서 동작하는지에 대한 의존도가 낮으며 자원을 자주 조회해야 한다면 ETag, Last-Modified 헤더를 통한 캐싱(Caching)도 가능하다. 

 

이러한 REST API 를 설계할 때 강제하는 표준은 없지만, 보다 'ful' 하게 설계하는 방법은 분명하게 존재한다. 따라서 이 포스트에서는 REST API 를 보다 'ful'(RESTful) 하게 만들 수 있도록 가이드를 제공한다.

'REST' 를 보다 'RESTful' 하게

REST API 는 기본적으로 URI 로 자원을 표현하고, 자원에 대한 행위는 HTTP Method 를 사용한다는 점이다. 이 부분은 핵심적인 부분이다. 따라서 다른 부분보다 가장 우선시 해야 하고, 일부는 선택적으로 제공한다. 실제로 REST API 를 제공하는 이름이 알려진 서비스들도 규칙을 완전히 준수하는 것은 아니며 기능을 제공하지 않는 경우도 많다.

URI 규칙

REST API 가 과연 'ful' 한지 볼 때 처음 살펴보면 좋은 것은 자원에 대한 URI 규칙이 제대로 적용되었는지 살펴보는 것이다. 아래의 사항이 지켜졌는지 살펴볼 필요가 있다.

규칙 나쁨 좋음
마지막이 / 로 끝나서는 안 된다. http://api.test.com/users/ http://api.test.com/users
_ 대신 - 를 사용한다. http://api.test.com/tag/rest_api http://api.test.com/tag/rest-api
소문자로 구성한다. http://api.test.com/tag/REST-API http://api.test.com/tag/rest-api
동사(Verb)는 포함하지 않고, HTTP Method 로 대체한다. POST http://api.test.com/delete-user/1 
POST http://api.test.com/delete/user/1
DELETE http://api.test.com/user/1
파일 확장자 표시하지 않기 http://api.test.com/users/1/profile.png http://api.test.com/users/1/profile (Accept 사용) 

Accept: image/png

리소스 관계 표현하기

자원 간 관계가 있는 경우가 많다. http://api.test.com/users/1/posts 처럼 표현할 수 있는데, 예를 들어 한 명의 유저는 다수의 게시글을 가지고 있는 경우가 대표적이다. 이 경우 경로 상에서 / 를 사용하여 표현할 수 있는데, 예시에서 users/1 는 다수의 유저 중에서 id: 1 에 해당하는 유저를 나타내고, 그 내부에 있는 posts 는 해당 유저에 속한 게시글들을 의미한다.

 

경로에서 posts 와 같이 복수형으로 쓰인 부분은 같은 계열의 여러 자원을 가지고 있는 컬렉션(Collection)이라하고, 1 과 같이 숫자로 쓰여있거나 단수형으로 나타낸 경우 컬렉션에 포함한 자원 중 하나를 나타내는 도큐먼트(Document)라고 한다. 다른 일부 권고사항이 설령 지켜지지 않더라도 URI 규칙의 준수여부에 따라 RESTful 인지 아닌지 분명하게 구분이 가능하다.

Header 설정하기

헤더를 설정하는 부분은 필수적이지는 않지만, 응답으로 돌려줄 때 프론트엔드에서 참고하기 좋을 것이다.

헤더 설명
Content-Location 예를 들어 POST /posts {"title": "Hello, world"} 와 같은 요청으로 유저를 생성했을 때 Content-Location 을 사용하여 리소스의 위치를 표현할 수 있다. 리소스 생성으로 발생한 자원 위치는 항상 다를 것이기 때문이다. 이 부분은 아래에서 나올 HATEOAS 으로 대체할 수 있다.

Content-Location: /posts/1
Content-Type 서비스에 따라 xml 을 제공하는 경우도 있지만, 어지간하면 json 으로만 사용하여 API 사용자가 일관성있게 사용할 수 있도록 유도하자.

Content-Type: application/json
Retry-After API 요청을 너무 많이 한 경우 429 Too Many Requests 응답과 함께 사용한다. 이 부분은 공격자가 API 서버를 사용하여 서비스에 과부하를 일으키려 할 때 조금이나마 보호막이 되어줄 수 있다. 물론, 이 기능만으로 네트워크에서 오는 공격을 막을 수만은 없을 것이다.

Retry-After: 3600

HTTP 메서드 적극적으로 사용하기

이 부분은 URI 규칙과 함께 가장 중요한 부분 중 하나이며 지켜져야 한다. REST API 는 URI 에 동사를 직접 명시하는 대신 HTTP Method 로 무엇을 할지 명시한다. 가장 많이 사용되는 메서드는 GET, POST, PUT, DELETE 이며 PUT 대신 PATCH 를 사용하는 경우도 있으고 완성도 높은 API 제공을 위해 추가적으로 OPTIONS, HEAD 를 제공하기도 한다.

/posts GET POST PUT DELETE
모든 포스트를 얻어온다. 새로운 포스트를 생성한다. 405 Method Not Allowed 모든 포스트를 제거한다.
/posts/1 GET POST PUT DELETE
id: 1 에 해당하는 포스트를 얻어온다. 405 Method Not Allowed id: 1 에 해당하는 포스트를 수정한다. id: 1 에 해당하는 포스트를 제거한다.

위의 표에서 PUT 은 전체 자원을 수정하기 위해 사용한다, 하지만 언급되지 않은 PATCH 는 일부 자원을 수정하기에 적합한 메서드다. 물론, 이것이 강제되는 것은 아니다. 그 외에 OPTIONS 는 자원에 대해 사용가능한 메서드를 반환하고, HEAD 는 Body 는 제외한 Header 만 반환한다.

Form

예를 들어 Post 에 대한 CRUD(Create, Read, Update, Delete)작업이 있다고 생각해보자. 이러한 작업은 HTML Form 을 통해 구성되는 경우가 많아서 뷰와 함께 제공되곤 한다. Form 을 포함한 구성 또한 URI 규칙을 통해 RESTful 하게 구성이 가능하다.

HTTP 메서드 URI 설명
GET /posts 모든 포스트를 조회한다.
GET /posts/create 포스트를 생성하기 위한 Form
POST /posts 포스트를 생성한다.
GET /posts/:id 포스트를 조회한다.
GET /posts/:id/edit 포스트를 수정하기 위한 Form
PUT/PATCH /posts/:id 포스트를 수정한다.
DELETE /posts/:id 포스트를 삭제한다.

메서드 스푸핑

다만 여기서 의문이 들 수 있는 부분은, HTML Form 의 경우는 일반적으로 GET, POST 만을 지원하고 PUT/PATCH, DELETE 는 지원하지 않는데, 어떻게 지원하지 않는 메서드를 사용하여 서버에 전달하냐는 것이다. 이를 처리하는 것은 프레임워크의 구현마다 다를 수 있는데, _method 라는 이름을 가진 필드에 메서드의 이름을 전달하여 서버가 해당 메서드인 것처럼 인지하도록 속인다. 메서드 스푸핑(Method Spoofing)이라 한다.

<form method="POST" action="/posts/1">
	<input type="hidden" name="_method" value="PUT"> 
</form>

HTTP 상태코드

RESTful API 를 구성하면서 고민하는 부분 중 하나는 자원을 생성하거나, 수정, 삭제했을 때 어떤 상태코드로 반환하냐는 것인데, 아래의 표를 참고하면 그에 대한 해답을 얻어낼 수 있다.

2xx

상태코드 설명
200 OK 요청이 올바르게 수행되었음.
201 Created 서버가 새로운 리소스를 생성했거나 수정했음. (POST, PUT)
204 No Content 리소스가 더 이상 존재하지 않으므로 응답할 데이터가 없음 (HTTP Body 가 없음). (DELETE)

4xx

상태코드 설명
400 Bad Request 요청이 잘못되었음. (Error 메시지 등의 에러가 발생한 이유를 같이 표현해야 하고, 특정 코드로 표현한다면 이를 문서에 정리해두고 별도의 필드로 링크 전달.)

400 Bad Reqeust {"message": "Parameter is not valid"}
400 Bad Request {"code": -765", "more_info":
https://api.test.com/errors/-765"}
401 Unauthorized 인증이 진행되지 않았음. 로그인이 되지 않은 경우를 말한다.
403 Forbidden 권한이 인가되지 않았음. 로그인이 되었으나 해당 자원에 대한 처리에 대한 권한이 없음을 말한다.
404 Not Found 자원을 찾을 수 없음. /posts/2 라는 요청을 했을 때 그에 해당하는 자원이 존재하지 않는 경우 반환.
405 Method Not Allowed 자원에 대한 특정 메서드를 지원하지 않음. 예를 들면, POST /posts/1 를 요청한 경우에 해당한다. 자원을 찾을 수 없다는 404 Not Found 와 헷갈려서는 안 된다.
409 Conflict 비지니스 로직상 요청을 처리하지 못한 경우.
429 Too Many Requests 요청을 너무 많이한 경우 (Retry-After 와 함께 사용)

5xx

500 과 관련된 에러는 서버 내부에서 발생한 에러이므로 클라이언트에 표시되지 않도록 해야한다. 파라매터의 필수값 등을 확인하고 유효한지 확인해야한다.

HATEOAS

HATEOAS(Hypermedia as the Engine of Application State)는 단어 자체를 처음 들으면 무슨 말인지 잘 모르겠지만, 간단하게 이야기하자면 어떤 자원에 대해 처리를 했을 때, 그에 대한 응답으로 프론트엔드에서 처리하기 용이하도록 자원에 대한 Links 함께 제공하자는 이야기다. Content-Location 을 대신할 수 있다.

 

예를 들어 POST /posts {"title":" Hello, world"} 요청을 보냈다고 가정해보자. 이 요청은 새로운 포스트를 만들라는 의미이며 아래와 같이 링크정보를 포함하여 응답할 수 있게된다. 이를 사용하면 RESTful API 스타일을 따르는 자원 접근방식을 통해 클라이언트가 자원을 조작할 수 있게되고 클라이언트에서 URL 을 직접 구성할 필요가 사라진다. 그저 응답에서 제공하는 정보만 참조하면 되기 때문이다.

201 Created
{
    "id": 1,
    "title": "Hello, world",
    "createdAt": "2021-08-14 12:00:00",
    "links": [
        {
            "rel": "self",
            "href": "http://api.test.com/posts/1",
            "method": "GET"
        }
    ]
 }

rel 부분은 self 를 제외하곤 임의로 정해서 사용할 수 있으며 href, method 이외에도 더 자세한 정보를 제공하는 링크인 more_info, 요청 예시를 제공하는 body 등을 사용할 수 있다. 이는 정해진게 아니며 내부적으로 약속에 의해 추가될 수 있다. 또한 링크를 표현할 때는 일관성있게 표현해야 한다.

페이징

REST API 를 통해 페이징을 처리하는 것은 자주 있는 일이다. GET /users 를 조회함에 있어 모든 포스트가 다 조회된다면 데이터가 많을수록 응답을 하기에 아주 오랜시간이 걸릴 것이다. 이러한 페이징을 처리할 때에는 Link Header 를 사용하거나 HATEOAS 를 사용하여 표현하는 것이 좋은 방법이다. 페이징 요청을 던지기 위한 예제 포맷은 아래와 같다.

예제포맷 설명
/users?offset=0&limit=10 SQL 문법에서 사용하는 OFFSET, LIMIT 를 그대로 사용하여 보다 손쉽게 작성할 수 있다는 것이 장점. 요청할 때 처음시작 부분을 지정하여 요청을 하므로 어플리케이션 내부에서는 어디까지 페이징을 했는지 계산하지 않는다는 점이 특징이다.
/users?limit=10 어플리케이션 내부에서 어디까지 페이징을 했었는지 계산하고 추가적인 요청이 있을 때 LIMIT 에 대한 것 까지만 응답한다.

표에 나와있는 것처럼 요청하면 아래와 같은 응답을 던져줄 수 있다. Link Header 와 HATEOAS 를 둘 다 사용하여 표현한 것이다. 둘 중에 하나만 사용해도 상관없다.

200 OK

Link: <https://api.test.com/users?offset=10&limit=10>; rel="next",
 <https://api.test.com/users?offset=50&limit=10>; rel="last",
 <https://api.test.com/users?offset=0&limit=10>; rel="first",
 <https://api.test.com/users?offset=0&limit=0>; rel="prev",
{
    {1, ...},
    {2, ...},
    ...
    {10,...},
    "links": [
        {
            "rel": "next",
            "method": "GET",
            "link": "https://api.test.com/users?offset=10&limit=10"
        },
        {
            "rel": "last",
            "method": "GET",
            "link": "https://api.test.com/users?offset=50&limit=10"
        },
        {
            "rel": "first",
            "method": "GET",
            "link": "https://api.test.com/users?offset=0&limit=10"
        },
        {
            "rel": "prev",
            "method": "GET",
            "link": "https://api.test.com/users?offset=0&limit=0"
        }
    ]
}

정렬, 필터링, 필드 선택

여기에서는 REST API 에서 GET 요청에 대해 자원을 정렬하고, 필터링하고, 선택하는 방법에 대해 이야기해본다. 응답의 결과는 SQL 로 데이터베이스와 요청한 것과 유사하다. 물론 이부분은 꼭 정해져있는 포맷이라기 보다는 구현에 따라 달리 표현할 수도 있다.

종류 예제포맷 설명
정렬 /posts?order=-title, id
/posts?sort_by=title&order=desc
/posts?sort=title:desc, id:asc
- 부호가 붙어있는 것은 내림차순하고, 그 외에는 오름차순으로 정렬. 그 외에 다른 표현으로 사용할 수 있다.
필터링 /posts?filter=id: 1 AND title: Hello, world
/posts?id=1&title=Hello, world
/posts?id[eq]=1&title[eq]=Hello, world
/posts?filter={
  "id": {"op": "=", "value": "1"},
  "title": {"op": "=", "value": "Hello, world"}
}
필터링을 하는 것은 여러가지 포맷이 존재할 수 있지만, Query 와 비슷한 형태로 제공할 수 있다면 다채롭게 사용자에게 검색기능을 제공할 수 있다.
필드 선택 /posts?-fields=title
/posts?fields=title, id
fields 파라매터에 나열된 필드만 응답하고, -fields 로 표현된 것은 응답에서 제외하고 응답. 존재하지 않는 필드를 요청하는 경우 무시한다.

버전 관리하기

REST API 의 버전을 사용자에게 명시하기 위한 방법으로는 URL, Accept Header, Accept-Version 로 처리하는 세 가지 방법이 있다. 

종류 예제포맷
URL http://api.test.com/v1
http://service1.test.com/api/v1
Accept Accept: application/vnd.test.v1+json
Accept: application/vnd.test+json;version=1.0
Accept-version Accept-version: v1

만약 URL Versioning 을 사용하여 구성할 때 어플리케이션 라우트에 v1, v2 와 같은 버전을 명시해서 처리하기 보다는 http://api.test.com/v1 에 대한 요청이 들어오면 버전에 해당하는 API 서버로 연결해서 요청하는 것이 좋다. 이를 때면 http://api.test.com/v1 요청에 대해서는 http://127.0.0.1:3001 서버로 요청한다. 따라서 어플리케이션 라우트에는 v1, v2 와 같은 버전에 대한 내용이 들어가있지 않고, 이러한 버전은 Git 에서 Branch 를 통해 관리하는 것이 좋다. 

마치며

여기까지 REST API 를 보다 'ful' 하게 작성하는 가이드를 적어보았다. REST 를 RESTful 로 만들기 위한 분명한 표준(Standard)은 없다. 그저 스타일이기 때문이다. 따라서 이를 제공하는 문서를 명확히 정리하는 것이 좋은 방법이다. RESTful API 를 만들 때 기억해야 할 것은 URI 를 사용하여 자원을 나타내고 XML, JSON 등의 포맷을 통해 표현하고 행위는 HTTP 메서드로 나타낸다는 점이다.

 

그저 GET, POST 두 가지만으로 Query Parameters 를 사용하여 PUT/PATCH, DELETE 를 표현하는 것은 상당히 안티패턴이므로 각별히 주의해야 한다. 언급한 것들 중, REST API 를 작성할 때 지켜주면 좋은 부분은 URI 규칙, HTTP 메서드, HTTP 상태코드이며 다른 것은 고사하고 이 부분은 만큼은 반드시 지켜주는 것이 좋을 것이다.