AI 코딩 어시스턴트와 SaaS 만들기 ②: AI가 내 계정을 삭제했던 날
시작하며
지난 편에서 MatchDa의 URL 구조와 잡 탐색 플로우를 정리한 이야기를 했다. 이번 편은 같은 세션 안에서 벌어진, 훨씬 아찔한 사건에 대한 기록이다.
결론부터 말하면 — AI 코딩 어시스턴트가 테스트 계정을 정리하겠다며 실행한 스크립트가, 내 실제 계정을 통째로 삭제했다. 프로필, 매칭 기록(matches), 등록해둔 채용페이지(job_sources), 그동안 만들어둔 맞춤 이력서(tailored_resumes)까지 전부 사라졌다. Free 플랜을 쓰던 Supabase 프로젝트라 백업 복원도 불가능했다.
이 이야기를 굳이 블로그에 남기는 이유는, "AI에게 DB 작업을 맡겨도 되는가"라는 질문에 실제 사례로 답하고 싶어서다. 답은 "맡기되, 방식이 중요하다"이다.
무슨 일이 있었나
세션 중간에 테스트용 더미 계정을 정리할 일이 있었다. 회원가입 플로우를 테스트하면서 만든 임시 유저들이 Supabase Auth에 쌓여 있었고, 이걸 지우는 게 목적이었다.
이때 사용한 방법이 문제였다. Supabase(GoTrue) admin REST API로 이메일을 필터링해서 특정 유저를 조회한 뒤 삭제하는 방식을 썼다.
GET /auth/v1/admin/users?email=테스트계정@example.com
여기서 치명적인 함정이 있었다. 이 엔드포인트는 email 쿼리 파라미터를 사실상 무시하고, 페이지네이션된 전체 유저 목록을 그대로 반환한다. 존재하지 않는 이메일로 조회해도 목록의 첫 번째 유저가 나온다. 겉보기엔 필터링된 결과처럼 보이지만 실제로는 아니었던 것이다.
그 결과 목록의 [0]번 유저를 "테스트 계정"으로 착각하고 삭제 API를 호출했는데, 그 [0]번이 하필 실제 서비스를 쓰던 내 계정이었다. Supabase Auth에서 유저가 삭제되면 profiles 테이블부터 시작해 matches, job_sources, tailored_resumes까지 외래키로 물려 있던 데이터가 전부 cascade 삭제로 함께 날아갔다.
그나마 다행이었던 건 공유 테이블인 jobs(회사·공고 정보, 특정 유저에 종속되지 않는 데이터) 319건은 살아남았다는 점이다. 이게 나중에 복구의 실마리가 됐다.
원인을 파고들기
사고가 난 직후 바로 복구에 뛰어들지 않고, 먼저 왜 이런 일이 가능했는지부터 확인했다. 재발 방지가 없으면 복구해봐야 의미가 없으니까.
Supabase의 admin 유저 조회 API 동작을 다시 테스트해보니 명확했다.
GET /auth/v1/admin/users?email=aaaaaaa-존재하지않는이메일@example.com
이 요청도 200 OK와 함께 유저 목록을 반환했다. email 파라미터가 필터로 전혀 동작하지 않고 있었던 것이다. 즉 이 API의 응답에서 "특정 이메일의 유저"라고 신뢰할 수 있는 필드는 없고, 응답은 사실상 "전체 유저 중 일부 페이지"였다.
문제는 이 특성을 모르고 [0]을 그대로 "찾던 그 계정"이라고 믿어버린 것이다. 코드로 보면 이런 실수였다.
// 잘못된 가정 — email 필터가 실제로 동작한다고 믿음
const res = await fetch(`${SUPABASE_URL}/auth/v1/admin/users?email=${targetEmail}`, {
headers: adminHeaders,
})
const { users } = await res.json()
await deleteUser(users[0].id) // 필터가 안 먹혔으므로 이건 "아무 유저"였다
복구 과정
다행히 실마리가 있었다.
- 공유
jobs테이블은 유저에 종속되지 않아 살아있었다. 회사명, 공고 제목, 설명 등 원본 채용공고 데이터 319건이 그대로 남아있었다. - 삭제된 것은 "내 계정과 잡의 연결"(
matches,job_sources,tailored_resumes)과 "내 프로필"(profiles)이었다.
복구 전략은 이렇게 잡았다.
- 새로 로그인해서 프로필을 다시 생성한다 (
getOrCreateProfile로직이 이미 있어서 자동으로 처리됨) - 살아있는
jobs데이터를 다시 계정에 연결해서, 적어도 "공고 풀"은 처음부터 다시 수집하지 않아도 되게 한다 - 사라진 것(과거에 만들어둔 맞춤 이력서, 지원 상태 기록, 개인 프로필 정보)은 되살릴 수 없다는 걸 인정하고 다시 채워 넣는다
즉 완전 복구는 아니었다. 읽기 전용으로 남아있던 공유 데이터만 복구되고, 사용자 개인 데이터(이력서, 지원 이력)는 영구히 소실됐다. 이 시점에서 가장 중요했던 건 복구 기술이 아니라, 사고 경위를 사용자에게 숨기지 않고 투명하게 보고하는 것이었다. "무엇이 왜 사라졌는지", "무엇을 복구할 수 있고 무엇은 안 되는지"를 먼저 명확히 하고 나서야 이후 작업을 이어갈 신뢰가 생겼다.
재발 방지: 세운 규칙
이 사고 이후 두 가지 규칙을 명시적으로 세우고, AI 어시스턴트의 메모리에도 반드시 지키도록 등록했다.
규칙 1 — DB 삭제는 절대 금지, 모든 DB 작업은 사전 승인.
프로덕션 DB(Supabase)에 대해:
1. 삭제(DELETE)는 절대 하지 않는다 — 테스트 데이터 정리 목적이라도 금지.
2. 모든 DB 관련 작업(INSERT/UPDATE/DELETE, 스토리지, admin API 등)은
bypass 퍼미션 모드여도 반드시 사전에 사용자에게 물어보고 승인받은 뒤 실행한다.
핵심은 "테스트 데이터 정리 목적이라도"라는 단서다. 사고의 시작이 바로 "이 정도는 테스트용이니 지워도 되겠지"라는 판단이었기 때문이다. AI 어시스턴트에게 위임 권한(bypass permission)을 준 상태였어도, 삭제·쓰기 계열 작업만큼은 예외 없이 사람 확인을 거치도록 선을 그었다.
규칙 2 — admin API의 email 필터를 신뢰하지 않는다.
GET /auth/v1/admin/users?email=X 는 email 쿼리 필터를 무시하고
페이지네이션된 전체 유저 목록을 반환한다.
특정 유저를 정확히 특정해야 할 때는 다음 순서를 따르기로 했다.
1. REST로 profiles?email=eq.X 조회 → 정확한 프로필 row 확보
2. 그 row의 id(UUID)를 확보
3. 이후 모든 조작은 이메일이 아니라 UUID로만 지정
트러블슈팅 체크리스트
비슷한 상황을 마주할 수 있는 분들을 위해, Supabase(또는 유사한 BaaS)에서 유저 관련 admin 작업을 할 때 점검하면 좋을 목록을 정리했다.
| 점검 항목 | 확인 방법 |
|---|---|
| admin API의 쿼리 필터가 실제로 동작하는가 | 존재하지 않는 값으로 조회해서 빈 배열이 오는지 확인 |
| 삭제 대상이 정확한 PK(UUID 등)로 지정되는가 | 이메일·이름 같은 부정확한 키로 대상 지정 금지 |
| cascade 삭제 범위를 파악하고 있는가 | 외래키에 ON DELETE CASCADE가 걸린 테이블 목록을 미리 확인 |
| 삭제 전 dry-run(조회만)이 가능한가 | 삭제 스크립트는 먼저 SELECT로 대상 목록을 사람이 확인하게 만들기 |
| 백업/PITR이 활성화되어 있는가 | Free 플랜은 대부분 백업이 없거나 제한적이라는 점 인지 |
정리
이번 사고에서 배운 건 기술적인 것 하나, 태도적인 것 하나다.
- BaaS의 admin API는 문서만 보고 "필터가 동작하겠지"라고 가정하면 안 된다. 실제로 존재하지 않는 값을 넣어 빈 결과가 오는지 검증하는 습관이 필요하다. 이번 경우처럼 필터가 조용히 무시되고 200 OK와 함께 엉뚱한 데이터가 오는 경우가 실제로 있다.
- AI 코딩 어시스턴트에게 위임 권한을 줄수록, 삭제·쓰기 계열 작업에는 더 엄격한 사전 승인 절차를 둬야 한다. "테스트 데이터니까 괜찮겠지"라는 판단이 사고의 시작점이었다. 읽기(SELECT)와 쓰기/삭제를 명확히 다른 급으로 취급하는 규칙을 세워두면, 같은 실수를 반복하지 않을 수 있다.
무엇보다, 사고가 난 순간 가장 중요했던 건 빠른 복구보다 투명한 보고였다. 무엇이 사라졌고 무엇은 복구 가능한지를 먼저 명확히 하는 것이, 이후 AI와 계속 함께 작업할 수 있는 신뢰의 최소 조건이었다.
다음 편에서는 이 사고를 딛고 진행한 기능 — 이력서 자동 분석과 RAG 기반 맞춤 이력서 생성 이야기를 다룬다.
backtodev
40대 PM, 다시 개발자로 돌아갑니다. 실패하고 배우며 성장하는 기록.