개인정보 보유·파기 기능 설계
1. 문서 상태와 목표
- 상태: 설계 확정, 구현 대기
- 적용 범위: 문의, 계정, 프로젝트·에셋·생성물, 결제·크레딧, 로그, 이메일 Outbox, S3 객체, 외부 AI 제공자 데이터
- 목표: 개인정보가 불필요해졌을 때 정해진 정책에 따라 삭제하고, 법정 보존 대상만 분리·최소화하여 보관하며, 파기 결과를 개인정보 원문 없이 검증할 수 있게 한다.
이 문서는 제품의 보관함·휴지통 기능과 개인정보 파기를 구분한다. 보관함은 사용자가 다시 복원할 수 있는 제품 상태이고, 개인정보 파기는 복원을 전제로 하지 않는 수명주기 종료 작업이다.
2. 법적 기준
설계의 기준은 다음과 같다.
- 개인정보가 불필요해지면 지체 없이 파기한다.
- 전자 파일은 복구 또는 재생되지 않도록 영구 삭제한다.
- 다른 법령에 따라 보존할 정보는 서비스 이용 데이터와 분리하여 저장·관리한다.
- 전자상거래의 계약·청약철회와 결제·공급 기록은 5년, 소비자 불만·분쟁 기록은 3년, 표시·광고 기록은 6개월 보존한다.
참고:
법령 해석이 필요한 예외는 운영자가 임의로 결정하지 않고 법률 검토 후 LEGAL_HOLD로 기록한다.
3. 현재 구현과 확인된 공백
현재 구현
- 문의는
support_inquiries에 이메일, 유형, 제목, 본문, 상태와 시각을 저장한다. - 로그인 문의는
userId와 계정 이메일을 사용하고, 비로그인 문의는 제출 이메일을 사용한다. - 문의 알림은
email_outbox에 본문을 포함한 Payload를 저장한 뒤 AWS SES로 발송한다. - Cinema 일부 데이터는 휴지통 이동 후 30일이 지나면 Cron 작업으로 DB에서 삭제한다.
- S3 객체 삭제 기능은
StorageService에 존재하지만, 계정 단위 파기 목록을 통합 관리하지 않는다.
공백
- 문의 처리 완료 시각과 정책별 파기 예정 시각이 없다.
- 문의 본문이
support_inquiries와email_outbox.payload에 중복 저장되고 Outbox Payload의 별도 파기 정책이 없다. - 계정 전체에 연결된 DB 행과 S3 객체를 찾고 삭제하는 중앙 오케스트레이터가 없다.
- 법정 보존 대상의 분리 저장, Legal Hold, 외부 제공자 삭제 요청, 파기 검증과 재시도 표준이 없다.
- 계정 삭제 API와 이용자용 진행 상태 화면이 없다.
4. 보유기간 기준표
| 데이터 등급 | 기준 시점 | 기본 보유기간 | 종료 처리 |
|---|---|---|---|
| 일반 서비스 문의 | closedAt | 1년 | 문의 원문과 답변 이메일 삭제 |
| 개인정보 권리 행사·고충 | closedAt | 1년 | 본문 삭제, 비식별 파기 증적만 유지 |
| 소비자 불만·분쟁 문의 | 접수 또는 처리 완료 시점 중 법령상 기준 | 3년 | 분리 보존 후 삭제 |
| 계약·청약철회 기록 | 거래일 | 5년 | 분리 보존 후 삭제 |
| 결제·서비스 공급 기록 | 거래일 | 5년 | 분리 보존 후 삭제 |
| 표시·광고 기록 | 게시 종료일 | 6개월 | 삭제 |
| 서비스 이용·보안 로그 | 수집일 | 3개월 | 삭제 또는 비식별 통계화 |
| 사용자가 휴지통에 넣은 Cinema 콘텐츠 | trashedAt | 30일 | DB 및 연결 미디어 삭제 |
| 계정·프로필·일반 콘텐츠 | 탈퇴 파기 시작 | 파기 작업 완료 시까지 | 법정 보존분을 제외하고 삭제 |
| 이메일 전송 Payload | 발송 성공 또는 영구 실패 | 30일 | 수신자·본문·사용자 식별값 비식별화 |
| 이메일 전송 메타데이터 | 발송 성공 또는 영구 실패 | 3개월 | 행 삭제 |
| 파기 수행 증적 | 파기 완료 | 3년 | 개인 콘텐츠 없이 상태·시각·건수만 보관 후 삭제 |
PAYMENT 유형 문의라는 이유만으로 모든 문의를 3년 보관하지 않는다. 실제 환불, 청약철회, 소비자 불만 또는 분쟁 처리에 사용된 경우에만 관리자가 보유 등급을 CONSUMER_DISPUTE로 변경한다.
5. 핵심 데이터 모델
5.1. 문의 수명주기 확장
support_inquiries에 다음 필드를 추가한다.
| 필드 | 형식 | 역할 |
|---|---|---|
resolvedAt | timestamptz nullable | 실질 답변·조치 완료 시각 |
closedAt | timestamptz nullable | 후속 처리를 종료한 시각 |
retentionClass | enum | STANDARD, CONSUMER_DISPUTE, LEGAL_HOLD |
purgeAfter | timestamptz nullable | 정책에서 계산된 파기 예정 시각 |
legalHoldReason | varchar nullable | 제한된 관리자용 보존 사유 코드 |
legalHoldUntil | timestamptz nullable | 보존 사유 검토 또는 종료 예정 시각 |
status=CLOSED 전환은 closedAt과 purgeAfter를 같은 트랜잭션에서 기록해야 한다. LEGAL_HOLD에는 만료 없는 보존을 허용하지 않고 검토 시각을 필수로 둔다.
5.2. 파기 요청
privacy_erasure_requests
id,subjectUserId,scope,statusrequestedAt,identityVerifiedAt,startedAt,completedAtpolicyVersion,failureCode,nextRetryAt- 완료 후
subjectUserId는 비가역 HMAC 식별값으로 치환하고 원래 사용자 ID는 제거한다.
상태는 다음 순서를 사용한다.
REQUESTED
-> VERIFIED
-> ACCESS_REVOKED
-> LEGAL_RECORDS_SEPARATED
-> PURGING
-> EXTERNAL_DELETION_PENDING
-> VERIFYING
-> COMPLETED
재시도 가능한 실패는 RETRY, 운영자 판단이 필요한 실패는 BLOCKED로 둔다. 한 요청은 Idempotency Key로 한 번만 생성되며 모든 단계는 반복 실행해도 같은 결과를 내야 한다.
5.3. 파기 작업과 증적
privacy_erasure_tasks
- 요청별 도메인 작업 단위:
AUTH,PROFILE,PERSONA,STYLING,CINEMA,WORLDS,CREATIONS,STORAGE,EMAIL,EXTERNAL_PROVIDER status,attempts,startedAt,completedAt,lastErrorCode- 삭제 대상의 원문, 이메일, S3 경로는 완료 증적에 남기지 않는다.
- 완료 결과는 삭제 건수, 실패 건수와 HMAC 처리한 매니페스트 Digest만 보관한다.
privacy_legal_holds
- 법적 근거 코드, 적용 범위, 시작·검토·종료 시각, 승인 관리자
- 자유 서술에는 개인정보를 넣지 않고 사내 사건 번호만 기록한다.
6. 서비스 구조
6.1. Retention Policy Registry
보유기간을 서비스 코드 곳곳에 숫자로 하드코딩하지 않는다. 공통 Registry가 다음 값을 제공한다.
type RetentionPolicy = {
policyKey: string;
trigger: "CREATED_AT" | "CLOSED_AT" | "TRANSACTION_AT" | "TRASHED_AT";
durationDays: number;
legalBasis: string;
policyVersion: string;
};
정책 변경 시 기존 행의 purgeAfter를 무조건 덮어쓰지 않는다. 새 정책 버전의 적용 범위와 소급 여부를 Migration 또는 관리 작업으로 명시한다.
6.2. 도메인 파기 Adapter
각 도메인은 공통 PersonalDataErasureContributor를 구현한다.
interface PersonalDataErasureContributor {
domain: string;
discover(subjectId: string): Promise<ErasureManifest>;
separateLegalRecords(manifest: ErasureManifest): Promise<void>;
purge(manifest: ErasureManifest): Promise<ErasureResult>;
verify(manifestDigest: string): Promise<VerificationResult>;
}
중앙 서비스는 도메인 테이블 구조를 직접 알지 않고 Adapter 결과를 조율한다. 새 기능은 출시 조건으로 Adapter 또는 “개인정보 없음” 선언을 등록해야 한다.
6.3. Scheduler와 Worker
RetentionScheduler: 매일 파기 기한이 지난 행을 최대 N개씩 조회한다.ErasureWorker:FOR UPDATE SKIP LOCKED로 작업을 임대하고 단계별로 실행한다.VerificationWorker: DB 참조, S3 객체, Outbox Payload와 외부 요청 상태를 재검사한다.- 운영자 Dashboard: 기한 초과, 실패, Legal Hold 만료 예정 작업만 표시한다.
대량 계정 삭제를 API 요청 안에서 동기 처리하지 않는다. API는 요청을 기록하고 접근을 차단한 뒤 Worker가 비동기로 파기한다.
7. 삭제 순서
- 최근 로그인 또는 비밀번호 재확인으로 본인을 확인한다.
- 삭제 범위, 복구 불가 항목, 법정 보존·블록체인 예외와 데이터 내려받기 필요성을 표시하고 최종 확인을 받는다.
- 로그인 세션과 Refresh Token을 폐기하고 계정을
ERASURE_PENDING으로 전환한다. - 결제·환불·분쟁 기록 중 법정 보존 대상을 최소 필드로 분리한다.
- 공개·큐레이션 항목을 비공개로 전환하고 공유 링크를 만료시킨다.
- 도메인 Adapter가 자식 행과 미디어 경로를 수집해 매니페스트 Digest를 만든다.
- S3 객체를 먼저 삭제 큐에 등록한 뒤 DB 참조와 행을 삭제한다. 객체 삭제 실패 시 DB에서 경로를 잃지 않도록 작업 매니페스트는 완료 전까지 암호화하여 유지한다.
- 이메일 Outbox의 개인 Payload와 인증 토큰·캐시·알림을 삭제한다.
- 외부 AI 제공자에 저장된 리소스 식별자가 있고 삭제 API가 제공되면 삭제 요청을 실행한다.
- 잔존 참조를 검증하고 완료 증적만 남긴다.
8. S3와 백업 처리
S3
- 현재
StorageService.deleteFile()을 사용하되, 계정 파기는 개별 도메인의 임의 호출이 아니라STORAGE작업 큐를 거친다. - 동일 S3 객체를 여러 에셋이 참조할 수 있으므로 참조 수를 확인하고 마지막 참조가 삭제될 때만 객체를 삭제한다.
- Versioning 사용 버킷은 Delete Marker만 생성하고 과거 버전이 남을 수 있다. 비현재 버전의 만료 Lifecycle을 별도로 설정하고 정책 문구와 실제 일수를 일치시킨다.
- Multipart Upload 중단 조각과 임시 버킷 객체에도 Lifecycle을 적용한다.
백업
- 운영 DB 복구 백업은 파기된 데이터를 서비스로 다시 가져오기 위한 수단으로 사용하지 않는다.
- 백업은 접근 권한을 제한하고 정해진 교체 주기가 끝나면 만료한다.
- 사고 복구로 과거 백업을 복원하면 완료된 파기 요청 목록을 먼저 재적용한 뒤 일반 서비스를 재개한다.
- 운영 전 실제 RDS·S3 백업 보존일을 확인하고 개인정보 처리방침에 최대 잔존 기간을 명시한다.
9. 외부 제공자와 삭제 불가능한 기록
- OpenAI, World Labs 등에는 서비스 사용자 ID 대신 요청별 가명 식별자를 사용한다.
- Provider 리소스 ID와 삭제 지원 여부를 생성 시점에 저장한다.
- 삭제 API가 없으면 계약상 보유기간과 자동 만료를 기록하고, 기능 화면과 처리방침에 제한을 알린다.
- 공개 블록체인의 지갑 주소와 거래 해시는 삭제할 수 없다. SurfAI 내부 계정과의 연결만 제거하고 법정 거래 기록은 분리 보관한다.
10. API와 UX
이용자 API
POST /api/v1/privacy/erasure-requests: 계정 전체 삭제 요청GET /api/v1/privacy/erasure-requests/current: 현재 요청 상태와 예외 조회POST /api/v1/privacy/data-access-requests: 열람·내보내기 요청
계정 삭제 화면은 삭제 대상별 예상 건수, 보존되는 법정 기록, 공개 블록체인 예외, 외부 Provider 처리 상태와 복구 불가 경고를 표시한다. 삭제 버튼은 재인증 후에만 활성화한다.
관리자 API
GET /api/v1/admin/privacy/erasure-requestsPOST /api/v1/admin/privacy/erasure-requests/:id/retryPOST /api/v1/admin/privacy/erasure-requests/:id/legal-holdsDELETE /api/v1/admin/privacy/erasure-requests/:id/legal-holds/:holdId
관리자도 개인정보 원문이 아니라 작업 상태와 오류 코드 중심으로 조회한다.
11. 구현 순서
Phase 1: 문의 수명주기
support_inquiries수명주기 필드와 관리자 종료 API 추가- 문의 종료 시
retentionClass기반purgeAfter계산 CONTACT_INQUIRYOutbox Payload 30일 비식별화와 3개월 후 행 삭제- 매일 실행되는 문의 파기 Scheduler, 실패 재시도와 단위 테스트 추가
Phase 2: 개인정보 Inventory
- 사용자 ID를 직접·간접 참조하는 모든 DB 테이블 목록 생성
- S3 경로 소유권과 공유 참조 Registry 구축
- 도메인별 Erasure Contributor와 Dry Run 결과 구현
Phase 3: 계정 삭제
- 재인증, 삭제 영향 미리보기와 파기 요청 API
- 계정 접근 차단, 법정 보존 분리와 도메인 파기 Worker
- 이용자 상태 화면과 관리자 실패 처리 화면
Phase 4: 외부·백업 검증
- Provider별 삭제 또는 만료 Adapter
- RDS·S3 백업 Lifecycle 점검과 복원 후 재파기 Runbook
- 정기 잔존 데이터 Scan과 정책-코드 일치 보고서
12. 완료 기준
- 만료된 일반 문의와 Outbox 원문이 자동으로 삭제된다.
- 소비자 분쟁 문의는 3년 동안 일반 문의와 분리되어 목적 외로 조회되지 않는다.
- 계정 삭제 Dry Run의 DB·S3 수량과 실제 삭제·검증 수량이 일치한다.
- 같은 파기 작업을 반복해도 데이터 손상이나 오류 없이 완료된다.
- S3 Versioning, 임시 객체, 중단 Multipart Upload와 백업의 최대 잔존 기간이 문서화된다.
- 외부 Provider와 공개 블록체인처럼 SurfAI가 직접 지울 수 없는 데이터가 이용자 결과 화면에 구분되어 표시된다.
- 개인정보 처리방침의 수집 항목·보유기간·파기 방법이 운영 설정과 자동 검사로 일치한다.