우테코 내 핫한 였던 DELETE 메서드에 대한 논란들을 내 생각과 함께 정리해보겠다.
1. DELETE는 멱등성을 보장하는가? 보장해주어야 하는가?
(1) DELETE는 멱등성을 보장하는가?
아래와 같은 상황에서 DELETE /reservations/1 은 멱등성을 보장한다고 말할 수 있을까?
1. DELETE /reservations/1 를 호출한다. 데이터가 존재하지 않아 400 상태코드를 반환했다.
2. id = 1 의 Reservation 데이터를 적재한다.
3. DELETE /reservations/1 를 호출한다. 데이터 삭제에 성공해서 200(혹은 204) 상태코드를 반환했다.
4. DELETE /reservations/1 를 호출한다. 데이터가 존재하지 않아 400 상태코드를 반환했다.
Java
복사
우테코 슬랙 채널에 올라온 재밌는 이야기를 가져와봤다. 대다수의 크루들이 멱등성을 보장한다고 답했다. 이때, 한 가지 재밌는 의견을 내준 크루가 있었다.
A: 데이터가 없어서 삭제하지 못했다는 예외 상황에 대한 상태코드를 반환했을 뿐 DELETE /reservations/1은 몇 번을 반복해도 같은 결과를 도출하기 때문에 멱등성을 보장한다고 볼 수 있을 것 같습니다! 데이터의 관점에서는 삭제된다는 결과 외에는 다른 부작용이 없는 것 같습니다!
B: 저도 A와 같은 이유로 멱등성을 해치는건 아니라고 생각합니다. 다만 3번과 4번이 거의 동시에 호출되었을때의 상황 (일명 따닥 눌러서 같은 요청이 두번 호출되는 상황) 에 대해서 생각해보면 좋을것 같아요. 3번 요청에 대한 처리가 서버에서 잘 진행되어데이터가 삭제 되었어요. 200 응답이 클라이언트로 전달되는 과정에 네트워크 오류 등으로 전달되지 못하게 되었어요. 그 후 4번 요청이 서버에 전달되면 삭제할 데이터가 없어 400응답을 하겠죠. 그렇게 400 응답만이 사용자에게 전달 되게 되면, 응답을 받은 사용자는 “id=1이라는 예약이 애초에 없어 삭제되지 않았구나” 라고 질못 착각하게 되는 문제가 있을것 같아요.
따라서 두 개의 요청(따당 눌린 3, 4번의 요청)에 대해 같은 요청이었는지 여부를 판단하는 기준을 세워야 할것 같아요. 예를들어 1초 이내의 시간에 한 동일한 요청은 같은 요청으로 생각해 2번째 응답에서도 200 상태코드를 응답한다던지 하는 방법도 있을것 같아요.아래글에서는 멱등키를 헤더에 전달하는 방식을 이용해 이런 문제를 해결했다고 하네요!
여기서 B 크루의 의견이 굉장히 흥미로웠다. 서버 구현마다 다를 수 있지만, 보통 GET 요청은 멱등성을 보장하지 못하기에 위의 토스페이먼츠 글에선 멱등키를 이용해 멱등성을 보장했다. 이에 대해 B 크루는 DELETE 도 멱등키를 이용해 멱등성을 더 정교하게 보장해줄 수 있단 의견을 내놓았다. 이 의견에 대한 내 생각을 글로 정리해보겠다.
(2) 멱등성의 정의와 의의
먼저, DELETE의 멱등성에 대해 논하기 위해 멱등성의 정의부터 내리고 시작하자.
나 역시 이 글의 맨 위 예시 상황의 delete 요청은 멱등성을 보장한다 생각한다. 멱등성을 따질 땐 실제 서버의 리소스만 보면 되며, 각 요청에서 반환하는 응답 코드는 다를 수 있기 때문이다. 위 예시의 delete 요청은 리소스 식별자로 삭제를 하고 있기에(DELETE /resource/{id}) 여러 번 요청하더라도 해당 리소스가 서버에서 삭제된 상태가 된단 결과에 변함이 없다. 즉, 멱등성을 보장한다.
만일 리소스 식별자 없이 삭제를 요청했다면(DELETE /resource/last), 이를 N번 호출했을 때 N개의 리소스가 삭제되기에 동일한 요청임에도 서버 리소스의 상태가 요청마다 변해 멱등성을 보장하지 못한다.
멱등성을 보장해야 하는 이유는 결국엔 중복 요청으로 인해 시스템이 불안정해지지 않도록 하기 위함이다. 예를 들어 매 요청마다 내부적으로 다른 값을 삭제하는 DELETE /resource/last 요청보단 DELETE /resource/{id} 과 같이 언제나 같은 값을 삭제하는 요청이 시스템을 더 안정적으로 만든다.
(3) 멱등성의 활용: DELETE의 멱등성을 “보장해주어야” 하는가?
위와 같은 측면에서 바라봤을 때, B 크루가 제안한 일명 ‘삭제 따닥 요청’에 대한 처리의 필요성에 대한 의문이 들었다. 물론 내어주신 의견이 잘못됐다고 생각하진 않는다. 어쩌면 내가 상상치 못한 특수한 상황에선 정말로 필요할 수 있다. 다만 일반적인 상황에서 바라봤을 때, 3번과 4번이 거의 동시에 호출되더라도 클라이언트에게 제공하는 DELETE /reservations/1 API는 멱등성을 보장하는데 멱등키를 헤더에 전달하는 방식과 같은 멱등성을 보장하는 장치를 추가로 둬야 한단 점이 조금 부자연스럽게 느껴졌다.
B 크루가 말씀해주신대로 멱등키를 헤더에 추가하는 방식은 요청의 중복 실행을 방지하고, 네트워크 지연이나 중복 전송으로 인해 발생할 수 있는 문제를 예방하기 위해 사용된다. 그러나 현재 논점인 DELETE /reservations/1 요청의 경우, 멱등성 자체가 요청의 중복을 문제시하지 않기에 이러한 추가적인 처리는 필요하지 않을 거 같단 생각이 들었다.
궁극적으론 클라이언트가 DELETE /reservations/1 요청을 통해 id=1이라는 예약이 애초에 없어서 삭제되지 않았다와 id=1이라는 예약이 방금 삭제되었다 라는 사실을 구분해 알아야 할까라는 의문이 든다. id=1이라는 예약은 없는 리소스다라는 사실 정도만 알아도 충분하지 않을까?
사실 존재하지 않는 id로 delete 요청을 보내 400이 뜬다는건 네트워크 문제나 사용자의 문제(ex. 새로고침을 안했을 때)로 사용자 화면에 제공되는 데이터가 부정확하단 의미다. 데이터를 정확하게 고치는 작업이 필요하지, 사용자에게 삭제가 방금 되었다라는 정보를 꼭 메시지로 알려줄 필요는 없을 거 같다. 데이터를 정확하게 고치면 사용자는 삭제를 원한 데이터가 삭제된 결과를 확인할 수 있기 때문이다.
(3) 멱등성 활용에 대한 글은 취향과 사견이 담긴 글이라 잘못된 내용이나 다른 의견이 있다면 주저없이 댓글로 피드백 주면 좋겠다. 끝이 아니다. 논란덩어리를 청산하는 글이기 때문이다.
2. DELETE id 검증 200인가 400인가
다음으로 우테코 내 가장 핫한 주제였던 DELETE 요청 시 삭제하려는 리소스가 존재하지 않을 때, 20x 대가 맞는가 40x대가 맞는가에 대한 이야기를 해보겠다. 이 이야기는 우테코 내 라이트닝 토크의 토론 주제로까지 선정됐었다. 결론부터 말하자면 정답은 없고 상황마다 다르단 거다.
아래 두 코드와 같이 id를 통해 레코드를 삭제하려 할 때, 테이블에 id를 가진 레코드가 있는지 확인하고 에러를 터트리는 로직의 필요성에 대해 의견이 양립했다. 한 마디로 정리하자면 DELETE 요청 시 삭제하려는 리소스가 존재하지 않을 때, 1번은 20x를 보내자 주장했고 2번은 40x를 보내자 주장했다.
1.
id의 존재를 확인하지 않고 항상 200을 보내자
•
레코드가 존재할 시 처음에 삭제하고, 이후 반복 요청 시 별도 처리하지 않는다.
•
참고로 id가 존재하지 않아도 db단에서 예외를 발생시키지 않는다.(나도 이번에 처음 알았다)
public void deleteReservation(long id) {
reservationRepository.deleteById(id);
}
Java
복사
2.
id의 존재를 확인해 없을 땐 400을 보내자
•
레코드가 존재할 시 처음엔 삭제하고, 이후 반복 요청 시 레코드가 없을 테니 에러 처리한다.
public void deleteReservation(long id) {
if (!reservationRepository.existsById(id)) {
throw new NotFoundReservationException();
}
reservationRepository.deleteById(id);
}
Java
복사
20x를 주장하는 사람들의 요지는 존재하지 않는단게 곧 삭제되어 없는 자원이란 뜻인데 굳이 별도 처리를 해줄 필요가 없단 점이었고, 40x를 주장하는 사람들의 요지는 삭제 자체를 성공했는지, 안했는지를 구분해 알려줘야 한단 점이었다. 우테코 토론장에서도 의견이 거의 반반으로 나뉘었었다. 그러다 재밌는 흐름으로 흘러가기 시작했다. 사실 나는 전날 이미 안돌, 재즈와 이 이야기를 나눴었기에 이 전개를 예상했었다.ㅎㅎ 바로, 정답은 없다는 것이다.
전날 안돌 왈 아래와 같이 말씀하셨다.
이건 리소스 성격 / 클라와의 협의에 따라 다를것 같아요.
DELETE가 transaction(디비 트랜잭션이 아니라 어느 기능의 처음과 끝)의 일부로써 중요한 역할을 한다면 에러를 던져주는게 당연히 좋을것 같고나 이거 버릴래 수준이라면 귀찮게 하지 않는 ok버전이 좋겠죠?
참고로 결제취소 는 에러를 던져주지만(ALREADY_CANCELED) 웹훅 endpoint 삭제는 무적권 200 내립니다.
여기서 마지막 문장이 논란을 잠재워줄 킥이었다. 왜 결제 취소는 400을 내리고, 웹훅 endpoint 삭제는 200을 내린단걸까? 둘의 차이가 뭘까? 사용자 입장에서 생각해보면 편하다. 핵심은 해당 DELETE 요청이 사용자에게 얼만큼 중요하냔 점이다.
예를 들어 삭제 성공을 이미 했는데, 사이트 새로고침이 제대로 안되던가 해서 네트워크 문제라던가 해서 사용자가 삭제 버튼을 다시 누르는 상황이 생길 수 있다. 이때, 결제 취소의 경우 이미 삭제됐다(40x)고 뜨는게 유저관점에서 납득갈 것이다. 취소 여부가 유저에게 중요하기 때문이다. 내 결제에 대한 취소가 제대로 된 건지 확인받을 필요가 있다. 마찬가지로 우테코에서 지금 크루들이 만들고 있는 예약 삭제 의 경우 사용자는 내 예약이 제대로 삭제됐는지 확인받고 싶어한다. 따라서 예약 삭제 API는 id를 검증해 id가 존재하지 않으면 40x 대 예외를 던져주는 편이 사용자에게 유익하다.
반면, 웹훅 endpoint 삭제의 경우 삭제를 성공했다(20x)고 떠도 유저는 크게 개의치 않을 것이다. 어차피 없어진 상태를 원하는 거고 없는 상태라면 오케이 할만한 수준이기에 오히려 귀찮게 하지 않는 200대가 뜨는 편이 낫다. 마찬가지로 우테코에서 지금 크루들이 만들고 있는 관리자가 테마와 시간을 삭제하는 기능의 경우, 말그대로 어떤 옵션이 사라지는 거기에 없어진 상태를 원할테고 굳이 실제로 삭제된 게 맞는지 검증해줄 필욘 없다. 따라서 테마 삭제 API와 시간 삭제 API는 id가 존재하는지 따로 검증하지 않고, 그저 API 실행 후 db에 해당 자원이 존재하지 않게 된단게 보장된다면 언제나 200을 보내주면 된다.