Files
2nd/10_Wiki/Topics/Coding/DB_Soft_Delete_Patterns.md
T
2026-05-09 21:08:02 +09:00

3.9 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
db-soft-delete-patterns Soft Delete — deleted_at / 일관성 / 인덱스 Coding draft B conceptual 2026-05-09 2026-05-09
database
soft-delete
vibe-coding
language applicable_to
SQL / ORM
Backend
soft-delete
tombstone
partial index
deleted_at

Soft Delete

행 자체 삭제 X — deleted_at 컬럼에 timestamp. 복구 / audit / FK 안 깨짐. 단 모든 query 에 WHERE deleted_at IS NULL 안 붙이면 leak. ORM scope / view / 부분 인덱스로 강제.

📖 핵심 개념

  • Hard delete: 행 사라짐 — FK 깨짐, 복구 불가.
  • Soft delete: 보존 — query 마다 필터.
  • Partial index: deleted_at IS NULL 만 인덱싱 — 빠름 + 작음.
  • Unique constraint: 유니크 컬럼은 (email, deleted_at IS NULL) 처럼 부분 unique.

💻 코드 패턴

스키마

CREATE TABLE users (
  id UUID PRIMARY KEY,
  email TEXT NOT NULL,
  deleted_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- partial unique: 살아있는 행 사이에서만 유니크
CREATE UNIQUE INDEX users_email_active ON users(email) WHERE deleted_at IS NULL;

-- partial index: 활성 lookup 빠름
CREATE INDEX users_active ON users(id) WHERE deleted_at IS NULL;

Prisma — 자동 필터

// extension 으로 모든 query 에 자동 필터
const prisma = new PrismaClient().$extends({
  query: {
    user: {
      async findMany({ args, query }) {
        args.where = { ...args.where, deletedAt: null };
        return query(args);
      },
      async findUnique({ args, query }) {
        args.where = { ...args.where, deletedAt: null };
        return query(args);
      },
    },
  },
});

Drizzle — 명시적 helper

const activeUsers = () => db.select().from(users).where(isNull(users.deletedAt));

// 삭제
await db.update(users).set({ deletedAt: new Date() }).where(eq(users.id, id));

// 복구
await db.update(users).set({ deletedAt: null }).where(eq(users.id, id));

View 로 강제

CREATE VIEW v_users AS SELECT * FROM users WHERE deleted_at IS NULL;
-- 앱은 v_users 만 사용, 직접 users 접근 금지

Cascade soft delete

-- user 삭제 시 그의 posts 도 soft delete
WITH deleted_user AS (
  UPDATE users SET deleted_at = NOW() WHERE id = $1 RETURNING id
)
UPDATE posts SET deleted_at = NOW()
WHERE user_id IN (SELECT id FROM deleted_user) AND deleted_at IS NULL;

진짜 영구 삭제 (GDPR)

-- 1년 전 soft-deleted 는 hard delete
DELETE FROM users WHERE deleted_at < NOW() - INTERVAL '1 year';

Audit log 와 함께

CREATE TABLE user_deletions (
  user_id UUID,
  deleted_at TIMESTAMPTZ,
  deleted_by UUID,
  reason TEXT
);

🤔 의사결정 기준

상황 추천
사용자 / 게시물 (복구 필요) Soft delete
일시적 데이터 (세션, 캐시) Hard delete
GDPR 영구삭제 요청 Hard delete (또는 anonymize)
큰 audit / compliance Soft + audit log
FK 가 자주 끊김 우려 Soft delete (FK 유지)
매우 큰 테이블 (10M+) Hard + 테이블 이력화

안티패턴

  • 모든 query 에 필터 누락: 삭제된 행 노출.
  • Unique constraint 그대로: 같은 email 재등록 막힘. partial unique.
  • 인덱스에 deleted 행 포함: 인덱스 비대.
  • deleted_at 만 — by 누구 / 왜 모름: audit 필드 같이.
  • Cascade 안 함: 삭제된 user 의 post 가 살아있음.
  • GDPR 무시: 영구 삭제 정책 + 일정 시간 후 hard delete.
  • is_deleted boolean: timestamp 보다 정보 적음.

🤖 LLM 활용 힌트

  • deleted_at timestamp + partial unique + partial index 3종.
  • ORM extension 또는 view 로 자동 필터.
  • GDPR = soft → 일정 시간 후 hard.

🔗 관련 문서