[UMC] Soft Delete

Backend·2025-09-30·by 1000hyehyang

이번주 UMC 시니어 미션 첫번째는 Soft Delete'에 대해 조사하기!

삭제라는 행위는 직관적으로 “데이터를 없앤다”로 이해되지만, 실제 구현 단계에서는 훨씬 복잡한 선택지를 요구한다. 사용자가 화면에서 “삭제” 버튼을 눌렀을 때 서버는 단순히 DB에서 행을 날려버려야 할까, 아니면 복구 가능성을 고려해 흔적을 남겨두어야 할까? 이 지점에서 Soft Delete와 Hard Delete의 차이가 드러난다.

 

HTTP 관점에서의 삭제

HTTP 명세에 따르면 DELETE 메서드는 특정 URI와 그 표현을 연결하지 않도록 요청하는 의미를 가진다. 여기서 중요한 점은 DB에서 행을 반드시 물리적으로 삭제해야 한다는 강제가 없다는 것이다. 다시 말해, REST API에서 DELETE /resources/{id}를 설계했다고 해서 반드시 DB에서 완전히 삭제해야 하는 것은 아니다. 상황에 따라 Soft Delete든 Hard Delete든 내부 구현을 선택할 수 있다.

HTTP DELETE 요청은 “DB에서 행을 완전히 지우라”는 명령이 아니다. 단지 “그 리소스를 더 이상 접근할 수 없게 만들어라”는 의미

이때 DELETE 요청의 응답으로는 보통 204 No Content가 많이 쓰인다. 비동기 처리라면 202 Accepted도 자연스럽다. 그리고 정말 영구적으로 자원이 사라졌음을 클라이언트에 알리고 싶다면 410 Gone을 선택할 수도 있다. 다만 410은 선택 사항이지 필수는 아니다.

  • 204 No Content 가장 흔히 쓰이는 응답. 삭제가 정상적으로 처리되었고 클라이언트가 추가로 받을 본문은 없다는 의미다.

  • 202 Accepted 삭제 요청을 수락했지만 실제 작업은 비동기적으로 처리될 때 사용한다. 예를 들어 대용량 데이터를 지우는 경우, 클라이언트는 즉시 결과를 알 수 없고 나중에 상태를 확인해야 한다.

  • 200 OK Soft Delete에서 삭제된 리소스를 응답 본문에 담아 돌려줄 때 사용된다.

  • 404 Not Found 이미 삭제된 리소스를 다시 삭제하려 할 때 쓸 수 있다. 다만 일부 서비스는 멱등성을 위해 같은 요청이 와도 그냥 204로 처리한다.

  • 410 Gone 리소스가 영구적으로 삭제되어 더 이상 접근할 수 없음을 명시적으로 나타낸다. 보류 기간이 끝난 뒤 Hard Delete가 완료된 상태를 알리는 데 적합하다.

 

Soft Delete의 기본 구조와 문제점

Soft Delete는 보통 테이블에 deleted_at이나 is_deleted 같은 컬럼을 두어 구현한다. 삭제 요청이 들어오면 해당 컬럼 값을 갱신하고, 일반 조회에서는 이 조건을 걸어 제외한다. 이렇게 하면 잘못 삭제된 데이터를 복구할 수 있고, 누가 언제 삭제했는지 추적할 수 있다.

하지만 문제도 따른다. 대표적인 것이 유니크 제약과 쿼리 성능이다. 예를 들어 이메일 같은 고유 값은 중복이 허용되지 않아야 한다. 그러나 Soft Delete된 행이 DB에 남아 있다면 새로 가입하려는 사용자가 동일한 이메일로 가입할 때 충돌이 발생할 수 있다.

PostgreSQL은 이 문제를 해결하기 위해 부분 인덱스(partial index) 기능을 제공한다. WHERE deleted_at IS NULL 조건을 건 유니크 인덱스를 만들면, 삭제되지 않은 데이터에 대해서만 유니크 제약이 동작한다.

 

Soft Delete에 적합한 HTTP Method

Soft Delete를 구현할 때 가장 적합한 HTTP Method는 여전히 DELETE이다. 클라이언트 입장에서는 단순히 “삭제 요청”임을 알리면 되고, 실제로 데이터를 물리적으로 지울지 여부는 서버가 결정한다.

간혹 PATCH를 사용해 { "is_deleted": true } 같은 JSON 바디를 보내는 방식도 보이지만, 이는 HTTP 표준이 정의한 PATCH의 의미(리소스의 일부 수정)와는 어울리지 않는다. 삭제는 단순 속성 변경이라기보다 리소스를 제거하는 행위 전체를 나타내기 때문에 DELETE로 표현하는 것이 REST 의미론에 더 맞다.

구글 API 디자인 가이드(AIP-135)는 이 점을 분명히 한다. 삭제 요청은 무조건 DELETE를 사용하고, 복구가 필요하다면 POST /resources/{id}:undelete 같은 별도 액션 엔드포인트를 두라고 권장한다. 즉, 삭제=DELETE, 복구=POST :undelete라는 패턴이 가장 일관적이고 이해하기 쉽다.

 

업계에서의 실제 사례

Google Cloud Storage도 2024년부터 Soft Delete를 버킷 단위로 기본 지원하기 시작했다. 보류 기간을 7일~90일 사이로 설정할 수 있으며, 이 기간 동안 삭제된 객체를 복구할 수 있다. 기간이 지나면 자동으로 영구 삭제가 된다. 클라우드 서비스처럼 데이터 규모가 큰 곳에서는 실수 방지를 위해 소프트 삭제를 안전망으로 제공하고, 일정 기간 뒤 하드 삭제로 정리하는 패턴이 이제 표준처럼 자리잡고 있다.

 

규제 준수와 Hard Delete

Soft Delete만으로는 충분하지 않은 영역도 있다. 분명히 유저는 영원히 탈퇴함으로써 개인 정보를 사이트로부터 완전히 삭제했다고 생각했을 것이다. 그런데 남아 있다면 분명 뒷통수를 맞은 기분이지 않겠는가!

GDPR 제17조, 이른바 “잊힐 권리” 조항은 데이터 주체가 요청하면 “지체 없이” 데이터를 파기해야 한다고 규정한다. 이 경우 Soft Delete로 가려놓는 것만으로는 불충분하고, 보류 기간이 끝난 뒤 반드시 Hard Delete나 비가역적 익명화로 이어져야 한다.

따라서 실제 서비스에서는 보통 이렇게 흐름을 잡는다.

  1. DELETE 요청 시 Soft Delete를 적용한다.
  2. 보류 기간 동안은 복구 가능하다.
  3. 보류 기간이 끝나면 배치 작업을 통해 Hard Delete한다.
  4. 개인정보가 포함된 경우에는 별도의 파기 정책을 마련해 규제를 준수한다.

 

종합 정리

결국 Soft Delete는 단순히 “지우지 않고 플래그만 남긴다”는 테크닉을 넘어, 운영 안정성과 규제 준수, 개발자 경험까지 연결되는 설계의 문제이다. REST 관점에서는 DELETE 메서드를 일관되게 쓰되, 복구는 :undelete 같은 명시적 액션으로 구분하는 것이 가장 자연스럽다.

데이터베이스 차원에서는 부분 인덱스로 유니크 문제를 해결하고, ORM 레벨에서는 기본적으로 “삭제 제외” 스코프를 걸어둔다. 운영 정책에서는 보류 기간과 영구 삭제 시점을 명시적으로 정의하고, 규제가 요구하는 개인정보 파기 요건까지 대응해야 한다.

댓글 로딩 중...