Backend 10

REST에서 자원, Method, Status Code 나누기

/books/delete 같은 주소가 더 쉬워 보이는데 왜 DELETE /books/{id}처럼 나누는지 모르겠다.

근거 · 교안 p61-p68

REST에서 자원, Method, Status Code 나누기 대표 이미지

1장: URI: 자원의 ‘이름’은 오직 그것 하나뿐

솔라는 노트북 화면에 떠 있는 코드 예제를 노려보았다. 모니터 불빛에 비친 미간이 살짝 찌푸려져 있었다. 문득 화면에서 눈을 뗀 솔라가 거실 테이블에 앉아 책을 읽던 언니 루나를 불렀다.

“언니, 잠깐만. 나 진짜 이해가 안 되는 게 있어.”

루나는 책에서 눈을 떼지 않은 채 조용히 고개를 끄덕였다. 계속 말해보라는 신호였다.

“API 주소를 만들 때 말이야. 예를 들어 ‘123번 책을 삭제한다’면, 그냥 주소창에 /books/delete/123이라고 쓰면 누가 봐도 직관적이잖아. ‘책들 중에, 삭제할 건데, 123번이야.’ 딱 떨어지잖아. 그런데 왜 자꾸 DELETE /books/123처럼 주소랑 명령을 분리해서 쓰라는 건지 모르겠어. 행위를 주소에 넣는 게 훨씬 쉬워 보이는데.”

솔라의 목소리에는 진심 어린 답답함이 묻어났다. 더 간단하고 명확한 길이 눈앞에 보이는데, 굳이 돌아가는 길을 택하라고 하는 규칙을 납득하기 어려웠다.

루나는 책을 탁 소리 나게 덮고 테이블 위에 내려놓았다. 그러고는 솔라의 책상 쪽을 턱짓으로 가리켰다. 솔라의 책상 위에는 파란색 머그컵이 놓여 있었다.

“솔라. 저기 네 책상 위에 있는 파란색 컵 있지.”

“응. 저 컵이 왜?”

“저 컵을 나한테 달라고 요청해봐. 단, 조건이 있어. 그 컵의 ‘이름’ 안에 네가 원하는 행동까지 전부 넣어서 한 번에 말해야 해.”

솔라는 잠시 어리둥절한 표정을 지었다. “이름 안에 행동을 넣어서?” 황당한 요구였지만, 솔라는 잠시 고민하더니 입을 열었다.

“음… ‘나에게-건네질-파란-머그컵’?”

말을 내뱉자마자 솔라 스스로도 어색함을 느꼈다. 루나는 아무 말 없이 기다렸다. 솔라는 다른 표현을 찾아 다시 입을 열었다.

“아니면… ‘저기-있는-파란-머그컵-가져오기’라고 불러야 하나? 이상한데.”

“왜 이상하게 들릴까?” 루나가 조용히 물었다.

“그야… 저 컵의 이름은 그냥 ‘파란 머그컵’이니까. ‘가져오기’는 내가 저 컵으로 하고 싶은 행동이지, 컵 이름의 일부가 아니잖아. 만약 내가 저 컵에 물을 채워달라고 하고 싶으면, 그때는 컵 이름이 ‘물을-채워야-할-파란-머그컵’으로 바뀌는 거야? 매번 이름이 바뀌면 그게 어떻게 같은 컵이야.”

솔라는 말을 이어가며 스스로 무언가 깨달은 듯 눈을 동그랗게 떴다.

루나는 그제야 희미하게 미소 지으며 말했다. “바로 그거야. 방금 네가 말한 ‘파란 머그컵’이 바로 자원(Resource)이야. 그리고 웹에서 그 자원을 가리키는 고유한 이름표가 URI(Uniform Resource Identifier)지.”

루나는 테이블 위에 놓인 자신의 책을 손가락으로 가볍게 두드렸다.

“이 책의 이름은 ‘클린 아키텍처’야. 내가 이 책을 읽든, 밑줄을 긋든, 누구에게 빌려주든, 이 책의 이름은 바뀌지 않아. /books/123이라는 URI는 방금 네가 말한 ‘파란 머그컵’이나 이 ‘클린 아키텍처’ 책처럼, 그냥 ‘123번 책’이라는 자원을 가리키는 고유한 이름표일 뿐이야. 그 이상도 이하도 아니야.”

솔라는 아, 하는 작은 탄성을 뱉었다. 머릿속에 흩어져 있던 조각들이 맞춰지는 느낌이었다.

“그럼 /books/delete/123은… 마치 ‘삭제될-123번-책’이라고 부르는 거랑 똑같은 거구나. 이름 자체에 행위가 박제되어 버리는 거네.”

“맞아. 만약 나중에 그 ‘123번 책’의 정보를 수정하고 싶으면 어떻게 할 거야? ‘수정될-123번-책’이라는 새로운 이름표, 즉 /books/update/123이라는 URI를 또 만들어야 할까? 하나의 자원에 대해 행위가 바뀔 때마다 새로운 URI가 생긴다면, 세상의 모든 자원은 수십, 수백 개의 다른 이름을 갖게 될 거야. 너무 복잡하지.”

솔라는 고개를 끄덕였다. 직관적이라고 생각했던 방식이 사실은 시스템 전체를 훨씬 더 복잡하고 관리하기 어렵게 만든다는 것을 깨달았다. URI는 변하지 않는 ‘명사’, 즉 자원의 고유한 이름이어야 했다. 그래야 하나의 이름표를 가지고 다양한 요청을 처리할 수 있었다. 주소가 단순한 경로가 아니라, 세상에 단 하나뿐인 이름표라는 개념이 명확해졌다.

“알겠어. 이제 URI는 오직 자원의 이름만 나타내야 한다는 건 확실히 이해했어. 주소는 명사로 딱 끝내야 하는 거구나.”

솔라는 잠시 생각에 잠기더니 새로운 질문을 던졌다.

“그런데 언니, 이름표에서 ‘삭제하기’ 같은 행위를 떼어내는 것까지는 좋았어. 그럼 그 떼어낸 행위는 어디에 둬야 해? ‘저 파란 컵’ 하고 가리킨 다음에 ‘나에게 줘’라고 말하는 것처럼, URI로 자원을 가리킨 다음엔 어떻게 ‘이걸 삭제해줘’라고 말하는 거야? 그것도 그냥 아무렇게나 말하면 서로 못 알아듣는 거 아니야?”

2장: HTTP Method: 자원에 대한 표준 ‘행위’ 동사

다음 날 오후, 솔라는 여전히 어제와 같은 자리에 앉아 노트북을 들여다보고 있었다. 하지만 화면의 내용은 달랐다. 솔라는 메모장을 열어 어제 깨달은 내용을 자신만의 언어로 정리하고 있었다.

URI = 자원의 고유한 이름표 (명사) - 행위(동사)는 이름표에 포함시키지 말 것. - 예시: /books/123 (O), /books/delete/123 (X)

한 줄을 더 적으려던 솔라의 손가락이 허공에서 멈췄다. ‘그래서, 삭제는 어떻게 하는데?’ 어젯밤 마지막에 던졌던 질문이 다시 머릿속을 맴돌았다.

그때, 책을 읽고 있던 루나가 조용히 자리에서 일어났다. 루나는 어제 솔라의 책상 위에 있던 그 파란색 머그컵을 가져와 거실 테이블 중앙에 놓았다. 그리고는 컵 옆에 작은 카드 뭉치를 내려놓았다. 카드에는 각각 다른 단어가 적혀 있었다.

[가져와] [치워] [새 걸로 바꿔] [색깔 칠해] [안에 뭐 있는지 알려줘]

솔라는 노트북에서 눈을 떼고 루나가 만든 작은 무대를 바라봤다. 루나는 아무 말 없이 솔라를 바라보았다. 의도를 파악한 솔라는 피식 웃으며 카드 하나를 집어 들었다. ‘치워’라고 적힌 카드였다.

“자, 이 컵의 이름표는 이제 /cups/blue라는 거, 알겠어. 그럼 이 ‘치워’ 카드를 같이 보여주면서 ‘/cups/blue를 치워줘’라고 요청하면 되는 건가?”

솔라는 자신의 말을 테스트하듯 카드를 컵 옆에 나란히 놓았다. 제법 그럴듯한 조합이었다.

“만약 내가 요청할 때 ‘치워’ 대신 ‘없애줘’라고 말하면?” 루나가 조용히 물었다.

“그래도 알아듣지 않을까? 뜻은 같으니까.” 솔라는 대수롭지 않게 대답했다.

“내가 아니라, 기계가?”

루나의 짧은 반문에 솔라는 말문이 막혔다. 기계는 사람처럼 유연하게 문맥을 파악하지 못한다. ‘치워’와 ‘없애줘’는 사람에게는 비슷한 뉘앙스지만, 기계에게는 완전히 다른 글자일 뿐이다.

“그럼… 요청하는 쪽이랑 받는 쪽이 항상 똑같은 단어를 쓰기로 미리 약속해야겠네. ‘삭제하기’는 꼭 ‘치워’라고만 말하기로. ‘정보 조회하기’는 ‘안에 뭐 있는지 알려줘’라고만 하기로.”

“모든 자원에 대해서? 모든 행위에 대해서? 세상의 모든 개발자들이?”

루나의 질문이 이어지자 솔라는 상황의 복잡성을 깨달았다. 개발자 A는 삭제를 delete라고 정의하고, 개발자 B는 remove라고 정의하고, C는 erase라고 정의한다면, 이들이 만든 시스템은 서로 소통할 수 없다. 매번 API 명세서를 열어보고 ‘아, 여기선 삭제를 remove라고 부르는구나’하고 확인해야만 한다. 어제 URI에 행위를 포함했을 때와 본질적으로 같은 문제가 발생했다. 끝없는 이름 짓기, 끝없는 약속의 연속이었다.

“말이 안 돼. 그럼 또다시 모든 서비스마다 다른 동사 사전을 외워야 하는 거잖아. 이건 표준이 아니야.”

솔라가 스스로 결론을 내리자, 루나는 솔라가 늘어놓았던 카드들을 옆으로 밀어내고, 주머니에서 다른 카드 네 장을 꺼내 테이블 위에 올려놓았다. 훨씬 간결하고 통일된 모양새의 카드였다.

GET POST PUT DELETE

“그래서 웹을 설계한 사람들은 생각했지. ‘모든 자원에 대한 행위를 그때그때 만들지 말고, 아주 기본적인 네다섯 가지의 표준 행동 양식을 정해두면 어떨까?’ 마치 모든 가전제품의 전원 버튼이 동그라미 안에 선이 그어진 모양으로 통일된 것처럼.”

솔라는 새로 놓인 네 장의 카드를 뚫어지게 쳐다봤다.

“이게 그 표준 행동 양식이구나. HTTP Method.”

“맞아. GET은 자원을 가져오고(조회), POST는 새 자원을 만들고(생성), PUT은 자원을 통째로 교체하고(수정), DELETE는 자원을 없애지(삭제).”

루나는 ‘치워’ 카드를 들어 DELETE 카드 옆에 놓았다. “네가 아까 말한 ‘치워’나 ‘없애줘’는 모두 DELETE라는 표준 행위로 표현될 수 있어. ‘안에 뭐 있는지 알려줘’는 GET으로 표현되고.”

솔라는 아, 하고 탄성을 내뱉었다. 비로소 분리의 의미가 명확하게 다가왔다. URI라는 ‘명사’로 대상을 명확히 지정하고, HTTP Method라는 표준 ‘동사’로 행위를 지시하는 것. 이 두 가지가 분리되어 각자의 역할을 할 때, 비로소 모두가 알아들을 수 있는 일관된 문장이 만들어지는 것이었다.

/cups/blue 라는 이름표는 그대로 둔 채, GET 카드를 옆에 놓으면 컵의 정보를 달라는 요청이 되고, DELETE 카드를 놓으면 컵을 없애달라는 요청이 된다. 자원의 이름은 바뀌지 않지만, 그 자원에 대해 수행할 수 있는 행위는 표준화된 Method를 통해 얼마든지 달라질 수 있었다.

“이제 알겠다. DELETE /books/123이 왜 더 나은 방식인지. /books/123은 그냥 ‘123번 책’이라는 이름표일 뿐이고, DELETE는 ‘이 자원을 삭제하라’는, 전 세계 컴퓨터가 모두 알아듣는 표준어 동사인 거구나. 내가 마음대로 ‘책-삭제하기’ 같은 말을 지어낼 필요가 없는 거였어.”

솔라는 명쾌해진 표정으로 고개를 끄덕였다. 역할의 분리가 가져오는 강력함, 즉 ‘표준’의 힘을 체감한 순간이었다. 이제 URI와 HTTP Method는 솔라의 머릿속에서 완벽히 분리되어 각자의 자리를 잡았다.

문득 솔라의 시선이 다시 테이블 위의 파란 컵과 DELETE 카드 조합에 멈췄다.

“좋아. 내가 서버에게 DELETE /cups/blue라고 요청을 보냈어. 그럼 서버는 내 요청을 받고 컵을 처리하겠지. 그런데… 내가 그 결과를 어떻게 알아? 요청이 성공적으로 처리됐는지, 아니면 컵이 원래 없어서 실패했는지, 또는 내가 컵을 깰 권한이 없었는지는 어떻게 알려주는 거야? 그냥 ‘처리 완료!’ 라고 답장을 보내주나? 이것도 혹시… 뭔가 정해진 약속이 있는 거야?”

3장: Status Code: 행위의 ‘결과’를 명확히 보고하기

솔라의 질문은 다음날까지 이어졌다. 어제 루나가 카드를 늘어놓았던 거실 테이블은 이제 깨끗이 치워져 있었다. 하지만 파란 머그컵과 DELETE 카드는 어젯밤 솔라가 남겨둔 그대로 테이블 한쪽에 놓여 있었다. 솔라는 그 조합을 가만히 내려다보며 어제의 질문을 곱씹었다. ‘요청을 보냈는데, 그 결과는 어떻게 알지?’

루나는 말없이 다가와 테이블 위의 DELETE 카드를 집어 솔라에게 건넸다. 솔라가 카드를 받자, 루나는 파란 머그컵을 들어 찬장으로 옮겨버렸다. 그리고는 솔라가 메모를 위해 꺼내둔 포스트잇 한 장을 가져가 무언가를 슥슥 적더니, 솔라 앞에 가만히 내려놓았다.

포스트잇에는 간결한 두 단어가 적혀 있었다.

[ 처리 완료! ]

솔라는 포스트잇을 보고 피식 웃었다. “이렇게 간단하다고? ‘처리 완료!’라고 알려주면, 내가 ‘아, 컵이 삭제됐구나’ 하고 알면 되는 거네.” 꽤 직관적이라고 생각했다. 성공했으면 성공했다고 알려주고, 실패하면 실패했다고 알려주면 될 일이었다.

“그래?” 루나가 조용히 되물었다. 루나는 솔라가 DELETE 요청을 다시 해보라는 듯 턱짓을 했다.

솔라는 어리둥절했지만, 다시 한번 DELETE 카드를 루나에게 건네는 시늉을 했다. “자, DELETE /cups/blue 요청이야.”

그러자 루나는 이번에도 똑같은 포스트잇, 즉 [ 처리 완료! ]라고 적힌 메모를 솔라 앞에 다시 내려놓았다.

솔라의 표정에 혼란이 스쳤다. “잠깐만. 컵은 아까 이미 치웠잖아. 없는데 뭘 처리했다는 거야?”

“결과적으로 네가 원하던 상태, 즉 ‘파란 컵이 테이블 위에 없는 상태’가 되었으니, 처리가 완료된 것 아닐까?” 루나의 반문은 평온했지만, 그 안에는 날카로운 함정이 숨어 있었다.

솔라는 말문이 막혔다. 분명 컵은 없다. 내가 원하던 결과이긴 하다. 하지만 첫 번째 요청은 ‘성공적으로 컵을 치우는 행위’가 있었고, 두 번째 요청은 ‘원래 없었기 때문에 아무 일도 일어나지 않은 것’이다. 원인은 완전히 다른데, 결과 메시지는 똑같았다.

“아니지. 이건 달라. 첫 번째는 ‘성공’이지만, 두 번째는 ‘이미 없었음’이잖아. 클라이언트 입장에서 ‘처리 완료!’라는 메시지만 받으면, 방금 내 요청으로 컵이 사라진 건지, 아니면 원래부터 없었던 건지 알 수가 없어. 만약 이게 돈을 보내는 요청이었다면? 이미 보냈는데 또 보내려고 시도한 경우랑, 성공적으로 보낸 경우를 구분 못 하면 큰일 나잖아.”

솔라는 자신이 만든 논리의 함정을 스스로 발견했다. ‘처리 완료!’ 같은 인간 친화적인 메시지는 기계 사이의 소통에서 심각한 오해를 불러일으킬 수 있었다.

그제야 루나는 주머니에서 새로운 카드 뭉치를 꺼냈다. 어제의 HTTP Method 카드처럼 간결하고 약속된 단어들이 적힌 카드였다. 루나는 [ 처리 완료! ] 포스트잇을 옆으로 치우고, 새 카드들을 테이블 위에 펼쳐 보였다.

[ 200 OK ] [ 204 No Content ] [ 404 Not Found ] [ 403 Forbidden ]

“그래서 웹의 설계자들은 결과에 대해서도 표준을 만들었어. 요청을 보낸 쪽이 응답 메시지의 문장을 해석할 필요 없이, 약속된 코드를 보고 즉시 결과를 판단할 수 있도록.”

루나는 아까의 상황을 다시 재연했다.

“첫 번째 DELETE /cups/blue 요청. 컵이 있었고, 내가 성공적으로 치웠지. 그럴 땐 이 카드를 주는 거야.” 루나는 [ 204 No Content ] 카드를 솔라 쪽으로 밀었다. “‘성공적으로 처리했고, 응답으로 보내줄 본문 내용은 없음’이라는 뜻이야.”

“그럼 두 번째 요청 때는?”

“두 번째 요청 때는 이미 컵이 없었지. 자원을 찾을 수 없었어. 그럴 땐 이거.” 루나는 [ 404 Not Found ] 카드를 밀었다. “‘요청한 자원을 찾을 수 없음’이라는 뜻이지.”

솔라는 두 개의 숫자 코드를 번갈아 보았다. 204와 404. 완전히 다른 숫자, 완전히 다른 의미. ‘처리 완료!’라는 모호한 단어와는 비교할 수 없이 명확했다.

“만약 저 컵이 언니만 쓸 수 있는 특별한 컵이라서 내가 치워달라고 할 수 없다면?” 솔라가 가설을 세워 물었다.

루나는 미소 지으며 [ 403 Forbidden ] 카드를 가리켰다. “‘서버가 요청을 이해했지만, 승인을 거부함’. 즉, 네가 그럴 권한이 없다는 뜻이야.”

솔라는 무릎을 탁 쳤다. 이제 모든 조각이 맞춰졌다. URI는 자원의 ‘이름(명사)’, HTTP Method는 표준 ‘행위(동사)’, 그리고 이 HTTP Status Code는 그 행위의 ‘결과(서술어)’를 알려주는 표준 보고서였다. 이 세 가지가 각자의 역할을 정확히 분담했기 때문에, 전 세계의 어떤 클라이언트와 서버라도 서로의 언어를 배울 필요 없이 명확하게 소통할 수 있었다. 404 에러 페이지가 그냥 ‘없는 페이지’라는 뜻을 넘어, 시스템 간의 정교한 약속이라는 사실을 깨달았다.

“알겠다… 전부 다 표준이구나. 이름표 붙이는 법, 행동하는 법, 그리고 결과 보고하는 법까지. 전부 다 약속된 방식이 있었던 거야.”

솔라는 자신의 노트북을 앞으로 당겨왔다. 그리고는 새 메모장을 열어 빠르게 무언가를 적기 시작했다. 그녀가 처음 REST API에 대해 가졌던 의문, ‘123번 책을 삭제하는 API’를 이제 자신만의 방식으로 새롭게 설계하고 있었다.

# 도서 관리 API 설계 (v2)

## 1. 특정 도서 삭제

- **요청:**
  - `DELETE /books/123`

- **응답 시나리오:**
  - **성공:** `204 No Content` (책이 성공적으로 삭제됨)
  - **실패 (자원 없음):** `404 Not Found` (123번 책이 원래 존재하지 않음)
  - **실패 (권한 없음):** `403 Forbidden` (사용자가 해당 책을 삭제할 권한이 없음)

솔라는 자신이 작성한 설계 노트를 만족스럽게 바라보았다. /books/delete/123이라는 모호하고 경직된 주소 대신, 이제 그녀의 설계에는 역할이 명확히 분리된 세 개의 표준 약속이 깔끔하게 정리되어 있었다. 이름, 행위, 그리고 결과. 시스템이 소통하는 명료한 문장이었다.