멀티테넌트 백엔드에서 tenant_id를 모든 테이블에 붙였다고 해서 테넌트 운영이 끝나는 것은 아니다. 실제 운영에서 더 어려운 문제는 “이 고객사가 지금 어떤 상태인가”, “누가 어떤 이유로 정지했는가”, “데이터를 언제까지 복구할 수 있는가”, “완전 삭제가 정말 끝났는가"를 시스템이 일관되게 대답하는 일이다. 테넌트 생명주기(Tenant Lifecycle)는 가입 화면보다 훨씬 뒤쪽, 즉 계약 변경, 권한 회수, 데이터 반출, 감사 로그, 검색 인덱스 정리까지 이어진다.

이 글은 멀티테넌시 전략에서 다룬 격리 모델을 운영 워크플로로 확장한다. 테넌트 컨텍스트를 안전하게 전달하는 방법은 Tenant Context Propagation 가드레일과 연결되고, 삭제·보존 정책은 데이터 보존과 삭제 아키텍처의 실제 사용 사례가 된다.

이 글에서 얻는 것

  • 테넌트를 단순 고객 레코드가 아니라 상태 전이를 가진 운영 객체로 모델링하는 기준
  • 생성, 활성화, 정지, 종료 요청, 보존, 삭제, 복구를 분리하는 이유
  • 오프보딩에서 데이터베이스, 오브젝트 스토리지, 검색 인덱스, 캐시, 권한 시스템을 함께 정리하는 절차
  • 장애와 분쟁을 줄이는 숫자 기준: 유예 기간, 승인 단계, purge 배치 크기, 검증 지표
  • 새 테넌트 온보딩과 기존 테넌트 종료를 같은 통제면에서 다루는 체크리스트

핵심 개념/이슈

테넌트는 상태 기계다

좋은 출발점은 active, suspended, delete_requested, retained, purging, purged 같은 명시 상태를 두는 것이다. 상태가 없으면 기능마다 각자 다른 해석을 만든다. 결제 시스템은 연체 고객을 막았다고 생각하지만 API 서버는 쓰기를 허용하고, 관리자 콘솔은 삭제 요청을 받았지만 검색 인덱스는 계속 노출하는 식의 불일치가 생긴다.

실무에서는 다음 전이를 기본값으로 둔다.

  • provisioning -> active: 기본 리소스 생성, 관리자 계정 초대, 초기 권한 부여가 끝났을 때만 활성화한다.
  • active -> suspended: 결제 실패, 보안 사고, 계약 종료 예고처럼 서비스를 멈춰야 하지만 데이터는 보존해야 할 때 사용한다.
  • suspended -> active: 정지 사유가 해결되고 변경 이력이 감사 로그에 남았을 때만 허용한다.
  • active|suspended -> delete_requested: 고객 또는 내부 승인자가 종료를 요청한 상태다.
  • delete_requested -> retained: 복구 가능 기간에 들어간다. 일반 SaaS는 730일, 엔터프라이즈 계약은 3090일을 많이 쓴다.
  • retained -> purging -> purged: 삭제 배치가 실행되고 잔여 데이터 검증이 끝난 뒤 최종 상태로 닫는다.

중요한 점은 상태 전이가 API 서버 한 곳에 숨어 있으면 안 된다는 것이다. 권한 시스템, 백그라운드 잡, 검색 인덱싱, 알림, 과금이 모두 같은 상태를 읽어야 한다. 테넌트 상태 저장소는 강한 일관성이 필요한 운영 기준값으로 보고, 캐시하더라도 TTL을 30~60초 이하로 낮추거나 상태 변경 이벤트를 통해 즉시 무효화한다.

오프보딩은 삭제가 아니라 회수와 증명이다

고객이 “계정을 삭제해 주세요"라고 말했을 때 백엔드는 실제로 여러 질문에 답해야 한다. 누가 요청했는가, 계약상 보존해야 하는 로그가 있는가, 미납 청구가 있는가, 고객이 데이터 export를 받았는가, 외부 연동 토큰은 회수됐는가, 삭제 후에도 남아야 하는 회계·보안 감사 데이터는 무엇인가. 따라서 오프보딩은 DELETE FROM tenants WHERE id = ?로 끝나지 않는다.

오프보딩 요청에는 최소한 다음 manifest를 만든다.

  • tenant_id, 고객명, 요청자, 승인자, 승인 시각
  • 종료 사유와 법무·보안 hold 여부
  • 삭제 대상 리소스 수: 데이터베이스 row 추정치, 오브젝트 수, 인덱스 문서 수, 큐 메시지 수
  • export 필요 여부와 export 파일 만료 시각
  • 복구 가능 종료 시각과 최종 purge 예정 시각
  • 참조해야 하는 감사 로그 범위

이 manifest가 있으면 나중에 “정말 삭제했는가"라는 질문을 상태와 숫자로 답할 수 있다. 감사 로그는 변조 방지 감사 로그 설계처럼 삭제 대상 데이터와 분리해 보존해야 한다.

리소스는 데이터베이스 밖에도 있다

테넌트 데이터는 RDB 테이블에만 있지 않다. 검색 인덱스, 오브젝트 스토리지, 분석 웨어하우스, 캐시, 메시지 큐, 서드파티 SaaS, webhook endpoint, feature flag, API key, 세션 토큰에도 흔적이 남는다. 오프보딩 설계에서 가장 흔한 실수는 주 데이터베이스 row만 지우고 나머지를 운영 부채로 남기는 것이다.

특히 검색 인덱스는 삭제 후 노출 사고로 이어지기 쉽다. 검색 색인 갱신은 Search Index Sync와 Reindexing에서처럼 원본 데이터와 별도의 재처리 지연을 가진다. 테넌트 purge가 끝났다고 판단하려면 인덱스 문서 수가 0인지 확인해야 한다. 오브젝트 스토리지는 prefix 단위 삭제가 실패하거나 버전 관리가 켜져 있을 수 있으므로 deleted_marker와 실제 버전 정리를 구분한다.

실무 적용

1. 상태 전이를 단일 API로 좁힌다

테넌트 상태 변경은 관리자 콘솔, 결제 webhook, 보안 대응 스크립트, 고객 성공팀 도구에서 모두 발생할 수 있다. 그래도 실제 변경 경로는 하나여야 한다. 예를 들어 TenantLifecycleService.transition(tenant_id, target_state, actor, reason, policy) 같은 내부 API를 만들고, 모든 도구가 이 API를 통과하게 한다.

권장 기준은 다음과 같다.

  • active -> suspended: 단일 승인으로 가능하되, 사유와 만료 시각을 필수로 둔다. 보안 사고 정지는 즉시 쓰기 차단을 우선한다.
  • suspended -> active: 정지 사유 해결 증거가 있어야 하며, 고위험 테넌트는 2인 승인을 요구한다.
  • delete_requested: 고객 관리자 또는 내부 승인자 1명 이상이 필요하다. 엔터프라이즈·개인정보 민감 테넌트는 법무/보안 hold 플래그를 확인한다.
  • purging: 자동 실행 전 1회 dry-run count를 만들고, 예상 삭제량이 최근 7일 평균 대비 2배 이상이면 수동 승인을 요구한다.
  • purged: 잔여 데이터 검증 리포트가 통과해야만 전이한다.

상태 변경 이벤트는 outbox로 발행한다. 구독자는 권한 회수, 토큰 폐기, 작업 큐 정지, 검색 문서 삭제, 청구 종료, 알림 발송을 처리한다. 이벤트 발행 실패가 상태 변경 성공으로 기록되면 위험하므로, 상태 변경과 outbox 기록은 같은 트랜잭션에 넣는다.

2. 온보딩도 idempotent하게 만든다

오프보딩만 위험한 것이 아니다. 테넌트 생성 중간에 장애가 나면 “DB는 생겼지만 기본 관리자 계정은 없는 상태”, “스토리지 bucket은 있는데 billing plan이 없는 상태"가 된다. 생성 단계도 멱등 작업으로 나눠야 한다.

온보딩의 현실적인 SLO는 보통 5~15분 안에 잡는다. 그 안에 끝나야 할 작업은 다음처럼 나눈다.

  • 테넌트 레코드 생성: 1분 이내
  • 기본 권한·역할 생성: 1분 이내
  • 기본 설정·feature flag 초기화: 2분 이내
  • 외부 연동·샘플 데이터 준비: 5~10분 이내
  • 실패 시 재시도 가능 상태 기록: 즉시

각 단계는 tenant_id + step_name을 멱등 키로 삼는다. 같은 단계가 두 번 실행돼도 role이 중복 생성되거나 초기 크레딧이 두 번 지급되면 안 된다. 운영자는 provisioning이 15분을 넘은 테넌트를 알림으로 받아야 한다.

3. 정지 상태의 읽기·쓰기 정책을 분리한다

정지는 단순히 로그인 금지가 아니다. 서비스마다 읽기는 허용하고 쓰기만 막아야 할 때가 있다. 예를 들어 결제 연체는 관리자 콘솔 읽기와 export 요청은 허용할 수 있지만, 신규 주문 생성이나 데이터 변경은 막아야 한다. 반대로 보안 사고 정지는 관리자 접근도 차단해야 한다.

그래서 suspended에는 reason code가 필요하다.

  • billing_overdue: 쓰기 차단, 읽기 제한 허용, export 가능
  • security_incident: 모든 사용자 세션 폐기, 관리자 포함 접근 차단, 내부 조사 권한만 허용
  • contract_end_pending: 신규 쓰기 차단, export와 이관 작업 허용
  • abuse: 공개 API 즉시 차단, 감사 로그 보존 강화

이 분류는 권한 검사 레이어에 들어가야 한다. 컨트롤러마다 if tenant.suspended를 흩뿌리면 누락이 생긴다. Permission Drift와 Access Review에서 다룬 것처럼 정지 상태에서도 남는 예외 권한은 정기 검토 대상이다.

4. 삭제는 단계별로 작게 실행한다

Purge 작업은 한 번에 크게 돌리지 않는다. 테넌트 하나가 수억 row를 가질 수 있고, 잘못된 조건문 하나가 다른 테넌트 데이터까지 건드릴 수 있기 때문이다. 실무 기준은 다음처럼 잡는다.

  • 삭제 배치는 tenant_id를 명시한 쿼리만 허용한다. 테넌트 필터가 없는 쿼리는 CI나 runtime guard에서 실패시킨다.
  • RDB 삭제는 테이블별 1,000~10,000 row 단위로 chunking한다. lock wait가 2초를 넘으면 batch size를 절반으로 줄인다.
  • 한 번의 purge run은 최대 1~3개 테넌트만 처리한다. 대량 종료는 큐로 분산한다.
  • 오브젝트 삭제는 prefix listing과 manifest count를 비교한다. 삭제 후 잔여 object가 0이 아니면 purged로 전이하지 않는다.
  • 검색 인덱스는 삭제 요청 후 재조회 검증을 별도 단계로 둔다. 잔여 문서율 목표는 0이다.
  • 캐시는 상태 변경 직후 namespace 또는 tag 기반 purge를 실행한다. TTL 자연 만료에만 기대지 않는다.

삭제가 오래 걸리는 것은 실패가 아니다. 더 위험한 것은 일부만 삭제된 상태를 성공으로 표시하는 것이다. purging 상태는 사용자에게 노출하지 않더라도 운영 콘솔에서는 명확히 보여야 한다.

5. 복구 가능 기간을 계약으로 고정한다

복구 가능 기간은 “혹시 모르니 한 달쯤"처럼 정하면 안 된다. 제품·법무·보안이 합의한 정책이어야 하고 고객 약관, 관리자 UI, 내부 runbook이 같은 숫자를 말해야 한다. 소규모 B2B SaaS라면 기본 14일, 엔터프라이즈라면 30일, 규제 산업은 법무 hold에 따라 90일 이상을 둘 수 있다.

복구가 가능한 기간에는 삭제 대상 데이터가 논리 삭제 또는 접근 차단 상태로 남는다. 이때도 비용이 든다. 따라서 retained tenant는 월간 스토리지 비용, 백업 보존 비용, 검색 인덱스 잔여 비용을 별도로 집계한다. retained 상태가 전체 활성 테넌트의 5%를 넘거나 retained storage가 전체 저장량의 10%를 넘으면 정책을 재검토한다.

트레이드오프/주의점

첫 번째 트레이드오프는 복구 가능성과 개인정보 최소화 사이에 있다. 복구 기간이 길수록 고객 실수에는 친절하지만, 보존해야 하는 민감 데이터와 침해 시 영향도 커진다. 고객이 직접 삭제를 요청한 데이터는 “복구 가능"이라는 이유만으로 무기한 보관하면 안 된다. 보존 기간은 데이터 유형별로 다르게 잡고, 감사 로그처럼 남겨야 하는 데이터와 서비스 데이터처럼 삭제해야 하는 데이터를 구분한다.

두 번째는 자동화와 승인 절차의 균형이다. 모든 종료를 수동으로 처리하면 운영팀 병목이 생기고, 모든 삭제를 자동으로 처리하면 잘못된 상태 전이가 큰 사고가 된다. 기준을 숫자로 나누는 편이 좋다. 예를 들어 예상 삭제 row가 100만 이하이고 법무 hold가 없으며 최근 24시간 내 관리자 로그인 이력이 없으면 자동 purge 후보로 본다. 반대로 VIP 테넌트, 민감 업종, 월 매출 상위 5%, 삭제량 상위 1%는 수동 승인으로 보낸다.

세 번째는 공유 인프라에서의 격리다. 데이터베이스 schema-per-tenant 구조라면 drop schema가 비교적 단순할 수 있지만 운영 비용이 높다. row-level 멀티테넌시라면 비용은 낮지만 삭제 쿼리와 인덱스 검증이 어렵다. 어떤 모델이든 오프보딩 기준은 격리 전략과 함께 설계해야 한다.

마지막으로, 외부 SaaS 연동을 잊기 쉽다. CRM, 이메일 발송 도구, 결제 시스템, 로그 분석 도구, 고객 지원 도구에 테넌트 데이터가 복제되어 있을 수 있다. 내부 purge가 끝났더라도 외부 도구 삭제 API가 실패했다면 완료가 아니다. 외부 삭제 실패는 purge_partial 같은 별도 상태로 남기고 재시도·수동 처리 큐를 제공한다.

체크리스트 또는 연습

운영 체크리스트

  • 테넌트 상태 목록과 허용 전이가 문서와 코드에 모두 존재하는가?
  • 모든 상태 변경이 단일 서비스/API를 통과하는가?
  • suspended reason code별 읽기·쓰기·관리자 접근 정책이 정리되어 있는가?
  • 삭제 요청 manifest에 승인자, 사유, hold 여부, 리소스 count, 복구 종료 시각이 포함되는가?
  • purge dry-run count와 실제 삭제 count 차이가 1%를 넘으면 경고하는가?
  • 데이터베이스, 검색 인덱스, 오브젝트 스토리지, 캐시, 외부 SaaS 잔여 검증을 모두 수행하는가?
  • retained 테넌트의 비용과 보존 기간 초과 건수를 대시보드로 보는가?
  • 최종 삭제 후 고객 또는 내부 운영자에게 증명 가능한 완료 리포트가 남는가?

연습

  1. 기존 서비스의 테넌트 상태를 active, suspended, delete_requested, retained, purging, purged로 다시 그려 본다. 현재 코드에서 이 상태를 우회하는 경로가 몇 개인지 세어 본다.
  2. 테넌트 하나를 삭제한다고 가정하고 리소스 manifest를 작성한다. RDB 테이블, 오브젝트 prefix, 검색 인덱스, 큐, 캐시, 외부 SaaS를 빠짐없이 적는다.
  3. purging dry-run 결과가 예상보다 3배 큰 상황을 설계한다. 어떤 승인 단계에서 멈추고, 어떤 로그를 보고, 어떤 롤백 또는 보류 결정을 할지 적는다.
  4. 복구 가능 기간을 7일, 30일, 90일로 바꿨을 때 고객 경험, 비용, 개인정보 리스크가 어떻게 달라지는지 비교한다.

테넌트 생명주기는 화려한 기능은 아니지만, 성숙한 B2B 백엔드의 신뢰도를 결정한다. 생성이 빠르고 삭제가 증명 가능하며 정지가 일관적인 시스템은 운영팀과 고객 모두에게 예측 가능하다. 반대로 상태가 흩어진 시스템은 고객이 떠나는 마지막 순간에 가장 큰 장애를 만든다.