해당 포스팅에서는 REST 규칙을 기반으로 기능을 구현하며 겪은 고민과 생각을 공유하고자 합니다.
위에서 다루지 않았지만 REST API 관련 글을 읽다 보면 특정 HTTP Method 중 일부가 충족해야 하는 멱등성이라는 개념이 등장합니다. 여기서 멱등성(Idempotent)이란, 연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질을 의미합니다.
먼저, 간단한 예시를 통해 HTTP 통신에서의 멱등과 비멱등 개념을 살펴보겠습니다.
서버에서 공지사항 정보를 다음과 같은 notice 테이블에서 관리하고 있다고 가정하고, id가 20에 해당하는 공지사항 정보를 조회하는 API를 연속으로 호출해 보겠습니다.

# request 1, 2
[GET] /notices/20
# response 1, 2
200 OK
...
"result" : {
"notice" : {
"id" : 20,
"title" : "4월 13일 서비스 점검 안내",
"content" : "안녕하세요. 4월...",
"viewCount" : 30,
"createdDate" : "2021-04-09T14:57:44",
"modifiedDate" : "2021-04-10T14:57:53"
}
}
두 번의 동일한 요청에 대해 동일한 응답 결과를 받았다는 것을 확인할 수 있습니다. 어떻게 보면 당연한 결과입니다. 클라이언트는 단지 서버에서 관리하고 있는 공지사항이라는 자원을 단순히 획득하는 것이 목적이었고, 그렇기 때문에 GET Method를 사용한 API를 선택한 것입니다. 해당 notice 테이블의 id = 20인 row 데이터가 중간에 변경되지 않는 이상, 같은 요청을 수 백 번 시도하더라도 응답 결과는 바뀌지 않을 것입니다.
바꿔 말하면, 클라이언트 측에서 [GET] /notices/20 요청을 보낸 후 서버의 응답을 읽기 전에 장애가 발생하더라도 동일한 요청을 다시 보낸다면 같은 결과를 기대할 수 있다는 것입니다. 이러한 경우를 바로 '멱등'으로 간주합니다.
이번에는 클라이언트가 Request Body에 제목과 내용을 담아 공지사항 자원을 생성(등록)하기 위한 API를 연속으로 호출해 보겠습니다.
# request 1, 2
[POST] /notices
{
"title" : "홍길동의 공지사항",
"content" : "내용은 없습니다."
}
# response 1
200 OK
...
"result" : {
"notice" : {
"id" : 21,
"title" : "홍길동의 공지사항",
"content" : "내용은 없습니다.",
"viewCount" : 0,
"createdDate" : "2021-04-14T15:56:05",
"modifiedDate" : null
}
}
# response 2
200 OK
...
"result" : {
"notice" : {
"id" : 22,
"title" : "홍길동의 공지사항",
"content" : "내용은 없습니다.",
"viewCount" : 0,
"createdDate" : "2021-04-14T15:56:06",
"modifiedDate" : null
}
}
서버에서 두 번의 동일한 요청에 의해 생성된 리소스를 응답 결과로 내려주었는데, id와 createdDate 값이 각각 다르게 생성되었다는 것을 알 수 있습니다. 내부적으로 테이블에서 Auto Increment 되는 Key값인 id와 리소스가 생성되는 시점으로 시스템 시간이 저장되는 created_date가 각 요청마다 별도 생성된 것입니다. 계속해서 같은 요청을 시도한다면 대략 아래와 같은 형태로 서버측 DB에 리소스가 생성될 것입니다.
이처럼 해당 요청을 반복하게 된다면 내부적으로 자원은 계속해서 추가 될 것이고, 그때마다 서버에서 각각 다른 응답을 반환합니다. 이러한 경우를 바로 '비멱등'으로 간주합니다.
멱등성은 요청의 효과를 보고 판단합니다. 서버의 상태는 멱등성이 유지되어야 하는 경우, 같은 행위를 여러 번 반복하더라도 같은 효과를 가져야 합니다. 다시 말해, 특정 Method의 요청을 한 번 요청했을 경우나 여러 번 요청했을 경우의 결과가 같다면
'멱등'으로 간주하고, 요청마다 다른 결과가 발생된다면'비멱등'하다고 할 수 있습니다.
일반적으로POST를 제외한GET,PUT,DELETE같은 HTTP Method는 멱등성을 보장합니다.
참고로PATCH의 경우 비멱등성을 가진다고 명확하게 구분 짓는 의견이 대부분이지만, 멱등/비멱등 둘 다 가능하다고 보는(구현하는 개발자의 몫) 의견도 있습니다.

그렇다면 여태까지 살펴본 REST 설계 가이드를 따라 아래와 같은 요구사항을 해결해 보겠습니다.
사용자가 공지사항 상세 조회 요청을 할 때마다, 해당 공지사항의 조회수 1 증가
한 줄로 마무리 되는 복잡하지 않은 요구사항입니다. 공지사항이라는 리소스를 조회하고, 해당 공지사항의 조회수를 1 증가시킨 값으로 수정하는 과정이 머릿 속에 벌써 그려질만큼 단순한데요.
(단, 공지사항에 종속되어 누가/언제 조회했는지에 대한 부가 정보 등은 따로 관리하지 않는다고 가정)
이 과정을 HTTP 통신으로 처리하고자 할 때, 다음과 같은 요청들을 통해 동작하도록 API를 설계할 수 있겠습니다.

# request 1
[GET] /notices/20
# response 1
200 OK
...
"result" : {
"notice" : {
"id" : 20,
"title" : "4월 13일 서비스 점검 안내",
"content" : "안녕하세요. 4월...",
"viewCount" : 30,
"createdDate" : "2021-04-09T14:57:44",
"modifiedDate" : "2021-04-10T14:57:53"
}
}
먼저 클라이언트 측에서 id가 20번에 해당하는 공지사항 정보 조회를 요청하고, 해당 리소스를 획득합니다.
이제 20번 공지사항의 조회수가 현재 30이라는 것을 클라이언트 측에서 알게 되었고, 요구사항에 맞춰 조회수를 1 증가시켜줘야 하니 수정 요청을 진행합니다.
# request 2
[PATCH] /notices/20
{
"viewCount" : 31
}
# response 2
200 OK
수정 요청에 대한 응답까지 OK로 떨어진 것을 확인했으니, 필요한 과정은 모두 수행되었다고 볼 수 있습니다. 이처럼 클라이언트가 공지사항 자원을 획득하고 해당 공지사항의 조회수가 1 증가하는 동작을 REST의 설계 원칙을 성실히 지킨 두 개의 API로 구현할 수 있습니다.
하지만 이 방법은 몇 가지 문제점을 가지고 있습니다.
첫째,클라이언트 측에서 공지사항 자원을 획득한 시점의 viewCount는 증가하기 전의 값인30입니다. 실제 사용자의 서비스 화면에서 본인 조회 내역이 반영된 조회수 값인31이 노출되기 위해서는 획득한 viewCount를 프론트 단에서 임의로 늘려주는 등의 추가 작업이 필요하게 됩니다.둘째,클라이언트는 단지GET요청을 통해 20번 공지사항의 상세 정보를 획득하고 싶을 뿐입니다. 그런데 이 시점에조회수 1 증가라는 내부 동작을 위해PATCH요청까지 클라이언트가 맡게 되었고, 이 동작에 대한 책임까지 클라이언트가 부담하게 되는 아이러니한 상황에 놓이게 됐습니다.
따라서GET요청에 대한 응답은 정상적으로 들어왔는데PATCH요청 과정에서 장애가 발생했을 경우와 같은 대처를 클라이언트가 처리해야 합니다.셋째,사용자의 공지사항 조회가 일어날 때 호출되는 API가2개로 늘어나면서 통신 횟수 또한2배로 늘어나게 됩니다. 10만 회 일어났을 때 20만 건의 API 요청, 500만 회 일어났을 때 1000만 건의 요청이 발생합니다. 공지사항 리소스 조회 요청이 많아지면 많아질수록, 분리된 API 두 개가 눈엣가시처럼 여겨질 것입니다.
사실 첫 번째 문제는 API 호출 순서를 서로 바꾸면 간단히 해결됩니다. 조회수 증가 요청을 먼저 보낸 뒤, 이것이 반영된 상태의 공지사항 리소스를 받아오면 되는 것이죠. 다만, 이 경우에는 클라이언트가 공지사항의 현재 조회수를 모르는 상태에서 조회수 증가 요청을 해야 합니다.
결국, 요청이 들어왔을 때 서버 내부에서 notice 테이블 내 id가 20인 view_count 컬럼을 1 증가시키는 자체적인 처리가 필요합니다. 이 부분은 URI 마지막에 increase(Controller)라는 행위를 따로 명시한 POST API로 표현했습니다.

API 호출 순서를 위와 같이 변경하고 나니 첫 번째 문제를 해결할 수 있었습니다. 하지만 아직 두 번째와 세 번째 문제가 해결되지 않았네요. 세 가지 문제를 한 번에 해결하기 위한 방법은 의외로 간단합니다. 클라이언트의 주 관심사인 [GET]/notices/20 요청이 들어올 때, 아래와 같이 서버 내부적으로 조회수를 증가시키는 과정이 동반되면 됩니다.

이처럼 분리된 API의 두 가지 동작을 GET 요청 하나가 들어올 때 내부적으로 모두 처리되도록 구현했을 때 어떤 변화가 생기게 되는지 알아보겠습니다.
- 이제 클라이언트는 공지사항 리소스 획득이라는 본연의 목적에만 초점을 맞추면 됩니다.
- 응답 결과가 사용자 본인의 조회 내역이 반영된 리소스인지 미반영된 리소스인지 신경 쓸 필요가 없습니다.
조회수 1 증가라는 내부 동작에 대한 장애가 발생하더라도 이는 서버에서 처리할 영역이 된 것이죠. 응답 결과는 서비스 요구사항에 맞춰 구현된 로직을 거쳐 나온 결과일 테니, 클라이언트는 이를 신뢰하고 사용자 화면에 노출시키면 됩니다.- 클라이언트와 서버의 통신 횟수가 반으로 줄었습니다.
조회수 1 증가에 대한 처리를 서버 내부에서 수행하기 때문에 클라이언트는 리소스 획득에 대한 요청만 보낼 뿐, 리소스 변환에 대한 어떠한 요청도 보낼 필요가 없습니다.
그렇다면 이제 모든 문제가 해결됐다고 볼 수 있을까요? 안타깝지만 놓친 부분이 하나 있습니다. 바로 앞서 살펴보았던 멱등성 개념입니다. POST를 제외한 GET, PUT, DELETE같은 HTTP Method는 멱등성을 보장해야 한다고 했죠. 하지만 서버 내부에서 조회수 증가 처리가 수행될 경우, [GET]/notices/20와 같은 요청에 따른 결과(조회수)는 매번 다를 것이고 이는 비멱등하다고 할 수 있습니다.
모든 REST 설계 규칙을 철저히 지키며 구현하자니 복잡도가 커지고(API 개수 증가 및 클라이언트 부담 증가), 이를 해결하려 일부 후처리를 서버에서 수행했더니 결과적으로 느슨한 느낌의 REST API(GET 방식이지만 비멱등)가 된 것을 알 수 있습니다.
이처럼 Restful한 API를 완벽하게 구현하는 것은 어려운 일이 아닐까 싶습니다. 앞서 소개한 개념을 제외하고도 파고들면 수많은 제약사항과 규칙이 존재하기 때문이죠. 생각보다 번거로운 사항도 많습니다. 때문에 투입 비용 대비 이점이 그렇게 크지 않다는 의견도 있습니다.
하지만 REST는 자원을 중심으로 동작을 구분 짓고, 누구나 내용을 파악하기 쉽게끔 도와준다는 점에서 가치가 크다고 생각합니다. 서비스가 커질수록 투입되는 인원은 많아지고 사용되는 API 개수도 늘어나겠죠. 내부적으로 REST와 같은 가이드를 일정 범위 참고하여 제시한다면 여러 사람이 하나의 서비스를 일관성 있게 구현하는 데 큰 도움을 줄 것입니다.
마지막으로 REST와는 별개로 조회수 증가 기능은 서비스별로 구현하는 방법도, 해결하는 부분도 제각각이기 때문에 생각하기 좋은 주제인 것 같습니다.
예를 들어 유튜브와 같이 조회수가 수익으로 직결되는 정보 서비스의 경우, 어뷰징에 대해 민감할 테니 특정 사용자가 조회수를 조작할 수 없게 보안적인 측면을 중점적으로 고려할 수 있겠습니다.
또한 수많은 트래픽이 실시간으로 몰리는 서비스의 경우, 조회가 일어날 때마다 자원을 변경한다면 DB 부하가 상당할 테니 자원 낭비를 최소화하는 방법을 중점적으로 고민할 수 있겠습니다.
사실 이 주제는 첫 직장에 입사하고 막 서비스 개발을 진행하며 고민했던 내용입니다. 글을 마치는 시점에서 벌써 3년 전 이야기네요. 현재는 웹 서비스 개발과는 거리가 있는 분야로 이직하기도 했고 현생이 바빠 포스팅을 계속 미뤄뒀는데 지금이라도 기억을 더듬어 가며 끝맺으니 홀가분합니다. 앞으로 틈틈이 공부하는 내용을 기록해야지, 다짐하며 마치겠습니다.