포스트

삭제하지 않는 삭제: soft delete의 비용

물리 삭제 대신 플래그로 데이터를 숨기는 soft delete 패턴이 모든 조회 조건과 유니크 제약, 인덱스에 어떤 비용을 청구하는지 분석한다.

삭제하지 않는 삭제: soft delete의 비용

데이터 삭제를 다룬 주였다. 요청은 단순했다 — “삭제해도 기록은 남겨야 한다.” 이 한 줄이 테이블 전체의 조회 규칙을 바꾼다. 물리 삭제(DELETE) 대신 플래그를 세우는 soft delete는 가장 흔한 패턴이지만, 그 비용을 모르고 도입하면 조용히 데이터가 새거나 중복된다.

soft delete의 동작과 숨은 계약

soft delete는 행을 지우는 대신 deleted_at 같은 컬럼에 값을 채워 “이 행은 없는 셈 친다”는 약속을 만든다. 문제는 이 약속이 모든 조회에 강제되는 암묵적 계약이라는 점이다. DB는 그 행을 여전히 가지고 있고, 인덱스에도 살아 있다. “없는 셈”은 오직 쿼리가 WHERE deleted_at IS NULL을 붙일 때만 성립한다.

조건을 한 군데라도 빠뜨리면 삭제된 데이터가 목록에, 카운트에, 집계에 다시 등장한다. 이건 버그가 아니라 패턴의 본질이다 — 삭제 책임이 DB 엔진에서 애플리케이션의 모든 쿼리로 분산된다.

유니크 제약과의 충돌

가장 날카로운 비용은 유니크 제약이다. 이메일이 유니크인 users 테이블을 생각하자. alice@x.com을 삭제(플래그)한 뒤 같은 이메일로 재가입하면, 물리적으로는 두 행이 같은 이메일을 가진다. 일반 유니크 인덱스는 삭제 여부를 모르므로 충돌로 거부한다.

1
2
3
4
-- 살아있는 행만 유니크하게: 부분 인덱스 (PostgreSQL)
CREATE UNIQUE INDEX uq_users_email_active
  ON users (email)
  WHERE deleted_at IS NULL;

PostgreSQL은 부분 인덱스(partial index)로 “삭제되지 않은 행 사이에서만 유니크”를 표현한다. 부분 인덱스를 지원하지 않는 엔진(MySQL 등)에서는 유니크 키에 삭제 시각 자체를 포함시키거나, 삭제 시 이메일을 변형해 충돌을 피하는 우회가 필요하다.

1
2
3
4
-- MySQL 우회: 삭제 표시를 0/타임스탬프로 둔 복합 유니크
ALTER TABLE users
  ADD UNIQUE KEY uq_email_alive (email, deleted_flag);
-- 살아있으면 deleted_flag=0, 삭제 시 행마다 다른 값으로 채워 충돌 회피

조회 메커니즘과 인덱스 비용

WHERE deleted_at IS NULL이 모든 조회에 붙으면 인덱스 설계도 따라간다. 자주 쓰는 검색 인덱스의 선두 컬럼이 삭제 플래그가 아니어야 카디널리티가 산다. 보통은 부분 인덱스로 살아있는 행만 색인하거나, 복합 인덱스 후미에 두는 식으로 옵티마이저가 인덱스를 타게 한다. 그렇지 않으면 옵티마이저가 인덱스를 버리고 풀스캔으로 빠지는 경우가 생긴다.

1
2
3
4
SELECT id, email, name
FROM users
WHERE deleted_at IS NULL
  AND name LIKE '김%';

운영 함정

1. 조건 누락은 영구 누수다. 새 쿼리, 새 조인, 새 집계마다 deleted_at IS NULL을 기억해야 한다. 사람의 기억에 의존하면 반드시 샌다. 뷰(view)로 살아있는 행만 노출하거나, ORM/매퍼 레벨에서 글로벌 필터를 거는 것이 안전하다.

2. 조인이 삭제를 뚫는다. orders는 살아있게 필터했는데 조인한 users는 안 걸면, 삭제된 사용자의 주문이 다시 보인다. 조인 대상마다 각자의 삭제 조건을 걸어야 한다.

핵심 요약

  • soft delete는 삭제 책임을 DB에서 모든 쿼리로 옮기는 거래다.
  • 유니크 제약은 부분 인덱스(또는 복합 유니크 우회)로 “살아있는 행 한정”으로 재정의해야 한다.
  • 조건 누락 방어는 사람이 아니라 뷰/글로벌 필터 같은 구조로 한다.

면접 한 줄 Q&A Q. soft delete 테이블에서 유니크 제약은 어떻게 거나? A. 일반 유니크는 삭제된 행과 충돌하므로, 삭제되지 않은 행만 색인하는 부분 인덱스로 살아있는 행 한정 유니크를 만든다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.