개인정보 파기 방법: 회원 탈퇴 후 DB·백업·로그 처리법
개인정보 파기 시점과 방법, 법정 보존 데이터의 분리 보관, 백업·로그·외부 서비스 사본 처리, 파기 기록까지 회원 탈퇴 흐름을 구현 단위로 정리했습니다.
회원이 탈퇴하거나 보유기간이 끝나 개인정보가 불필요해지면 지체 없이, 복구·재생되지 않도록 파기해야 합니다(개인정보 보호법 제21조). 실무 기한은 표준 개인정보 보호지침을 근거로 흔히 정당한 사유가 없는 한 5일 이내로 잡습니다. 다른 법령이 보존을 요구하는 데이터만 예외이고, 이 데이터는 다른 개인정보와 분리해서 저장·관리해야 합니다.
개발자 관점에서 답을 옮기면 이렇습니다. deleted_at을 찍는 것은 파기가 아닙니다. 탈퇴 시점에 데이터를 ① 즉시 파기, ② 법정 보존(분리 보관), ③ 비식별 후 유지로 나누고, 운영 DB 밖의 사본까지 작업 큐로 추적한 뒤, 무엇을 언제 어떻게 지웠는지 기록으로 남겨야 합니다.
이 글은 이 흐름을 구현 단위로 풀어냅니다. 일반적인 정보 제공이 목적이므로, 업종별 보존 의무와 처리방침 문구는 법무 검토로 확정하기 바랍니다.
법이 요구하는 것: 시점, 방법, 예외
조문이 정한 뼈대는 단순합니다.
- 언제: 보유기간 경과, 처리 목적 달성, 가명정보 처리 기간 경과 등으로 불필요해졌을 때 지체 없이 파기합니다(제21조 제1항). 회원 탈퇴와 동의 철회도 처리 근거가 사라지는 대표적인 사유입니다.
- 5일 기준: 법 조문이 아니라 표준지침과 ISMS-P 해설에서 쓰는 기준입니다. 근무일로 보는 자료와 단순히 5일로 적은 자료가 함께 있으므로, 시스템은 달력 기준 5일 안에 끝나도록 설계하는 편이 안전합니다.
- 어떻게: 전자 파일은 복원이 불가능한 방법으로 영구 삭제하고, 종이는 파쇄·소각합니다(시행령 제16조).
- 예외: 다른 법령상 보존 의무가 있으면 파기하지 않되, 분리해서 저장·관리합니다(제21조 제3항).
「개인정보의 안전성 확보조치 기준」 제13조는 방법을 더 구체화합니다. 개발자에게 중요한 조항은 다음 두 가지입니다.
| 상황 | 고시가 정한 방법 |
|---|---|
| 매체 전체 파기 | 완전파괴, 전용 소자장비 삭제, 복원되지 않는 초기화·덮어쓰기 중 하나 |
| 일부만 파기(운영 DB에서 특정 회원 삭제 등) | 전자 파일은 삭제 후 복구·재생되지 않도록 관리·감독 |
| 기술적 특성상 위 방법이 현저히 곤란 | 법 제58조의2에 해당하는 정보로 처리해 복원 불가능하게 조치 |
제재는 파기 미이행이 3천만 원 이하, 분리 저장·관리 위반이 1천만 원 이하 과태료입니다(제75조). 2026년 9월 11일 시행 조문에서도 이 항목의 상한은 같게 표시되어 있습니다. 다만 같은 시기 시행령의 부과 기준이 상향됐다는 설명이 있으므로 세부 금액은 현행 원문으로 확인해야 합니다. 2026년 6월에는 개인정보보호위원회가 쿠팡 사건을 의결하면서 탈퇴 회원 개인정보 처리 체계의 개선을 함께 권고했다고 보도되었습니다. 탈퇴 후 데이터 처리가 유출 조사에서 함께 살펴볼 수 있는 항목이라는 뜻입니다. 조사에서 어떤 원인이 반복되는지는 개인정보 유출 사고 사례에 정리해 두었습니다.
소프트 삭제가 파기가 될 수 없는 이유
다음 코드는 탈퇴 API에서 자주 보는 형태입니다.
-- 취약: 화면에서만 사라지고 데이터는 전부 남는다
UPDATE users SET deleted_at = now() WHERE id = :user_id;
소프트 삭제 자체가 나쁜 것은 아닙니다. 탈퇴 철회나 실수 복구를 위한 짧은 대기 상태로는 쓸 수 있습니다. 하지만 이것을 최종 상태로 삼으면 문제가 생깁니다.
- 파기가 아닙니다: 값이 그대로 있으므로 복구·재생이 가능합니다.
- 분리도 아닙니다: 보존 대상과 파기 대상이 한 행에 섞여 같은 권한으로 조회됩니다.
- 필터가 새기 쉽습니다: 관리자 화면, 마케팅 발송 배치, 데이터 추출 쿼리 하나가
deleted_at IS NULL을 빠뜨리면 탈퇴 회원이 다시 처리됩니다. - 제약이 꼬입니다: 이메일 유니크 제약 때문에 재가입이 막히고, 이를 피하려고 값에 접미사를 붙이면 원문은 그대로 남습니다.
그렇다면 DELETE는 충분할까요? PostgreSQL 공식 문서는 UPDATE나 DELETE가 이전 행 버전을 즉시 제거하지 않으며, 일반 VACUUM도 공간을 재사용 가능하게 표시할 뿐 대개 운영체제에 돌려주지 않는다고 설명합니다. 즉 운영 DB에서 개별 행을 지울 때 스토리지 수준의 덮어쓰기를 보장하기는 어렵습니다. 고시가 일부 파기에 "삭제 후 복구·재생되지 않도록 관리·감독"을 둔 것이 현실적인 경로입니다. autovacuum이 밀리지 않는지 모니터링하고, DB 파일과 덤프 접근을 통제하는 것까지가 파기 작업의 일부입니다.
탈퇴 시점의 데이터 분류표부터 만든다
코드보다 먼저 컬럼 단위 분류표가 필요합니다. 보존 예외는 법령이 정한 기록에만 미치며, 결제 기록을 보존한다고 이용 로그나 마케팅 이력까지 남길 근거가 생기지는 않습니다.
| 기록 | 기간 | 근거 |
|---|---|---|
| 표시·광고 기록 | 6개월 | 전자상거래법 시행령 제6조 |
| 계약·청약철회 기록 | 5년 | 전자상거래법 시행령 제6조 |
| 대금결제·재화 공급 기록 | 5년 | 전자상거래법 시행령 제6조 |
| 소비자 불만·분쟁처리 기록 | 3년 | 전자상거래법 시행령 제6조 |
| 세법상 장부·증빙 | 법정신고기한 경과일부터 5년 | 국세기본법 제85조의3 |
| 인터넷 로그기록자료, 접속지 추적자료 | 3개월 | 통신비밀보호법 시행령 제41조 |
두 가지를 주의해야 합니다. 첫째, 위 표는 일반적인 예시입니다. 예를 들어 통신비밀보호법의 보관 조항은 전기통신사업자의 협조 의무를 다루므로, 우리 서비스에 적용되는지부터 확인해야 합니다. 둘째, 기간은 탈퇴일이 아니라 근거별 기산점으로 계산합니다. 세법 자료는 거래가 속한 과세기간의 법정신고기한이 기준입니다.
분류 결과는 보통 세 갈래입니다.
- 즉시 파기: 비밀번호 해시, 프로필, 연락처, 주소록, 마케팅 동의, 푸시 토큰, 행태 로그
- 분리 보관: 법정 보존 대상 거래 기록과, 그 기록의 확인에 꼭 필요한 최소 식별 항목
- 비식별 유지: 사람과 연결을 끊어도 서비스에 필요한 집계 수치
세 번째 갈래는 신중해야 합니다. user_id만 지웠어도 주문번호·배송지·자유 입력 메모로 다시 식별할 수 있다면 파기라고 보기 어렵습니다.
분리 보관 구현: 복사하고 지우고, 권한을 나눈다
법은 분리의 구체적 수준을 정하지 않고 "물리적 또는 기술적 방법"이라는 해설만 있습니다. 판단 기준은 간단합니다. 일반 서비스 코드와 일반 개발자 계정이 그 데이터를 읽을 수 없어야 합니다. 같은 테이블의 is_retained 플래그로는 이 기준을 통과할 수 없습니다. 작은 팀은 전용 스키마와 전용 계정으로 시작하고, 규모가 커지면 별도 인스턴스로 옮기는 것을 권장합니다.
CREATE SCHEMA retention;
CREATE TABLE retention.order_records (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
order_no text NOT NULL,
buyer_name text NOT NULL,
amount bigint NOT NULL,
paid_at timestamptz NOT NULL,
legal_basis text NOT NULL, -- 예: 'ecommerce_payment'
destroy_after date NOT NULL -- 근거별 기산점으로 계산
);
REVOKE ALL ON SCHEMA retention FROM app_rw;
GRANT USAGE ON SCHEMA retention TO withdrawal_worker;
GRANT INSERT ON retention.order_records TO withdrawal_worker; -- 쓰기만 허용
탈퇴 트랜잭션은 소프트 삭제 한 줄 대신 다음처럼 바뀝니다.
BEGIN;
-- 1) 보존 대상만 분리 영역으로 복사
INSERT INTO retention.order_records
(order_no, buyer_name, amount, paid_at, legal_basis, destroy_after)
SELECT o.order_no, u.name, o.amount, o.paid_at, 'ecommerce_payment',
(o.paid_at + INTERVAL '5 years')::date
FROM orders o JOIN users u ON u.id = o.user_id
WHERE o.user_id = :user_id;
-- 2) 운영 테이블에서 연결과 식별 값 제거
UPDATE orders
SET user_id = NULL, receiver_name = NULL,
receiver_phone = NULL, receiver_address = NULL
WHERE user_id = :user_id;
-- 3) 나머지 파기
DELETE FROM user_addresses WHERE user_id = :user_id;
DELETE FROM marketing_consents WHERE user_id = :user_id;
DELETE FROM users WHERE id = :user_id;
-- 4) 후속 작업 등록(외부 호출은 트랜잭션 밖에서)
INSERT INTO erasure_jobs (subject_ref, target, status)
SELECT :subject_ref, t, 'pending'
FROM unnest(ARRAY['cache','search','object_storage','crm','analytics']) AS t;
COMMIT;
subject_ref는 개인정보에서 파생하지 않은 내부 랜덤 UUID입니다. 이메일 해시를 쓰면 대조가 가능해지므로 피합니다. 분리 영역을 별도 인스턴스에 두면 하나의 트랜잭션으로 묶이지 않으니, 복사 확인 후 원본을 지우는 단계형 작업으로 바꿔야 합니다. 그 전에 세션과 리프레시 토큰을 먼저 폐기하는 것도 잊지 않아야 합니다.
분리 보관은 쌓아 두는 것으로 끝나지 않습니다. 만료 배치가 있어야 합니다.
DELETE FROM retention.order_records WHERE destroy_after <= current_date;
보존 중인 데이터의 조회는 정산·분쟁 대응 같은 목적으로 제한하고 조회 기록을 남깁니다. 접근 통제와 접속기록의 구현 항목은 개인정보 안전성 확보조치 기준 개발자 가이드를 참고하기 바랍니다.
백업: 지울 수 없다면 만료와 재적용으로 설계한다
백업에 든 개인정보도 보유기간이 끝나면 파기 대상입니다. 공식 해설도 백업 디스크나 테이프에서 특정 정보주체의 정보만 파기하는 경우를 일부 파기의 예로 듭니다. 다만 스냅샷 안의 한 행을 골라 지우기는 사실상 어렵고, 백업 전용의 세부 공식 기준은 이번 조사에서 찾지 못했습니다. 그래서 아래는 고시의 "복구·재생되지 않도록 관리·감독"을 구현하는 실무 설계로 이해하면 됩니다.
1. 보존 기간을 짧고 명시적으로 고정합니다. Amazon RDS 자동 백업의 보존 기간은 0~35일이고, 기간 안의 어느 시점으로든 복구할 수 있습니다. 즉 탈퇴 회원의 데이터가 이 기간만큼 복구 가능 상태로 남습니다. 복구 요구사항에 필요한 최소 기간을 정해 처리방침의 설명과 맞춥니다.
2. 만료되지 않는 사본을 찾습니다. 자동 백업과 달리 수동 스냅샷은 삭제할 때까지 남고, 인스턴스 삭제 시 만든 최종 스냅샷도 만료되지 않습니다. 개발 환경으로 복사한 덤프, 개인 노트북의 CSV도 같은 문제입니다. 분기마다 목록을 뽑아 소유자와 만료일이 없는 사본을 정리합니다.
3. 복구하면 재파기합니다. 가장 자주 빠지는 단계입니다. 백업을 복구하면 탈퇴 회원이 되살아나므로, 파기 대상 ID 목록을 복구 대상 DB 바깥에 보관해야 합니다. DB 안에 두면 목록도 함께 과거로 돌아갑니다.
-- 복구 런북: 서비스 트래픽을 붙이기 전에 실행
-- erasure_ledger는 외부 저장소에서 가져온 subject_ref 목록
DELETE FROM users u
USING erasure_ledger l
WHERE u.subject_ref = l.subject_ref;
-- 이후 연관 테이블, 캐시, 검색 인덱스 재색인까지 수행
이 절차는 복구 훈련에 포함해 실제로 동작하는지 확인합니다. 복구 시점이 데이터 분리 복사 이전이라면 분리 영역과의 정합성도 점검합니다.
4. 암호화 삭제는 보조 수단으로 봅니다. 민감 필드를 사용자별 키로 암호화한 뒤 키를 폐기하면 백업 속 사본도 읽을 수 없게 만들 수 있습니다. NIST SP 800-88 Rev. 2도 암호화 삭제를 매체 소독 기법으로 다루지만, 강한 전제 조건을 둡니다. 키 자체의 백업도 함께 관리해야 하고, 국내 기준에서 이 방식 단독으로 파기가 인정되는지는 확인하지 못했습니다.
로그와 외부 서비스: 사본의 지도를 그린다
운영 DB만 지워서는 끝나지 않습니다. 사본은 대개 다음 위치에 있습니다.
- 내부: Redis 캐시, 검색 인덱스, 읽기 복제본, 데이터 웨어하우스, 객체 스토리지의 프로필 이미지와 첨부파일, CDN 캐시
- 외부: 이메일·알림톡 발송, CRM, 고객센터 도구, 분석 도구, 에러 수집 도구, 결제대행사
로그는 지우는 쪽보다 남기지 않는 쪽이 훨씬 쉽습니다.
// 취약: 로그 전체를 뒤져야 파기할 수 있다
logger.info(`withdraw requested email=${user.email} phone=${user.phone}`);
// 개선: 개인정보 없이 내부 참조만 남긴다
logger.info({ event: "withdraw_requested", subject: user.subjectRef });
OWASP Logging Cheat Sheet는 민감 개인정보를 로그에서 제외하거나 마스킹하고, 이름·전화번호·이메일은 신원이 필요 없으면 비식별 처리하라고 안내합니다. 또한 로그와 그 백업·사본은 정해진 기간 전에 파기해서도, 그 기간을 넘겨 보관해서도 안 된다고 설명합니다. 로그 저장소는 종류별 보존 기간을 나눠 수명주기 규칙으로 자동 만료시킵니다. Sentry 같은 도구는 SDK의 beforeSend와 서버 측 스크러빙으로 수집 단계에서 걸러 둡니다.
로그를 분류할 때는 이용자 활동 로그와 개인정보처리시스템 접속기록을 구분해야 합니다. 후자는 개인정보취급자의 접근을 남기는 기록으로, 고시 제8조상 최소 1년, 일정 요건에 해당하면 2년 이상 보관 대상이라 탈퇴를 이유로 지우면 안 됩니다. 현행 요건은 고시 원문으로 확인하기 바랍니다.
외부 서비스는 작업 큐로 추적합니다. 호출 실패가 트랜잭션을 되돌리지 않도록 분리하고, 멱등하게 재시도합니다.
const erasers: Record<string, (ref: string) => Promise<void>> = {
cache: eraseCache,
search: eraseSearchDocs,
object_storage: eraseUserFiles,
crm: eraseCrmContact, // 이미 없으면 성공으로 처리
analytics: eraseAnalyticsProfile,
};
async function runJob(job: ErasureJob) {
try {
await erasers[job.target](job.subjectRef);
await markDone(job.id, new Date());
} catch (err) {
await scheduleRetry(job.id); // 지수 백오프
if (job.createdAt < daysAgo(3)) await alertOwner(job); // 기한 전에 사람이 개입
}
}
삭제 API가 없는 서비스는 수동 절차와 담당자를 정해 두고, 그런 도구에는 애초에 개인정보를 적게 보내야 합니다. 개인정보 처리 수탁자에게는 제21조가 준용됩니다(제26조 제8항). 위탁계약에 파기 시한과 증빙 제출을 넣고 재위탁 현황도 확인합니다. 반면 결제대행사처럼 자기 의무로 보존하는 사업자의 기록은 우리가 지우는 대상이 아니므로, 위탁과 제3자 제공 중 어떤 관계인지 먼저 정리합니다.
파기 기록: 증빙이 개인정보가 되지 않게
ISMS-P 기준은 파기 기록을 남기고 관리하도록 요구하며, 개인정보 보호책임자의 책임 아래 파기 시행과 결과 확인이 이루어져야 한다고 설명합니다. 건별 탈퇴는 시스템 기록으로, 일괄 파기는 파기 관리대장으로 남기는 구성이 현실적입니다.
{
"subject_ref": "7f0c2a64-9f1e-4c6e-8a4d-2b1f0e6d3a11",
"trigger": "member_withdrawal",
"requested_at": "2026-11-02T01:12:00Z",
"targets": {
"primary_db": "done",
"crm": "done",
"analytics": "retrying"
},
"retained": ["ecommerce_payment"],
"method": "logical_delete_with_access_control",
"backup_expires_by": "2026-12-07",
"completed_at": null
}
- 이름, 이메일, 전화번호를 넣지 않습니다. 기록 자체가 또 하나의 개인정보 저장소가 되면 안 됩니다.
backup_expires_by처럼 "복구 불가 상태가 되는 날"을 따로 둡니다. 운영 DB 삭제일과 다르기 때문입니다.- 실패와 미완료를 완료로 표시하지 않습니다. 확인되지 않은 대상은
retrying으로 남겨야 기록이 증빙으로 쓸모가 있습니다. - 일괄 파기 대장에는 대상, 건수, 방법, 담당자, 확인자, 완료일을 기록하고, 필요하면 증적 파일을 함께 보관합니다.
마지막으로 복구 지도를 점검합니다. 테스트 계정을 탈퇴시킨 뒤 운영 DB, 검색, 캐시, 파일 URL, 외부 도구에서 실제로 조회되지 않는지 확인하는 자동 검증을 분기마다 돌리면 신규 테이블이 분류에서 누락되는 문제를 잡을 수 있습니다.
탈퇴 처리 점검표
- 개인정보 컬럼별로 파기·분리 보관·비식별 분류와 근거가 문서화되어 있다
- 새 테이블과 새 외부 서비스 추가 시 분류표 갱신이 리뷰 항목이다
- 세션·토큰이 탈퇴 즉시 폐기된다
- 분리 영역을 일반 앱 계정과 개발자가 조회할 수 없다
- 분리 데이터에
destroy_after가 있고 만료 배치가 동작한다 - 수동 스냅샷·덤프에 소유자와 만료일이 있다
- 백업 복구 런북에 재파기 단계가 있고, 파기 목록은 DB 밖에 있다
- 로그에 이메일·전화번호가 남지 않고 보존 기간이 자동 적용된다
- 외부 삭제 실패가 재시도되고 기한 전에 알림이 간다
- 처리방침의 보유 항목·기간과 실제 시스템 동작이 일치한다
자주 묻는 질문
탈퇴 후 복구를 위해 며칠 유예해도 되나요?
법은 "지체 없이"를 요구하므로 유예는 짧을수록 안전합니다. 둔다면 처리방침에 알리고, 유예 중에는 로그인과 마케팅 등 모든 처리를 막고, 5일 기준 안에서 끝내는 쪽을 권합니다. 30일 유예처럼 긴 기간이 정당한 사유가 되는지는 법무 검토 없이 단정하기 어렵습니다.
탈퇴 회원 한 명 때문에 백업 전체를 지워야 하나요?
일반적으로는 그렇게 운영하지 않습니다. 백업 보존 기간을 짧게 고정해 만료로 사라지게 하고, 그 전까지 접근을 통제하며, 복구 시 재파기하도록 설계합니다. 다만 만료 없는 수동 스냅샷은 이 논리가 통하지 않으므로 정리 대상입니다.
쿠폰 부정 재가입을 막으려고 탈퇴자 정보를 남겨도 되나요?
보존 법령이 없다면 별도의 처리 근거와 고지가 필요하고, 항목과 기간도 최소화해야 합니다. 이메일 해시를 남겨도 대조가 가능하므로 파기로 보기 어렵습니다. 처리방침에 명시할 수 없는 목적이라면 남기지 않는 것이 맞습니다.
정리
개인정보 파기는 삭제 쿼리 하나가 아니라 분류, 분리, 사본 추적, 기록으로 이어지는 흐름입니다. 소프트 삭제는 대기 상태로만 쓰고, 법정 보존 데이터는 필요한 항목만 권한이 나뉜 영역으로 옮겨 만료일을 붙입니다. 백업은 짧은 보존 기간과 복구 시 재파기로 다루고, 로그는 개인정보를 처음부터 남기지 않습니다. 직접 점검이 어렵다면 무료 분석 신청으로 현재 구조를 확인받아 볼 수 있습니다.
참고 자료
함께 읽으면 좋은 글
Django 보안 설정 가이드: check --deploy가 못 잡는 빈틈
Django 보안 설정을 배포 전에 점검하는 가이드다. check --deploy가 잡는 항목과 놓치는 DRF 기본 권한, 객체 권한, 프록시 HTTPS, Admin 노출을 고친 코드로 확인한다.
레이스 컨디션 취약점이란? 포인트 이중 사용의 원인과 방어법
레이스 컨디션 취약점으로 포인트 이중 사용과 선착순 초과가 생기는 원리, 트랜잭션만으로 부족한 이유, 원자적 업데이트·행 잠금·유니크 제약 선택 기준과 점검법을 코드로 정리했습니다.
웹서비스 출시 전 보안 점검표: 배포 환경에서 놓치는 10가지
출시 전 보안 점검을 코드 밖 배포 환경에 집중해 정리했습니다. 디버그 모드, .git, 백업 파일, 스테이징, 기본 계정 등 10가지를 curl로 확인하고 고치는 방법을 얻을 수 있습니다.