Files
2nd/10_Wiki/Topic_Programming/Coding/DB_Soft_Delete_Patterns.md
T
Antigravity Agent 9148c358d0 docs(10_Wiki): 위키 전체 재구성 — Topic_* 폴더를 4개 카테고리로 통합 + 대규모 중복 제거
Topic_Agent/Topic_Blog/Topics/Topics_Biz/Topics_Meeting/Topics_Rag의 마크다운 지식 문서를
Topic_General/Topic_Programming/Topic_Graphic/Topic_Business 4개 카테고리로 재분류.

- 중복 제거: frontmatter의 status:duplicate/merged + duplicate_of/redirect_to 필드로
  자기 자신을 중복으로 선언한 리다이렉트 stub 1032개 제거, 완전 동일 내용 파일 472개 제거,
  동일 파일명·다른 내용 충돌 시 더 큰(완전한) 버전만 유지(162개 제거) — 총 1639개 중복 제거.
- 분류: 폴더 단위로 명확한 항목(AI_and_ML/Coding/Architecture 등 → Programming,
  Comfyui/Visual_Effects → Graphic, Topics_Biz/Topics_Meeting/사업 등 → Business,
  Poetic_Blog_Writing/창의성/Game_Design 등 → General)은 폴더 우선순위로,
  나머지 혼재 폴더(Topic_Agent/Topic_Blog/Topics 루트/Thinking & Reasoning/Other/UI_UX_Assets)는
  title/tags 키워드 스코어링으로 파일 단위 분류(불명확한 경우 General로 폴백).
  원본 폴더명은 "From_*" 서브폴더로 보존해 추적 가능성 유지.
- 최종 배치: Programming 2784 / General 1608 / Graphic 285 / Business 249 = 4926개 문서.
- 에이전트 운영 상태(.astra/.agent/.obsidian/sessions/memory/_company/docs/lessons/_shared/src)는
  지식 콘텐츠가 아니므로 재분류 대상에서 제외하고 원위치 유지.
- Topics/Topic_email(상위 보호 폴더 Topic_email과 파일명 100% 중복) 삭제 — 보호 폴더 자체는 미변경.
- 완전히 비게 된 Topic_Agent/Topic_Blog/Topics_Biz/Topics_Rag 폴더 제거.
2026-07-05 00:33:48 +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.

🔗 관련 문서