삭제 API인데 왜 POST였을까? - http method과 멱등성에 대한 고찰
POST vs. DELETE, 언제 뭘 써야 할까
들어가는 글
백엔드 실무를 하면서 회사의 API 중 특이하다고 여겨진 API들이 있었다. 리소스 삭제를 위한 API인데 DELETE 메소드를 사용했을 거라는 예상과는 달리 POST로 구현되어 있던 것이다. ‘이유는 있을텐데?’라는 생각에 좀 더 설계 관점에서 확인을 해보게 되었다.
실제로는 삭제 API에선 두 가지 다 흔히 쓰이는 패턴이었다. 그럼 언제 이를 구분 지을까? 여기에는 ‘멱등성’이라는 개념을 함께 알아야 했다.
이 글은 RESTful API 설계 시 사용할 메소드와 멱등성의 관계를 설명하고, 삭제 로직의 API라도 DELETE와 POST가 언제 쓰이는지 구분을 짓는 경계를 설명하고자 한다. 다음에 해당하는 사람에게 이 글이 도움이 되고자 한다.
- 당장 RESTful API 설계를 해야 하는 사람
- 회사에서 사용하는 삭제 API의 메소드가 섞여 있어 설계 기준을 세우고 싶은 사람
- 멱등성이라는 개념이 뭔지 궁금한 사람
멱등성이란
멱등성(Idempotency)은 같은 요청을 여러 번 보내도 결과가 한 번 보낸 것과 동일한 성질을 말한다. 즉 동일한 요청을 여러 번 보내면 DB의 상태가 계속 변경이 되는가 vs. 동일한 요청을 여러 번 보내도 한 번 보낸 것과 **같이 DB의 데이터가 한 번 바뀌고 끝나는가**로 구분하며 후자의 경우는 ‘멱등하다’라고 말한다.
또한 멱등성은 "서버가 요청을 한 번만 처리한다"는 뜻이 아니다. 요청은 여러 번 처리될 수 있어도, 그 결과 상태가 동일하게 유지된다는 게 핵심이다.
예를 들어 POST /api/users 에 request body {"name": "John"}을 두 번 보내면,
- 첫 번째 요청: 이름이 John인 사용자가 생성됨(id=1)
- 두 번째 요청: 이름이 John 사용자가 또 생성됨(단 id=2)
따라서 두 번 요청하면 한 번 요청할 때와 달리 데이터가 두 개 생긴다. 이는 멱등하지 않다.
반면에 DELETE /users/1을 두 번 보냈을 때
- 첫 번째 요청: 사용자 1이 삭제됨
- 두 번째 요청: 이미 삭제된 상태이므로 "없음" 상태 유지
결과(사용자 1이 존재하지 않는다)가 한 번 요청할 때와 두 번 요청할 때 동일하므로 이는 멱등한 동작이다.
HTTP 메소드별 멱등성
HTTP 메소드마다 멱등성은 다음과 같이 정의한다. 이는 메소드가 동작을 멱등/비멱등으로 강제한다기보단 설계 시 그렇게 해야 한다는 규약이다.
| 메소드 | 멱등성 |
|---|---|
| GET | 멱등 |
| PUT | 멱등 |
| DELETE | 멱등 |
| POST | 원칙적으로 비멱등 |
| PATCH | 구현에 따라 다름 |
삭제 API이지만 DELETE 대신 POST를 쓰는 경우?
실무에서는 삭제 동작인데도 POST로 구현된 API를 종종 본다. 당연히 문자로는 DELETE 메소드가 더 맞을 것만 같아보이는데 왜 그럴까?
이는 해당 API의 내부 구현에 달린다. 크게 두 가지 이유로 POST 메소드를 쓴 삭제 API가 존재할 수 있다.
1. 삭제에 딸린 부수효과가 "한 번만" 실행되어야 하는 경우
데이터 추출, 재고 복구, 포인트 환급 등 삭제와 함께 일어나는 부수효과가 중복 실행되면 안 되는 상황도 있다.
위의 경우에는 삭제 자체는 멱등해도 부수효과가 멱등해선 안된다.(데이터 추출이 2번 일어남, 재고 복구나 포인트 환급이 2번 일어남, 일반적으론 ‘사고’라고 볼 수 있다^^;) 그래서 전체 로직을 관장하는 API 핸들러의 로직을 의도적으로 멱등하지 않게 설계해야 한다. 따라서 이럴 때 POST를 쓰기도 한다.
2. Request Body가 필요한 경우
삭제 사유를 기록하거나, 연관된 리소스(보통은 1:N으로 연결된 데이터)를 함께 정리하거나, 알림을 발송하는 등 body에 데이터를 실어 보내야 하는 상황이다.
DELETE는 관례적으로 body를 사용할 수 없기 때문이다. 이런 경우 또한 POST로 우회하는 경우가 많다.
실무할 때 고려하면 좋을 API 엔드포인트 규칙
위와 같이 삭제 로직도 단순히 데이터 삭제만 할 게 아니라 부수적인 동작까지 고려해야 하는 경우가 오히려 더 많다.
그래서 서비스를 개발하다보면 CRUD 4가지의 동작 이외에 다양한 동작을 요구하기 마련이라 이럴 땐 API 엔드포인트 명칭을 어떻게 할지 고민된다.
메소드 자체가 동작을 나타내는 것이 RESTful API의 설계 원칙이긴 하지만 구분이 분명히 되어야 할 때는 동사를 쓰기도 했다. 이번 삭제 API의 경우도 POST 메소드를 쓸 땐 다음과 같은 예시처럼 설계했다.
- 예시: POST 메소드의 삭제 로직 API는
POST /api/users/1/delete라고 엔드포인트를 명명하여, 동작 자체를 명시할 수 있다.
마치며
별 생각 없이 지나쳤던 부분에 사실은 이런 원리가 있었다는 걸 알게 되어 새로운 관점을 하나 더 얻었다. 설계에는 이유가 꼭 있기 마련이다.
또한 메소드 이름만 보고 로직을 당연하게 가정하기보다, 이 API가 실제로 어떤 약속을 지키고 있는지 자원(리소스) 상태 관점에서 판단하는 습관이 필요하다고 느꼈다. "이 요청이 리소스 상태를 어떻게 바꾸는가"가 먼저고, 메소드 선택은 그 약속을 표현하는 수단일 뿐이라는 것이 이번에 정리하면서 다시 확인한 지점이다. 이 점을 이해한 후에는 자원 상태에 입각하여 API 설계를 하게 되었다.
AI가 코드를 잘 짜주는 시대가 되어 ‘설계’ 자체가 중요해진 현 시대이니, API 설계 또한 더욱 뾰족한 경계를 갖고 해야겠다는 다짐 또한 하였다.