thenullpage.com

동작마다 주소를 만드는 방식

서버에 기능을 붙일 때마다 주소를 하나씩 새로 만드는 방식이 있다. 글을 가져오면 /getArticle, 새로 쓰면 /createArticle, 지우면 /deleteArticle을 만드는 식이다. 이름만 보고 무슨 일을 하는지 알 수 있어 처음에는 편하다. 문제는 기능이 늘어나는 만큼 주소도 늘어난다는 점이다. 비슷한 이름이 겹치고, 새 기능을 붙이려는 사람은 매번 기존 목록을 뒤져 이름 규칙을 짐작해야 한다.


더 큰 문제는 이런 주소들이 대개 POST 하나로 통일된다는 것이다. 주소 이름에 이미 동작이 들어 있으니 메서드는 아무거나 써도 되는 것처럼 보인다. 그러면 중간에 놓인 캐시나 프록시는 그 요청이 조회인지 삭제인지 알 방법이 없다. 전부 위험한 요청으로 취급할 수밖에 없어 캐시도 재시도도 걸 수 없다.


자원이라는 단위

REST는 Representational State Transfer의 줄임말로, 서버가 다루는 것들을 자원이라는 단위로 나누고 그 자원마다 주소를 부여하는 설계 방식이다. 여기서 자원은 기능이 아니라 대상이다. 글, 사용자, 댓글, 주문처럼 이름을 붙일 수 있는 것이 자원이고, 고유한 주소로 식별된다.


/articles
글이라는 자원의 모음
/articles/42
그중 42번 글 하나
/articles/42/comments
42번 글에 달린 댓글 모음


주소에는 대상만 적고 동작은 적지 않는다. 무엇을 할지는 메서드가 말한다. 같은 /articles/42라도 GET으로 부르면 조회이고 DELETE로 부르면 삭제다. 주소는 명사, 동작은 동사라는 역할 분담이라고 보면 된다.


이름에 표현을 뜻하는 Representational이 들어간 이유도 있다. 클라이언트가 주고받는 것은 서버 안에 있는 데이터 그 자체가 아니라 그것을 옮겨 적은 사본이다. 42번 글이 데이터베이스 행 하나로 저장돼 있어도 클라이언트에는 JSON이나 HTML 형태로 전달된다. 자원과 그것을 담아 내보내는 형식은 별개라서, 저장 구조를 바꿔도 주소와 응답 형식은 그대로 유지할 수 있다.


같은 어휘를 계속 쓴다는 것

REST에서 가장 실질적인 이득은 통일된 인터페이스에서 나온다. 자원이 몇 종류든 쓰는 동사는 GET, POST, PUT, PATCH, DELETE로 정해져 있다. 새 자원이 추가돼도 사용법을 따로 배울 것이 없다. 주소만 알면 조회는 GET이라는 사실이 이미 정해져 있다.


GET /articles/42
PATCH /articles/42
DELETE /articles/42


어휘가 고정돼 있으면 중간 장치들도 요청의 성격을 읽을 수 있다. GET은 서버 상태를 바꾸지 않는다고 약속돼 있으므로 캐시가 응답을 저장해 뒀다가 다시 쓸 수 있고, PUT과 DELETE는 여러 번 실행해도 결과가 같으므로 응답이 끊겼을 때 그대로 재시도할 수 있다. 반대로 이 약속을 어기면 사고가 난다. 조회처럼 보이는 GET 주소에 삭제를 얹어 두면, 크롤러나 브라우저의 미리 불러오기가 그 주소를 건드리는 순간 의도치 않은 변경이 일어난다.


요청 하나가 자족해야 하는 이유

REST는 무상태 통신을 전제한다. 서버가 이전 요청을 기억하고 있다고 가정하지 말라는 뜻이다. 각 요청은 처리에 필요한 정보를 스스로 다 담고 있어야 한다.


세 단계짜리 가입 화면을 예로 들면 차이가 분명하다. 서버 메모리에 진행 상태를 쌓아 두는 방식은 1단계와 2단계를 같은 서버가 받아야만 동작한다. 서버를 두 대로 늘려 요청이 다른 쪽으로 가면 진행 상황이 사라진다. 반면 각 단계의 요청이 지금까지의 입력을 함께 보내거나, 서버가 공용 저장소에 기록해 두고 식별값만 주고받으면 어느 서버가 받아도 처리된다.


인증도 같은 원칙을 따른다. 로그인 한 번으로 서버가 그 연결을 기억하는 것이 아니라, 요청마다 신원을 증명할 값을 쿠키나 Authorization 헤더에 실어 다시 밝힌다.


중간에 무엇이든 끼울 수 있는 구조

요청이 자족적이고 메서드의 의미가 고정돼 있으면 클라이언트와 서버 사이에 다른 장치를 끼워 넣을 수 있다. CDN, 로드 밸런서, API 게이트웨이가 그런 장치다. 클라이언트는 지금 최종 서버와 이야기하는지 앞단 캐시와 이야기하는지 몰라도 동작에 지장이 없어야 한다.


이 성질은 클라이언트가 서버 내부 사정을 알아야만 다음 요청을 만들 수 있게 짜 두면 깨진다. 특정 서버의 내부 주소를 응답에 그대로 실어 보내면, 앞에 캐시를 세우거나 서버를 늘리는 순간 클라이언트까지 고쳐야 한다.


응답이 다음 길을 알려 주는 방식

REST 설명에는 하이퍼미디어라는 항목이 따라붙는다. 응답 안에 다음에 할 수 있는 동작의 주소를 함께 실어, 클라이언트가 주소를 미리 알고 있지 않아도 되게 하자는 발상이다.


{
"items": [ ... ],
"links": {
"self": "/articles?page=3",
"next": "/articles?page=4"
}
}


목록의 다음 페이지 주소를 응답에 넣어 주면 클라이언트는 번호 계산 규칙을 몰라도 된다. 서버가 나중에 방식을 바꿔도 받은 주소를 그대로 따라가면 되므로 고칠 것이 없다. 다만 모든 동작을 링크로만 안내하는 완전한 형태는 실무에서 드물다. 링크를 해석하는 로직을 따로 만드는 비용이 얻는 것보다 큰 경우가 많아서, 페이지네이션처럼 값이 자주 바뀌는 자리에만 부분적으로 쓰는 편이다.


원칙과 현실 사이

모든 주소를 명사로 바꿀 수 있는 것은 아니다. 검색, 로그인, 발송처럼 본래 동작에 가까운 기능은 억지로 자원 이름을 지어내면 오히려 알아보기 어려워진다. 이런 자리는 동작을 그대로 주소에 두되 POST로 처리하고, 나머지 대다수 자원은 명사와 표준 메서드로 맞추는 절충이 일반적이다.


POST /articles/42/publish
POST /sessions


중요한 것은 규칙을 몇 퍼센트 지켰는지가 아니라 한 API 안에서 방식이 섞이지 않는 것이다. 삭제가 어디서는 DELETE이고 어디서는 POST /delete인 상태가 가장 나쁘다. 쓰는 쪽이 자원마다 문서를 다시 봐야 하기 때문이다.


확인하는 방법

만들고 있는 API가 자원 중심인지 보려면 주소 목록을 뽑아 놓고 동사가 섞여 있는지 훑는 것이 가장 빠르다. get, create, update, delete, list 같은 단어가 주소 안에 들어 있으면 그 자리는 메서드로 옮길 수 있는 후보다.


명령줄에서는 같은 주소에 메서드만 바꿔 불러 보면 된다.


curl -i https://example.com/api/articles/42
curl -i -X DELETE https://example.com/api/articles/42
curl -i -X OPTIONS https://example.com/api/articles/42


첫 줄과 둘째 줄이 각각 조회와 삭제로 동작하면 주소 하나가 자원 하나를 가리키고 있는 것이다. 지원하지 않는 메서드에는 405 Method Not Allowed가 돌아와야 하고, 셋째 줄의 응답에 붙는 Allow 헤더에는 그 주소가 받아 주는 메서드가 나열된다. 아무 메서드나 200으로 받아 준다면 그 주소는 자원이 아니라 함수 호출구 역할을 하고 있다는 신호다.


무상태인지 보는 방법도 간단하다. 로그인한 상태에서 요청 하나를 개발자 도구에서 복사해 명령줄에서 그대로 실행해 본다. 헤더와 쿠키가 같은데도 실패한다면 서버가 앞선 요청의 흔적에 의존하고 있다는 뜻이다.