2026년 08월 22일

Soft Delete는 정말 좋은 설계일까? 삭제 플래그가 만드는 예상 밖의 문제들

Soft Delete는 데이터를 실제로 지우지 않고 is_deleted 같은 플래그 컬럼을 두어 ‘삭제된 것처럼’ 처리하는 패턴이다. 처음 접하면 직관적이고 안전해 보인다. 실수로 지운 데이터도 복구할 수 있고, 감사 로그도 자연스럽게 남는다. 그래서 많은 프로젝트가 별 고민 없이 이 패턴을 도입한다.

하지만 시스템이 커지고 데이터가 쌓이면서, Soft Delete는 처음 기대했던 것과는 다른 모습을 보이기 시작한다. 쿼리마다 조건이 추가되고, 유니크 제약이 엉키고, 연관 데이터 처리가 복잡해진다. 이 글에서는 Soft Delete가 실제로 어떤 문제를 일으키는지, 그리고 어떤 상황에서 대안을 고려해야 하는지를 구체적으로 살펴본다.

Soft Delete의 기본 구조

가장 일반적인 형태는 테이블에 deleted_at 또는 is_deleted 컬럼을 추가하는 방식이다. 삭제 요청이 들어오면 해당 행을 실제로 지우는 대신 이 컬럼에 타임스탬프나 불리언 값을 기록한다. 조회할 때는 WHERE deleted_at IS NULL 조건을 붙여서 삭제된 데이터를 걸러낸다.

-- 삭제 처리
UPDATE users SET deleted_at = NOW() WHERE id = 42;

-- 조회 시 필터링
SELECT * FROM users WHERE deleted_at IS NULL;

ORM 레벨에서는 Rails의 acts_as_paranoid, Laravel의 SoftDeletes 트레이트처럼 이 패턴을 자동화해주는 도구들도 많다. 덕분에 개발자는 큰 노력 없이 Soft Delete를 적용할 수 있다.

쿼리 전반에 퍼지는 복잡도

Soft Delete가 도입되는 순간, 해당 테이블을 참조하는 모든 쿼리에 삭제 여부 필터링 조건이 붙어야 한다. 테이블 하나라면 관리할 수 있지만, 조인이 많아질수록 문제가 커진다.

예를 들어 users, posts, comments 세 테이블이 모두 Soft Delete를 사용한다면, 이 세 테이블을 조인하는 쿼리에는 각 테이블마다 deleted_at IS NULL 조건이 필요하다. 조건 하나라도 빠지면 삭제된 데이터가 결과에 포함되는 버그가 생긴다.

ORM이 자동으로 처리해주더라도 완전하지 않다. 네이티브 쿼리를 직접 작성하거나, 뷰(View)를 정의하거나, 리포팅 쿼리를 짤 때 이 조건을 빠뜨리는 실수는 생각보다 자주 발생한다. 그리고 이런 버그는 눈에 잘 띄지 않아 한참 지나서야 발견되는 경우가 많다.

유니크 제약 조건이 무너진다

Soft Delete가 가장 골치 아픈 문제를 만드는 지점 중 하나가 유니크 제약이다. 예를 들어 이메일 주소나 사용자명은 고유해야 한다. 그런데 Soft Delete를 사용하면, 삭제된 행이 테이블에 그대로 남아 있으므로 데이터베이스 레벨의 유니크 인덱스가 제대로 작동하지 않는다.

사용자가 계정을 삭제했다가 같은 이메일로 다시 가입하려 하면, 데이터베이스는 이미 해당 이메일이 존재한다고 판단해 오류를 낸다. 삭제된 행인데도 불구하고.

이를 우회하려면 여러 방법이 있다.

  • 삭제 시 이메일 값을 deleted_42@example.com처럼 변조해서 저장하는 방법
  • 유니크 인덱스를 (email, deleted_at) 복합 인덱스로 바꾸는 방법
  • 데이터베이스 레벨 제약을 포기하고 애플리케이션 레벨에서만 검증하는 방법

어느 방식이든 데이터 무결성을 지키기가 훨씬 어려워지고, 코드도 복잡해진다. 특히 데이터 변조 방식은 삭제된 데이터를 복구할 때 원래 값을 추적해야 하는 추가 부담을 만든다.

외래 키와 연관 데이터 처리

Soft Delete는 외래 키 관계에서도 혼란을 일으킨다. 부모 레코드가 Soft Delete되었을 때, 자식 레코드들은 어떻게 처리해야 할까?

데이터베이스의 ON DELETE CASCADE는 물리적 삭제에서만 작동한다. Soft Delete를 사용하면 이 기능을 활용할 수 없으므로, 연관된 레코드들을 직접 순회하며 삭제 처리를 해야 한다. 이 로직을 빠뜨리면 부모는 삭제됐는데 자식 데이터는 살아있는 고아 레코드가 생긴다.

반대 상황도 문제다. 삭제된 부모를 참조하는 자식 레코드가 있을 때, JOIN을 통해 부모 데이터를 가져오는 쿼리는 예상치 못한 결과를 낼 수 있다. 부모가 삭제 필터에 걸러지면서 자식 데이터가 조회 결과에서 통째로 사라지거나, 반대로 삭제된 부모 정보가 노출되는 경우가 생긴다.

인덱스와 성능 문제

테이블에 데이터가 쌓일수록 Soft Delete는 성능 측면에서도 부담이 된다. 실제로 서비스에서 필요한 데이터는 전체의 일부인데, 인덱스와 테이블 스캔은 삭제된 데이터를 포함한 전체를 대상으로 이루어진다.

deleted_at IS NULL 조건은 인덱스를 효율적으로 타기 어렵게 만들 수 있다. 삭제 비율이 높은 테이블이라면 대부분의 행이 사실상 불필요한 데이터임에도 불구하고 테이블 크기를 차지하고 있다. 파티셔닝이나 아카이빙 전략 없이는 이 문제를 해결하기 어렵다.

물론 deleted_at 컬럼에 인덱스를 적절히 구성하면 어느 정도 완화할 수 있지만, 복합 인덱스 설계가 복잡해지고 유지보수 비용이 늘어난다.

Soft Delete가 정말 필요한 경우

그렇다고 Soft Delete를 무조건 피해야 한다는 말은 아니다. 다음과 같은 상황에서는 충분히 합리적인 선택이 될 수 있다.

데이터 복구가 실제로 필요한 경우

사용자가 실수로 삭제한 데이터를 일정 기간 내에 복구할 수 있어야 하는 기능이 요구사항에 포함되어 있다면, Soft Delete는 자연스러운 구현 방법이다. 이메일함의 휴지통 기능이 대표적인 예다.

감사 추적과 규정 준수

금융, 의료, 법률 등 규정 준수가 중요한 도메인에서는 데이터 변경 이력 자체를 보존해야 하는 경우가 있다. 이때 Soft Delete는 감사 로그의 일부로 활용될 수 있다. 다만 이런 목적이라면 Soft Delete만으로는 부족하고, 별도의 감사 로그 테이블이나 이벤트 소싱 패턴과 함께 사용하는 것이 더 적합하다.

관계가 단순하고 데이터가 작은 경우

테이블 간 조인이 복잡하지 않고, 데이터 규모도 크지 않으며, 유니크 제약이 별로 없는 단순한 구조라면 Soft Delete의 단점이 크게 부각되지 않는다. 이런 환경에서는 비교적 안전하게 사용할 수 있다.

대안으로 고려할 수 있는 방법들

Soft Delete의 단점이 부담스럽다면 다음 대안들을 고려해볼 수 있다.

아카이브 테이블 분리

삭제된 데이터를 별도 아카이브 테이블로 이동하는 방식이다. 원본 테이블에는 활성 데이터만 남으므로 쿼리가 단순해지고 인덱스 효율도 유지된다. 복구가 필요하면 아카이브 테이블에서 원본 테이블로 다시 이동하면 된다. 스키마 변경 시 아카이브 테이블도 함께 관리해야 하는 부담이 있지만, 운영 측면에서는 훨씬 깔끔하다.

이벤트 소싱

데이터의 현재 상태 대신 변경 이벤트 자체를 저장하는 패턴이다. 삭제도 하나의 이벤트로 기록되므로, 언제든지 특정 시점의 상태를 재현할 수 있다. 복잡도가 높아 모든 상황에 적합하지는 않지만, 이력 추적이 핵심인 도메인에서는 강력한 접근 방식이다.

물리적 삭제 + 로그 테이블

실제로 데이터를 삭제하되, 삭제 전 데이터를 별도 로그 테이블에 스냅샷 형태로 저장하는 방법이다. 원본 테이블은 깨끗하게 유지되고, 필요한 경우 로그에서 데이터를 확인할 수 있다. 복구 빈도가 낮고 주로 감사 목적이라면 이 방식이 단순하면서도 효과적이다.

설계 전에 물어봐야 할 질문들

Soft Delete를 도입하기 전에 다음 질문들을 먼저 검토해보는 것이 좋다.

  • 사용자가 실제로 삭제된 데이터를 복구하는 기능이 필요한가?
  • 삭제 이력을 감사 목적으로 보존해야 하는 법적 요구사항이 있는가?
  • 해당 테이블에 유니크 제약이 있는가?
  • 해당 테이블을 참조하는 외래 키 관계가 복잡한가?
  • 장기적으로 데이터가 얼마나 쌓일 것으로 예상되는가?

이 질문들에 대한 답이 Soft Delete의 필요성과 리스크를 가늠하는 데 도움이 된다. 복구 기능이 실제로 필요하지 않은데 ‘혹시 몰라서’ 도입하는 경우라면, 그 비용이 생각보다 크다는 점을 기억해야 한다.

결론

Soft Delete는 만능 패턴이 아니다. 단순한 구조에서 복구 기능이 필요할 때는 유용하지만, 복잡한 도메인에서 무심코 도입하면 쿼리 복잡도, 데이터 무결성, 성능 문제를 동시에 안게 된다.

중요한 것은 ‘이 패턴이 일반적으로 좋은가’가 아니라 ‘우리 시스템의 요구사항에 맞는가’를 따지는 것이다. 설계 결정은 항상 구체적인 컨텍스트 안에서 이루어져야 하고, Soft Delete도 예외가 아니다.