TIL - 20260819

juni·2026년 8월 19일

TIL

목록 보기
434/468

0819 데이터베이스 실무 심화 (7/N): Soft Delete, 복구, 데이터 보존 정책


✅ 1. 데이터를 삭제한다는 것은 무엇인가?

  • 서비스 운영에서 “삭제”는 단순히 DB row를 없애는 작업이 아닙니다.
  • 상품, 상담, 주문, 관리자 계정, 배너, 알림 이력, 상태 변경 이력처럼 서비스 데이터는 서로 연결되어 있습니다.
  • 하나를 삭제하면 관련 목록, 통계, 이력, 엑셀, 고객 응대 기록까지 영향을 받을 수 있습니다.
상품 삭제
  ↓
상담 신청에서 참조 중
  ↓
과거 상담의 상품 정보 확인 불가
  ↓
운영/고객 응대 문제 발생

➕ 1-1. 삭제가 위험한 이유

과거 상담 기록이 깨질 수 있음
주문/상담 이력 추적이 어려워질 수 있음
관리자 실수 복구가 어려움
통계 기준이 바뀔 수 있음
외래키 제약조건 오류가 발생할 수 있음
누가 삭제했는지 알 수 없을 수 있음
  • 운영 DB에서는 “진짜 삭제해도 되는 데이터인지”를 먼저 판단해야 합니다.
  • 대부분의 핵심 운영 데이터는 바로 삭제하지 않는 것이 안전합니다.

✅ 2. Hard Delete와 Soft Delete

  • 삭제 방식은 크게 Hard Delete와 Soft Delete로 나눌 수 있습니다.
구분의미예시
Hard DeleteDB row를 실제로 삭제DELETE FROM products WHERE id = 1
Soft Delete삭제 표시만 하고 row는 유지deletedAt에 시간 저장

➕ 2-1. Hard Delete

DELETE FROM products
WHERE id = 1;

특징:

DB에서 실제로 사라짐
복구가 어려움
참조 관계가 깨질 수 있음
저장 공간은 줄어듦

➕ 2-2. Soft Delete

삭제 버튼 클릭
  ↓
DB row 삭제 X
  ↓
deletedAt에 삭제 시간 저장
  ↓
일반 목록에서는 제외
  ↓
필요 시 복구 가능
UPDATE products
SET deleted_at = NOW()
WHERE id = 1;

특징:

데이터가 DB에 남아 있음
실수 삭제 복구 가능
과거 참조 기록 유지
조회 조건 관리 필요
  • 실무에서는 핵심 데이터에 Soft Delete를 많이 사용합니다.
  • 단, 모든 데이터에 무조건 Soft Delete를 적용하는 것도 좋은 것은 아닙니다.

✅ 3. Soft Delete가 필요한 데이터

  • Soft Delete는 운영상 복구 가능성이 있거나, 과거 이력을 유지해야 하는 데이터에 적합합니다.

➕ 3-1. 적용 후보

products:
상품

product_options:
용량/색상/지원금 옵션

banners:
배너

admin_users:
관리자 계정

consult_memos:
상담 메모

faq_items:
FAQ

lead_sources:
유입 소스 설정

promotion_configs:
프로모션 설정

➕ 3-2. 특히 상품에 필요한 이유

과거 상담이 상품을 참조할 수 있음
상품을 잠시 숨겼다가 다시 노출할 수 있음
관리자 실수 삭제 복구 가능
통계에서 과거 상품명을 확인해야 함

➕ 3-3. 관리자 계정에 필요한 이유

과거 상태 변경 이력의 changedByAdminId 보존
Audit Log의 actorId 보존
퇴사/비활성 관리자를 기록으로 남김
계정을 실제 삭제하면 이력 추적이 어려워짐
  • 관리자 계정은 삭제보다 isActive=false, disabledAt 처리하는 것이 더 좋습니다.
  • 과거 이력의 주체가 사라지면 운영 추적성이 떨어집니다.

✅ 4. Hard Delete가 가능한 데이터

  • 모든 데이터가 Soft Delete 대상은 아닙니다.
  • 임시 데이터, 캐시성 데이터, 만료된 작업 데이터는 Hard Delete 또는 주기적 정리가 가능할 수 있습니다.

➕ 4-1. Hard Delete 후보

임시 인증 코드
만료된 세션
만료된 refresh token
짧게 보관하는 debug log
처리 완료 후 오래 지난 임시 파일
중복 생성된 테스트 데이터

➕ 4-2. 주의할 점

운영 데이터와 테스트 데이터 구분
고객/상담/주문 원본 데이터는 신중히 처리
삭제 전 참조 관계 확인
삭제 작업은 Audit Log 남기기
  • Hard Delete는 복구가 어렵습니다.
  • 운영 DB에서 직접 삭제하는 습관은 매우 위험합니다.

✅ 5. Soft Delete 기본 컬럼

  • Soft Delete를 적용할 때는 보통 deletedAt 컬럼을 둡니다.
  • 추가로 누가 삭제했는지, 삭제 사유가 무엇인지 남길 수도 있습니다.
deletedAt:
삭제 처리 시간

deletedByAdminId:
삭제한 관리자

deleteReason:
삭제 사유

isDeleted:
삭제 여부 boolean

➕ 5-1. 추천 기본 구조

deletedAt:
nullable DateTime

deletedByAdminId:
nullable Int

deleteReason:
nullable String

➕ 5-2. Prisma 예시

model Product {
  id               Int       @id @default(autoincrement())
  modelName        String
  carrier          String
  isActive         Boolean   @default(true)
  createdAt        DateTime  @default(now())
  updatedAt        DateTime  @updatedAt

  deletedAt        DateTime?
  deletedByAdminId Int?
  deleteReason     String?

  deletedByAdmin   AdminUser? @relation(fields: [deletedByAdminId], references: [id])

  @@index([deletedAt])
}

➕ 5-3. isDeleted보다 deletedAt이 좋은 이유

isDeleted:
삭제 여부만 알 수 있음

deletedAt:
삭제 여부 + 삭제 시각을 함께 알 수 있음
  • deletedAt IS NULL이면 살아 있는 데이터입니다.
  • deletedAt IS NOT NULL이면 삭제 처리된 데이터입니다.

✅ 6. Soft Delete 조회 기준

  • Soft Delete를 적용하면 모든 일반 조회에서 deletedAt: null 조건을 넣어야 합니다.
  • 이 조건을 빼먹으면 삭제된 데이터가 고객 화면이나 관리자 목록에 다시 보일 수 있습니다.

➕ 6-1. Prisma 조회 예시

const products = await prisma.product.findMany({
  where: {
    deletedAt: null,
    isActive: true,
  },
});

➕ 6-2. 관리자 삭제함 조회

const deletedProducts = await prisma.product.findMany({
  where: {
    deletedAt: {
      not: null,
    },
  },
  orderBy: {
    deletedAt: 'desc',
  },
});

➕ 6-3. 주의

일반 목록에는 deletedAt null 조건 필수
상세 조회에도 deletedAt 조건 필요
관리자 복구 화면에서는 삭제된 데이터 포함
통계에서 삭제된 데이터를 포함할지 기준 필요
  • Soft Delete의 가장 흔한 실수는 조회 조건 누락입니다.
  • Repository나 공통 query helper로 기준을 정해두면 좋습니다.

✅ 7. Soft Delete와 Unique Constraint 문제

  • Soft Delete를 쓰면 unique constraint와 충돌할 수 있습니다.
  • 예를 들어 관리자 이메일에 unique가 걸려 있는데 계정을 soft delete하면, 같은 이메일로 새 계정을 만들 수 없을 수 있습니다.

➕ 7-1. 문제 예시

admin_users.email = test@example.com
deletedAt = 2026-08-19

새 관리자 생성:
email = test@example.com

결과:
unique constraint 때문에 실패

➕ 7-2. 해결 방향

삭제된 row를 복구해서 재사용
partial unique index 사용
삭제 시 email에 suffix 추가
새 계정 생성을 막고 복구만 허용

➕ 7-3. PostgreSQL Partial Unique Index

CREATE UNIQUE INDEX uniq_admin_users_email_active
ON admin_users (email)
WHERE deleted_at IS NULL;

의미:

deleted_at이 NULL인 활성 row끼리만 email unique 적용
삭제된 row의 email은 unique 검사에서 제외
  • PostgreSQL에서는 partial index가 강력한 해결책입니다.
  • Prisma schema에서 직접 표현이 제한될 수 있으므로 migration SQL을 별도로 관리해야 할 수 있습니다.

✅ 8. Soft Delete와 Foreign Key

  • Soft Delete는 row를 실제 삭제하지 않기 때문에 FK 관계를 유지하기 좋습니다.
  • 하지만 참조 대상이 삭제 처리된 상태인지에 대한 로직은 별도로 필요합니다.

➕ 8-1. 예시

consults.productId → products.id

products.deletedAt != null

상담은 과거 상품을 계속 참조 가능
하지만 고객 화면 상품 목록에서는 제외

➕ 8-2. 기준

과거 데이터:
삭제된 상품도 참조 가능

신규 생성:
삭제된 상품으로 상담 신청 불가

관리자 상세:
삭제된 상품명은 표시 가능

고객 화면:
삭제된 상품은 숨김
  • Soft Delete는 참조 관계를 유지해주지만, “신규 작업에서 삭제된 데이터를 선택할 수 있는지”는 별도로 막아야 합니다.
  • 예를 들어 삭제된 상품으로 새 상담 신청이 들어가면 안 됩니다.

✅ 9. 복구 Restore 설계

  • Soft Delete의 장점은 복구가 가능하다는 것입니다.
  • 하지만 복구도 아무렇게나 하면 안 됩니다.
  • 복구 시 현재 데이터와 충돌하지 않는지 확인해야 합니다.

➕ 9-1. 복구 기본 흐름

관리자 복구 요청
  ↓
삭제된 row 조회
  ↓
복구 가능 여부 확인
  ↓
deletedAt = null
  ↓
deletedByAdminId = null
  ↓
deleteReason = null
  ↓
Audit Log 기록

➕ 9-2. Prisma 예시

await prisma.$transaction(async (tx) => {
  const product = await tx.product.findUniqueOrThrow({
    where: { id: productId },
  });

  if (!product.deletedAt) {
    throw new BadRequestException('삭제된 상품이 아닙니다.');
  }

  await tx.product.update({
    where: { id: productId },
    data: {
      deletedAt: null,
      deletedByAdminId: null,
      deleteReason: null,
    },
  });

  await tx.auditLog.create({
    data: {
      actorType: 'ADMIN',
      actorId: adminId,
      action: 'PRODUCT_RESTORE',
      targetType: 'PRODUCT',
      targetId: String(productId),
      beforeValue: {
        deletedAt: product.deletedAt,
      },
      afterValue: {
        deletedAt: null,
      },
      requestId,
    },
  });
});

➕ 9-3. 복구 시 확인할 것

같은 unique 값의 활성 데이터가 있는가?
연결된 옵션도 함께 복구할 것인가?
복구 후 고객 화면에 바로 노출할 것인가?
isActive는 어떤 상태로 둘 것인가?
권한 있는 관리자만 복구 가능한가?
  • 복구는 삭제 취소가 아니라 운영 상태를 다시 활성화하는 작업입니다.
  • 복구 후 노출 여부까지 함께 설계해야 합니다.

✅ 10. 상품 삭제와 노출 비활성화 차이

  • 상품 운영에서는 “삭제”와 “노출 중지”를 구분해야 합니다.
  • 판매를 잠시 중단하는 것과 데이터 자체를 삭제 처리하는 것은 다릅니다.
구분의미컬럼
노출 중지고객 화면에서 숨김isActive=false
삭제 처리운영 데이터에서 삭제 상태deletedAt
품절판매 불가 상태stockStatus
예약 종료사전예약 종료reservationEndedAt

➕ 10-1. 노출 중지

isActive = false
deletedAt = null

의미:
데이터는 살아 있음
고객 화면에서는 숨김
관리자에서는 수정 가능

➕ 10-2. 삭제 처리

deletedAt != null

의미:
삭제함으로 이동
일반 관리자 목록에서도 숨길 수 있음
복구 전까지 신규 사용 제한

➕ 10-3. 실무 기준

잠시 숨기기:
isActive=false

운영에서 제거:
deletedAt 설정

과거 상담 참조:
상품 snapshot으로 보존

완전 삭제:
매우 신중히, 보통 지양
  • 상품은 대부분 hard delete보다 isActive와 deletedAt을 조합하는 것이 좋습니다.
  • 특히 사전예약, 이벤트 상품, 단종 상품은 삭제보다 비활성화가 더 안전합니다.

✅ 11. 상담 데이터는 삭제해도 될까?

  • 상담 데이터는 고객 정보가 들어 있기 때문에 조심해야 합니다.
  • 운영 기록으로도 중요하지만 개인정보 보존 정책도 고려해야 합니다.
  • 무조건 영구 보관도 답이 아니고, 무조건 삭제도 답이 아닙니다.

➕ 11-1. 상담 데이터의 특성

개인정보 포함
고객 응대 기록
유입 분석 자료
운영 성과 자료
상태 변경 이력과 연결
알림톡/SMS 발송 이력과 연결

➕ 11-2. 삭제보다 먼저 고려할 것

보존 기간이 필요한가?
개인정보 파기가 필요한가?
통계용 익명화가 가능한가?
상담 이력은 남겨야 하는가?
고객 요청 삭제인지 운영자 실수 삭제인지 구분되는가?

➕ 11-3. 후보 방식

Soft Delete:
운영 화면에서 숨김

Anonymization:
개인정보를 마스킹/삭제하고 통계만 남김

Hard Delete:
정책상 완전 삭제가 필요할 때 신중히 수행
  • 상담 데이터는 상품이나 배너보다 훨씬 민감합니다.
  • 개인정보 보존·파기 정책과 함께 설계해야 합니다.

✅ 12. 익명화 Anonymization

  • 익명화는 row 자체는 유지하되 개인을 식별할 수 있는 정보를 제거하거나 마스킹하는 방식입니다.
  • 통계는 유지하면서 개인정보 리스크를 줄일 수 있습니다.

➕ 12-1. 상담 익명화 예시

customerName:
김고객 → 익명

phone:
01012345678 → null 또는 010****5678

memo:
상담 메모 삭제

ipAddress:
null 또는 일부 마스킹

userAgent:
필요 시 삭제

➕ 12-2. Prisma 예시

await prisma.consult.update({
  where: { id: consultId },
  data: {
    customerName: '익명',
    phone: null,
    phoneNormalized: null,
    phoneLast4: null,
    memo: null,
    ipAddress: null,
    anonymizedAt: new Date(),
  },
});

➕ 12-3. 주의

익명화 후 복구 불가능하게 볼 것
상담 처리 중인 데이터에 적용 금지
통계에 필요한 값과 개인정보를 구분
상태 이력/Audit Log에도 개인정보가 없는지 확인
  • 익명화는 Soft Delete보다 강한 조치입니다.
  • 복구가 어려우므로 권한과 승인 절차가 필요합니다.

✅ 13. 데이터 보존 정책이란 무엇인가?

  • 데이터 보존 정책은 어떤 데이터를 얼마나 오래 보관하고, 언제 삭제·익명화·아카이브할지 정하는 기준입니다.
  • 운영 데이터가 계속 쌓이면 성능, 비용, 보안 리스크가 모두 커집니다.
데이터 생성
  ↓
운영 기간 동안 사용
  ↓
보존 기간 경과
  ↓
익명화 / 아카이브 / 삭제

➕ 13-1. 보존 정책이 필요한 이유

개인정보 과다 보관 방지
DB 용량 증가 관리
로그 비용 관리
운영 화면 성능 유지
보안 사고 시 피해 범위 감소

➕ 13-2. 정책에 포함할 것

데이터 종류
보존 기간
삭제 방식
익명화 방식
접근 권한
복구 가능 여부
작업 기록 방식
  • 보존 정책은 법무/회사 정책과 연결될 수 있습니다.
  • 개발자는 기술적으로 삭제·익명화·아카이브가 가능하도록 구조를 만들어야 합니다.

✅ 14. 데이터 종류별 보존 기준 후보

  • 아래는 실무 설계용 기준 예시입니다.
  • 실제 보존 기간은 회사 정책과 법적 요구사항에 맞춰 결정해야 합니다.
데이터보존 방향삭제/정리 방식
상담 신청일정 기간 보관만료 후 익명화 검토
상담 상태 이력장기 보관개인정보 제외 후 보관
상품장기 보관Soft Delete
관리자 계정비활성화 보관삭제보다 disable
Audit Log장기 보관개인정보 제외
Access Log단기 보관기간 경과 후 삭제
Debug Log짧게 보관자동 삭제
알림톡 로그운영 기준 보관수신자 마스킹
Export 파일짧게 보관만료 후 삭제

➕ 14-1. 오래 보관해도 되는 데이터

상품 기본 정보
상태 변경 이력
Audit Log
통계용 집계 데이터
익명화된 상담 통계

➕ 14-2. 짧게 보관해야 할 수 있는 데이터

원본 전화번호
상담 메모
IP Address
Access Log
임시 Export 파일
인증 토큰
  • 핵심은 “운영에 필요한 기록”과 “개인정보 원본”을 분리하는 것입니다.
  • 통계는 남기되 개인정보는 줄이는 방향이 좋습니다.

✅ 15. 아카이브 Archive

  • 아카이브는 자주 조회하지 않는 오래된 데이터를 별도 영역으로 옮기거나, 일반 조회에서 제외하는 방식입니다.
  • 상담/주문이 많아지면 운영 목록 조회 성능을 위해 아카이브 전략이 필요할 수 있습니다.

➕ 15-1. 아카이브 방식 후보

status로 ARCHIVED 처리
archivedAt 컬럼 추가
별도 archive table로 이동
S3 파일로 export 후 DB에서는 요약만 보관

➕ 15-2. 예시 컬럼

archivedAt
archivedByAdminId
archiveReason

➕ 15-3. 기준

일반 관리자 목록에서는 제외
검색 조건에서 아카이브 포함 옵션 제공
통계에는 포함할지 기준 필요
복구 가능 여부 결정
  • 지금 당장 아카이브가 필요하지는 않을 수 있습니다.
  • 하지만 데이터가 빠르게 쌓이는 구조라면 나중에 고려해야 합니다.

✅ 16. Export 파일 보존 정책

  • 엑셀 다운로드 파일은 개인정보가 포함될 가능성이 높습니다.
  • 따라서 생성 후 오래 보관하지 않는 것이 좋습니다.

➕ 16-1. ExportJob 구조

export_jobs
  - id
  - status
  - fileUrl
  - expiresAt
  - downloadedAt
  - requestedByAdminId
  - createdAt

➕ 16-2. 보존 기준 예시

다운로드 파일은 1~7일 후 만료
만료 후 S3 파일 삭제
export_jobs row는 이력으로 보관 가능
fileUrl은 만료 후 null 처리 가능
다운로드 요청자는 Audit Log에 기록

➕ 16-3. 주의

파일 URL 외부 노출 주의
권한 없는 관리자 다운로드 방지
S3 pre-signed URL 만료 시간 설정
개인정보 포함 파일 장기 보관 금지
  • 엑셀 파일은 운영상 편하지만 개인정보 유출 위험이 큽니다.
  • ExportJob은 반드시 만료와 접근권한을 고려해야 합니다.

✅ 17. 로그 보존 정책

  • 로그는 장애 분석에 필요하지만, 무제한 보관하면 비용과 보안 문제가 생깁니다.
  • 로그 종류에 따라 보존 기간을 다르게 가져가야 합니다.

➕ 17-1. 로그 종류

Application Log
Nginx Access Log
Nginx Error Log
CloudWatch Log
Audit Log
상태 변경 이력
알림 발송 로그
Debug Log

➕ 17-2. 보존 방향

Audit Log:
장기 보관

상태 변경 이력:
장기 보관

Application Error Log:
중기 보관

Access Log:
단기~중기 보관

Debug Log:
짧게 보관

개인정보 포함 가능 로그:
마스킹 또는 짧게 보관

➕ 17-3. CloudWatch Logs 주의

보관 기간 설정 안 하면 비용 증가 가능
민감정보 로그 전송 금지
Log Group별 retention 설정 필요
장애 분석에 필요한 기간은 확보
  • 로그 보존은 비용 최적화와 보안의 교차점입니다.
  • 특히 개인정보가 포함될 수 있는 로그는 더 짧고 보수적으로 관리해야 합니다.

✅ 18. Cascade Delete 주의

  • Foreign Key에는 삭제 시 관련 데이터를 어떻게 처리할지 정하는 옵션이 있습니다.
  • CASCADE는 부모 row를 삭제하면 자식 row도 함께 삭제합니다.
  • 운영 데이터에서는 매우 조심해야 합니다.

➕ 18-1. 위험한 예시

Product 삭제
  ↓
관련 Consult 삭제
  ↓
상담 상태 이력 삭제
  ↓
운영 기록 손실

➕ 18-2. 삭제 정책 종류

정책의미
CASCADE부모 삭제 시 자식도 삭제
RESTRICT자식이 있으면 부모 삭제 금지
SET NULL부모 삭제 시 자식 FK를 null
NO ACTION기본 제약 유지

➕ 18-3. 실무 기준

상담/주문/이력:
CASCADE 신중히 사용

상품/관리자:
삭제보다 soft delete 또는 disable

임시 데이터:
CASCADE 가능

핵심 운영 데이터:
RESTRICT 또는 SET NULL 검토
  • Cascade는 편하지만, 운영 데이터 손실을 크게 만들 수 있습니다.
  • 특히 상담/주문/이력 계열은 자동 삭제를 조심해야 합니다.

✅ 19. 삭제 작업과 Audit Log

  • 삭제는 중요한 관리자 작업입니다.
  • Soft Delete든 Hard Delete든 누가, 언제, 무엇을 삭제했는지 기록해야 합니다.

➕ 19-1. Audit Log action 예시

PRODUCT_SOFT_DELETE
PRODUCT_RESTORE
PRODUCT_HARD_DELETE
ADMIN_USER_DISABLE
CONSULT_ANONYMIZE
EXPORT_FILE_EXPIRE
BANNER_SOFT_DELETE

➕ 19-2. 삭제 로그 예시

{
  "actorType": "ADMIN",
  "actorId": 1,
  "action": "PRODUCT_SOFT_DELETE",
  "targetType": "PRODUCT",
  "targetId": "10",
  "beforeValue": {
    "deletedAt": null,
    "isActive": true
  },
  "afterValue": {
    "deletedAt": "2026-08-19T02:00:00.000Z",
    "isActive": false
  }
}

➕ 19-3. 주의

삭제 대상 전체 row를 beforeValue에 넣지 않기
개인정보/Secret 제외
삭제 사유 저장
복구 작업도 Audit Log로 남기기
  • 삭제와 복구는 반드시 Audit Log 대상입니다.
  • 운영 사고가 생겼을 때 가장 먼저 확인할 수 있는 근거가 됩니다.

✅ 20. 삭제 권한 설계

  • 삭제 기능은 권한을 강하게 제한해야 합니다.
  • 특히 상품 삭제, 상담 익명화, 관리자 비활성화, 엑셀 파일 삭제는 민감한 작업입니다.

➕ 20-1. 권한 후보

PRODUCT_DELETE
PRODUCT_RESTORE
CONSULT_DELETE
CONSULT_ANONYMIZE
ADMIN_USER_DISABLE
EXPORT_FILE_DELETE
AUDIT_LOG_VIEW

➕ 20-2. UI 기준

삭제 버튼은 위험 색상 사용
삭제 전 확인 모달 표시
삭제 대상명 명확히 표시
복구 가능 여부 안내
Hard Delete는 별도 권한 필요

➕ 20-3. 확인 모달 예시

상품을 삭제 처리하시겠습니까?

상품명:
Galaxy S 시리즈

삭제 후 고객 화면과 일반 관리자 목록에서 숨겨집니다.
과거 상담 기록은 유지됩니다.
삭제함에서 복구할 수 있습니다.
  • 삭제 UX는 실수를 줄이는 방향이어야 합니다.
  • “확인”만 누르게 하는 모달보다 어떤 영향이 있는지 알려주는 모달이 좋습니다.

✅ 21. Soft Delete API 설계

  • 삭제 API는 단순 DELETE /products/:id로 만들 수도 있지만, soft delete라면 의미를 명확히 하는 것이 좋습니다.

➕ 21-1. 후보 API

DELETE /admin/products/:id
POST /admin/products/:id/restore
POST /admin/consults/:id/anonymize
POST /admin/products/:id/archive

➕ 21-2. Soft Delete Service 예시

async softDeleteProduct(productId: number, adminId: number, reason?: string) {
  return this.prisma.$transaction(async (tx) => {
    const product = await tx.product.findUniqueOrThrow({
      where: { id: productId },
    });

    if (product.deletedAt) {
      throw new BadRequestException('이미 삭제된 상품입니다.');
    }

    await tx.product.update({
      where: { id: productId },
      data: {
        deletedAt: new Date(),
        deletedByAdminId: adminId,
        deleteReason: reason,
        isActive: false,
      },
    });

    await tx.auditLog.create({
      data: {
        actorType: 'ADMIN',
        actorId: adminId,
        action: 'PRODUCT_SOFT_DELETE',
        targetType: 'PRODUCT',
        targetId: String(productId),
        beforeValue: {
          deletedAt: product.deletedAt,
          isActive: product.isActive,
        },
        afterValue: {
          deletedAt: new Date(),
          isActive: false,
        },
      },
    });
  });
}

➕ 21-3. 주의

new Date()를 여러 번 호출하면 값이 미세하게 달라질 수 있음
deletedAt 값을 변수로 만들어 재사용
이미 삭제된 데이터 중복 삭제 방지
삭제와 Audit Log를 transaction으로 묶기

개선:

const deletedAt = new Date();

await tx.product.update({
  where: { id: productId },
  data: {
    deletedAt,
    deletedByAdminId: adminId,
    deleteReason: reason,
    isActive: false,
  },
});

await tx.auditLog.create({
  data: {
    actorType: 'ADMIN',
    actorId: adminId,
    action: 'PRODUCT_SOFT_DELETE',
    targetType: 'PRODUCT',
    targetId: String(productId),
    afterValue: {
      deletedAt,
      isActive: false,
    },
  },
});

✅ 22. 정기 정리 Cleanup Job

  • 데이터 보존 정책을 실제로 실행하려면 정기 정리 작업이 필요합니다.
  • 수동으로 매번 삭제하기보다 Cron/Worker로 관리할 수 있습니다.

➕ 22-1. 정리 대상

만료된 Export 파일
오래된 debug log
만료된 session/token
오래된 임시 인증 코드
처리 완료 후 오래 지난 job payload

➕ 22-2. Cleanup Job 흐름

매일 새벽 실행
  ↓
만료 대상 조회
  ↓
S3 파일 삭제
  ↓
DB 상태 EXPIRED 처리
  ↓
cleanup 결과 로그 저장

➕ 22-3. 주의

처음부터 Hard Delete하지 말고 dry-run 가능하게 만들기
삭제 대상 개수 로그 남기기
실패 시 재시도 가능하게 하기
운영 데이터 삭제 조건 명확히 하기
  • Cleanup Job은 조용히 위험한 작업입니다.
  • 조건이 잘못되면 대량 데이터가 사라질 수 있으므로 보수적으로 만들어야 합니다.

✅ 23. Dry Run

  • Dry Run은 실제 삭제를 하지 않고 어떤 데이터가 삭제 대상인지 미리 확인하는 방식입니다.
  • 정리 작업이나 대량 삭제 작업에 매우 유용합니다.

➕ 23-1. Dry Run 예시

cleanup export files --dry-run

결과:
삭제 예정 파일 124개
만료된 ExportJob 124건
예상 S3 prefix 목록
실제 삭제는 하지 않음

➕ 23-2. API/Script 기준

dryRun=true면 삭제하지 않음
대상 count 반환
일부 sample 반환
조건 로그 출력
관리자 승인 후 실제 실행

➕ 23-3. 주의

dry run 결과와 실제 실행 사이 데이터가 바뀔 수 있음
대량 삭제 전 snapshot/backup 확인
권한 있는 관리자만 실행
  • 대량 정리 작업은 dry run을 기본으로 두는 것이 안전합니다.
  • 특히 운영 DB에서 바로 삭제하는 스크립트는 위험합니다.

✅ 24. 실무 체크리스트

➕ 24-1. Soft Delete 체크리스트

  1. Soft Delete가 필요한 테이블을 정리했는가?
  2. deletedAt 컬럼이 있는가?
  3. deletedByAdminId와 삭제 사유가 필요한가?
  4. 일반 조회에서 deletedAt: null 조건이 적용되는가?
  5. 삭제함/복구 화면이 필요한가?
  6. 복구 시 unique 충돌을 확인하는가?
  7. 삭제와 복구가 Audit Log에 남는가?
  8. Hard Delete가 필요한 경우 권한이 분리되어 있는가?

➕ 24-2. 데이터 보존 체크리스트

  1. 상담 데이터 보존 기준이 있는가?
  2. 개인정보 원본 보관 기간 기준이 있는가?
  3. 익명화 대상 데이터가 정리되어 있는가?
  4. Export 파일 만료 정책이 있는가?
  5. Access/Debug log 보관 기간이 정해져 있는가?
  6. Audit Log와 상태 이력 보존 기준이 있는가?
  7. 오래된 데이터 아카이브 전략이 필요한가?
  8. Cleanup Job이 dry run을 지원하는가?

➕ 24-3. 삭제 보안 체크리스트

  1. 삭제 권한이 별도로 분리되어 있는가?
  2. 삭제 전 확인 모달이 있는가?
  3. 삭제 대상과 영향 범위를 관리자에게 보여주는가?
  4. 상담/주문 원본 데이터를 쉽게 삭제할 수 없게 되어 있는가?
  5. 개인정보 익명화는 복구 불가능성을 안내하는가?
  6. Cascade Delete가 위험하게 설정되어 있지 않은가?
  7. 삭제 스크립트 실행 전 backup/snapshot을 고려하는가?
  8. 삭제 로그에 개인정보가 그대로 남지 않는가?

✅ 25. AI에게 Soft Delete/보존 정책 설계를 물어볼 때 좋은 질문법

NestJS + Prisma + PostgreSQL 기반 온라인 휴대폰 판매몰에서 Soft Delete, 복구, 데이터 보존 정책을 설계하려고 해.

서비스 상황:
1. 상품, 상품 옵션, 배너, 상담 신청, 관리자 계정, Audit Log, ExportJob이 있음
2. 상품은 삭제해도 과거 상담 기록에서 참조될 수 있어야 함
3. 관리자 계정은 삭제보다 비활성화가 적합할 수 있음
4. 상담 데이터에는 이름, 전화번호, 메모, IP 같은 개인정보가 포함될 수 있음
5. 엑셀 Export 파일에는 개인정보가 포함될 수 있어 오래 보관하면 위험함
6. 삭제/복구 작업은 Audit Log로 남기고 싶음
7. Soft Delete는 deletedAt, deletedByAdminId, deleteReason으로 관리하려고 함
8. 일부 데이터는 보존 기간 이후 익명화 또는 정리하고 싶음

요청:
- Soft Delete 적용 후보 테이블
- Hard Delete 가능한 데이터 후보
- Prisma schema 예시
- deletedAt 조회 조건 관리 방법
- unique constraint와 soft delete 충돌 해결 방법
- 복구 API 설계
- 상품 삭제와 노출 비활성화 차이
- 상담 데이터 익명화 기준
- Export 파일 보존/만료 정책
- Cascade Delete 주의사항
- Cleanup Job과 dry run 설계
- Audit Log action 예시
를 실무 기준으로 정리해줘.

➕ 25-1. AI 답변 검증 기준

상담/주문 데이터를 쉽게 hard delete하라고 하지 않는가?
상품 삭제와 노출 비활성화를 구분하는가?
관리자 계정 삭제보다 비활성화를 고려하는가?
Soft Delete 조회 조건 누락 위험을 언급하는가?
partial unique index 같은 PostgreSQL 특성을 고려하는가?
Export 파일 개인정보 보존 위험을 설명하는가?
Cascade Delete 위험을 경고하는가?
삭제/복구 Audit Log를 제안하는가?
Cleanup Job에 dry run을 포함하는가?

📌 요약

  • 운영 DB에서 삭제는 단순히 row를 지우는 일이 아니라 과거 상담, 상품 참조, 통계, 상태 이력, Audit Log, 고객 응대 근거에 영향을 주는 중요한 작업입니다.
  • Hard Delete는 데이터를 실제로 삭제하는 방식이고, Soft Delete는 deletedAt으로 삭제 상태만 표시해 일반 조회에서 제외하는 방식입니다.
  • 상품, 상품 옵션, 배너, 관리자 계정처럼 복구 가능성과 과거 참조가 중요한 데이터는 Soft Delete가 적합합니다.
  • 상담 데이터는 개인정보와 운영 이력을 모두 포함하므로 무조건 삭제하거나 무조건 영구 보관하기보다 보존 기간, 익명화, 접근 권한 기준을 함께 설계해야 합니다.
  • Soft Delete를 적용하면 일반 조회에서 항상 deletedAt: null 조건을 넣어야 하며, 조건 누락을 막기 위해 Repository/helper 기준을 만드는 것이 좋습니다.
  • Soft Delete와 unique constraint는 충돌할 수 있으므로 PostgreSQL partial unique index 같은 해결책을 고려할 수 있습니다.
  • 상품 삭제와 노출 중지는 다릅니다. 잠시 숨기는 것은 isActive=false, 운영에서 삭제 처리하는 것은 deletedAt을 사용하는 식으로 구분하는 것이 좋습니다.
  • 복구 Restore는 deletedAt=null만 하는 작업이 아니라 unique 충돌, 노출 여부, 옵션 복구, 권한, Audit Log까지 함께 확인해야 하는 운영 작업입니다.
  • 엑셀 Export 파일은 개인정보가 포함될 수 있으므로 짧은 만료 시간, S3 파일 삭제, pre-signed URL, 다운로드 권한 관리를 함께 설계해야 합니다.
  • Cascade Delete는 핵심 운영 데이터에서 매우 위험할 수 있으므로 상담/주문/이력 계열에는 신중히 사용해야 합니다.
  • 오래된 임시 데이터나 만료된 Export 파일은 Cleanup Job으로 정리하되, 대량 삭제 전에는 dry run, 로그, 권한, backup 기준을 준비하는 것이 안전합니다.

0개의 댓글