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/1
은 user 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 API 를 보다 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 |
요청이 올바르게 수행되었음 (GET, PUT) |
201 Created |
서버가 새로운 리소스를 생성했음 (POST) |
204 No Content |
응답할 데이터가 없음 (HTTP Body 가 없음). (DELETE, PUT) |
4xx
상태코드 | 설명 |
400 Bad Request |
요청이 잘못되었음. (Error 메시지 등의 에러가 발생한 이유를 같이 표현해야 하고, 특정 코드로 표현한다면 이를 문서에 정리해두고 별도의 필드로 링크 전달.)400 Bad Reqeust {"message": "Parameter is not valid"} 400 Bad Request {"code": -765", "more_info": |
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={ |
필터링을 하는 것은 여러가지 포맷이 존재할 수 있지만, 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 상태코드이며 다른 것은 고사하고 이 부분은 만큼은 반드시 지켜주는 것이 좋을 것이다.