회원탈퇴 시 개인정보는 다 지우라면서, 당일 재가입은 막으라고?
회원탈퇴 버튼 하나에 서로 반대 방향을 보는 요건이 두 개 붙습니다. 하나는 탈퇴하면 개인정보를 지우라는 요건이고, 다른 하나는 탈퇴한 당일에는 다시 가입하지 못하게 막으라는 운영 정책입니다. 어떤 급식서비스의 회원 기능을 손보면서 이 둘이 같은 화면에서 만났습니다.
따로 떼어놓고 보면 둘 다 평범한 요구입니다. 붙여놓으면 서로를 지웁니다. 이름과 전화번호, 본인인증 연계정보(CI) 암호문까지 전부 지우고 나면 다시 가입하려는 사람이 오늘 탈퇴한 그 사람인지 확인할 방법이 남지 않습니다. 반대로 차단하려고 회원 정보를 그대로 들고 있으면 삭제 요건이 무너집니다.
다 지우면 오늘 탈퇴한 사람을 알아볼 수가 없습니다
탈퇴 처리를 코드로 열어보면 문제가 더 선명해집니다. 삭제 대상은 개인을 식별할 수 있는 값 전부입니다. 그런데 재가입 차단은 정확히 그 식별 능력에 기대는 기능입니다. 지워야 하는 것과 필요한 것이 같은 값을 가리키고 있었습니다.
처음 떠올린 방법은 탈퇴 회원 정보를 별도 테이블로 옮겨 담는 것이었습니다. 자리만 바꾸면 삭제한 것처럼 보입니다. 실제로는 이름만 바꾼 보존입니다. 유출 리스크는 그대로 남고, 처리방침에 적어둔 문장과도 어긋납니다.
질문을 바꿨습니다 — 막는 데 필요한 최소 신호는 무엇인가
무엇을 지울까를 묻는 대신, 막는 데 필요한 최소 신호가 무엇인지부터 좁혔습니다. 차단 판정이 답해야 하는 질문은 하나뿐이었습니다. 이 사람이 오늘 탈퇴했는가입니다. 이름도 전화번호도 그 판정에는 쓰이지 않습니다.
선택지 | 개인정보 최소화 | 재가입 차단 | 판단 |
|---|---|---|---|
탈퇴 정보를 전부 보존 | 위반 | 가능 | 유출 리스크를 계속 안고 갑니다 |
전부 삭제 | 충족 | 불가 | 탈퇴와 재가입 반복에 무방비입니다 |
단방향 해시만 기간 한정 보존 | 충족 | 가능 | 두 요건의 교집합이라 채택했습니다 |
그래서 탈퇴 시점에 CI 암호문과 나머지 개인정보는 지우고, CI 를 단방향 해시로 바꾼 값 하나만 남겼습니다. 원문을 되돌릴 수 없는 값이라 들고 있는 부담이 작습니다. 같은 사람이 다시 본인인증을 하면 같은 해시가 나오므로 대조는 그대로 됩니다. 남긴 것은 사람이 아니라 대조용 지문 한 줄입니다.

가입 검사는 세 갈래로 갈립니다
가입 요청이 들어오면 판정은 순서대로 세 갈래를 탑니다.
이미 쓰고 있는 기존 회원인지 먼저 확인합니다.
기존 회원이 아니면 보존된 해시와 대조해, 당일 탈퇴한 사람이면 재가입을 차단합니다.
둘 다 아니면 신규 가입으로 처리합니다.
순서가 중요합니다. 기존 회원 판정을 맨 앞에 두면 이미 쓰고 있는 계정은 차단 로직을 아예 지나가지 않습니다. 차단 검사는 신규 가입 경로에만 얹히므로 기존 사용자의 로그인 경험에는 영향을 주지 않습니다.
본인인증 단계의 검사는 안내이고, 방어는 서버에서 끝냅니다
차단 검사를 본인인증 단계에 붙여두면 화면 흐름은 깔끔해집니다. 사용자는 가입 정보를 다 채우기 전에 막힌 이유를 봅니다. 다만 그 검사는 앞단 안내이지 방어가 아닙니다. 가입 API 를 직접 호출하면 그 단계는 통째로 건너뛸 수 있습니다.
그래서 가입을 실제로 처리하는 서버에서 당일 차단을 한 번 더 검사합니다. 같은 조건을 두 번 쓰는 것이 중복처럼 보이지만 두 검사는 목적이 다릅니다. 앞의 것은 사용자에게 이유를 알려주기 위한 것이고, 뒤의 것은 우회 호출을 막기 위한 것입니다.

하루만 보관한다는 말은 지우는 장치까지가 구현입니다
보존 기간을 정하는 것과 그 기간이 지나면 실제로 지워지는 것은 별개입니다. 코드 주석에 하루만 보관한다고 적어둬도 지우는 쪽 코드가 없으면 그 값은 계속 쌓입니다. 기간 한정 보존은 삭제 장치가 붙어야 완성됩니다.
지우는 코드가 없는 기간 한정 보존은, 문서에서만 기간이 한정된 영구 보관입니다.
그래서 만료된 해시를 매일 새벽에 정리하는 스케줄러를 같이 만들었습니다. 인스턴스가 여러 대인 환경에서는 같은 배치가 동시에 여러 번 돌 수 있으므로 중복 실행 방지 락을 걸었습니다.
삭제 배치는 두 번 돌아도 결과가 같은 편입니다. 그래도 락을 건 이유는 로그 때문입니다. 같은 삭제가 두 벌로 남으면 나중에 어떤 값이 언제 지워졌는지를 재구성하기 어려워집니다. 보존 기간을 다루는 기능은 그 자체가 감사 대상이라 기록이 깨끗해야 합니다.

삭제를 막은 건 NOT NULL 이었습니다
탈퇴 처리에서 전화번호를 비우는 코드가 저장 단계에서 막혔습니다. 지워야 하는 컬럼에 NOT NULL 제약이 걸려 있었습니다. 값이 반드시 있어야 한다는 제약과 값을 반드시 지워야 한다는 요건이 정면으로 부딪힌 것입니다.
제약을 완화하는 DDL 이 코드 배포보다 먼저 나가야 했습니다. 지울 대상 컬럼 목록을 뽑았다면 그 컬럼의 NOT NULL 과 유니크 제약을 같은 자리에서 확인해두는 편이 낫습니다. 개인정보 삭제 설계는 스키마 제약 감사와 세트로 볼 때 운영 반영 직전의 소동이 줄어듭니다.
안내 문구와 코드가 서로 다른 말을 하고 있었습니다
작업 중에 하나가 더 나왔습니다. 앱 탈퇴 안내에는 동일한 아이디를 영구히 다시 쓸 수 없다고 적혀 있었습니다. 실제 구현은 탈퇴 시 아이디를 비우고 있어서, 차단 기간이 지나면 같은 아이디로 다시 가입이 됩니다. 문구와 동작이 어긋나 있었습니다.
이건 코드에서 조용히 결정할 문제가 아니었습니다. 아이디를 영구히 막는 쪽으로 코드를 문구에 맞출 수도 있고, 문구를 실제 동작에 맞춰 고칠 수도 있습니다. 어느 쪽이든 사용자 고지와 CS 응대에 영향을 줍니다. 그래서 두 안과 CS용 차단 해제 기능이 필요한지 여부를 붙여 정책 결정 항목으로 올렸습니다.
고지 문구는 정책의 일부입니다. 문구와 동작이 다르면 언젠가 문의가 들어오고, 그때는 개발자가 아니라 응대하는 쪽이 답을 만들어야 합니다.
남는 패턴
지운다와 막는다가 충돌하면 막는 데 필요한 최소 신호부터 좁힙니다. 원문이 아니라 해시나 불리언, 타임스탬프 수준으로 충분한 경우가 많습니다.
기간 한정 보존은 삭제 배치까지가 구현입니다. 지우는 장치가 없으면 사실상 영구 보관입니다.
차단과 제한 검사의 최종 관문은 서버입니다. 화면 흐름 안의 검사는 안내 역할입니다.
개인정보 삭제 설계는 스키마 제약 감사와 같이 갑니다. NOT NULL 과 유니크가 삭제를 막습니다.
탈퇴 설계 점검 항목
탈퇴 시 지우는 값과 차단에 쓰는 값이 각각 무엇인지 목록으로 적혀 있는지
차단용으로 남기는 값이 원문이 아니라 복원 불가능한 형태인지
보존 기간이 지난 값을 실제로 지우는 배치가 있고 중복 실행 방지가 걸려 있는지
차단 검사가 화면 흐름뿐 아니라 처리 서버에서도 한 번 더 도는지
앱과 웹의 안내 문구가 현재 구현과 같은 말을 하고 있는지
남은 것과 한계
당일 재가입 차단과 개인정보 삭제는 결국 같이 굴러갑니다. 남긴 값은 대조용 해시 한 줄이고, 그 값도 하루가 지나면 배치가 지웁니다.
한계도 같이 적어둡니다. 해시도 식별 가능성 관점에서는 개인정보로 볼 여지가 있습니다. 이 경우는 보존 기간이 하루라 부담이 작았던 것이고, 기간을 늘리면 처리방침의 보유 목적과 기간 기재를 다시 봐야 합니다. 당일 차단은 약한 방어이기도 합니다. 혜택 초기화를 노린 반복 가입이 실제로 관측되면 차단 기간을 늘려야 하는데, 그건 보존 기간을 늘리는 대가를 요구합니다.
미확정으로 남긴 정책은 결정 대기 항목으로 따로 관리하고 있습니다. 코드가 먼저 답을 정해버리면 나중에 정책이 반대로 정해졌을 때 되돌리는 비용이 사용자 고지까지 번집니다.