그룹 소유 구조 정합화 — 그룹장 역할 분리·승계·소멸 (2026-09-01)
- 기간: 2026-09-01 (분석·오너 결정 1세션 + 구현 1세션)
- 랜딩: PR #1065 (
fe9b27f7) — 마이그레이션20260901900000_group_ownership_v1, Production 적용·사후검사 완료(2026-09-01) - 설계서: 이슈 #1061 본문 (오너 결정 G1~G3, 2026-09-01)
- 정본:
supabase/migrations/20260901900000_group_ownership_v1.sql(승계·소멸 규칙 전문 주석) - 도구: Management API 쿼리 러너 — 착수 전 실측·raise-롤백 리허설
- 게이트: pgTAP
group_ownership_v1.test.sql28건 (migration-smoke 레인) - 버그리포트: 없음 (사고 전 예방 트랙 — 데이터 소유 체계 점검에서 발견)
- 계약: 없음 (신규 클라이언트 표면 없음 — 해산 UI는 오너 결정 대기)
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 1 | 스키마 정합 (G1): owner_user_id 제거·단일 owner 인덱스·create_group_v1 재작성 | ✅ PR #1065 |
| Phase 2 | 승계·소멸 트리거 (G2·G3 전반) | ✅ PR #1065 |
| Phase 3 | 명시 해산 RPC (G3 후반) — UI 노출은 오너 결정 대기 | ✅ 서버 경로 PR #1065 |
1. 배경
2026-09-01 데이터 소유 체계 점검(바벨릭/유저/그룹 소유 전수 분류)에서 발견됐다. 오너 원칙: 그룹은 고유 id로 존재하는 독립 개체이고, 그룹장은 그룹에 귀속된 역할이다. 그런데 스키마는 그룹 행에 그룹장 개인의 계정 id를 박아두고(groups.owner_user_id), 그 컬럼에 계정 삭제 연쇄(cascade)를 걸어두고 있었다.
2. 문제 제기
그룹장 계정 삭제 = 그룹 전체 소멸
owner_user_id에 걸린 on delete cascade 때문에, 그룹장이 계정을 삭제하면 그룹·보드·댓글·좋아요·공지·채팅이 통째로 삭제됐다. 유저 예시: A(그룹장)·B·C가 함께 쓰는 그룹에서 A가 앱을 접고 계정을 지우면, B와 C의 화면에서 그룹이 통째로 사라진다. 멤버들의 공유 데이터가 그룹장 개인 계정의 운명에 묶여 있었다.
그룹장 진실이 두 벌
권한 판정은 전부 group_members.role('owner')로 하는데, 컬럼에도 그룹장이 한 벌 더 기록돼 어긋날 수 있는 구조였다. 실측: 컬럼은 생성 때 1회 기록이 유일한 접근, 읽는 로직 0곳 — 죽은 컬럼이 cascade 폭탄만 쥐고 있었다.
그룹 생명주기 규칙 부재
그룹이 언제 사라지는지에 대한 명시 규칙이 없었다 — cascade 사고가 유일한 "소멸 경로"였다.
3. 해결 방안
원칙 (오너 결정 2026-09-01)
- G1 = 그룹은 독립 소유 주체.
groups.owner_user_id제거, 그룹장 진실 =group_members.role='owner'단일. - G2 = 그룹장 이탈 시 자동 승계 — writer 우선, 동순위면 가입 오래된 순.
- G3 = 마지막 멤버 이탈 시 그룹 자동 소멸 + 그룹장의 명시적 해산 기능 별도 제공.
접근
- 승계·소멸은
group_members삭제 트리거 한 지점에 건다. 계정 삭제(연쇄 삭제 경로)든 앞으로 생길 탈퇴 기능이든, 멤버가 빠져나가는 모든 경로가 이 관문을 지난다 — 경로별 중복 구현이 원천적으로 없다. (#663 계정 삭제 트랜잭션에 개별 편입하는 대안은 미래 탈퇴 경로를 못 덮어 기각.) - 단일 그룹장 불변식은 코드 약속이 아니라 부분 유니크 인덱스로 DB가 강제한다.
- 명시 해산은 계정 삭제와 같은 방식의 확인 절차(이름 재입력)를 서버 게이트로 둔다.
4. 적용한 내용
Phase 1 — 스키마 정합 (G1)
groups.owner_user_id 컬럼·인덱스 제거. create_group_v1은 컬럼 기록 없이 그룹을 만들고 생성자의 owner 멤버십 행이 유일한 그룹장 기록이 된다. group_members_single_owner_idx(그룹당 owner 행 정확히 1개) 추가 — 두 번째 owner 행은 DB가 거부(23505).
Phase 2 — 승계·소멸 트리거 (G2·G3 전반)
group_members_departure_v1 AFTER DELETE 트리거: ① 그룹 자체가 지워지는 중이면 통과 ② 남은 멤버 0명이면 그룹 삭제 ③ 남은 멤버 중 그룹장이 없으면 승계(writer 우선, 가입 오래된 순, 최종 동순위는 id 순으로 결정적). 승계 조건을 "떠난 사람이 그룹장이면"이 아니라 "그룹장이 없으면"으로 걸어, 혹시 모를 드리프트도 다음 이탈 때 자가 치유된다.
Phase 3 — 명시 해산 RPC (G3 후반)
disband_group_v1(그룹 id, 확인용 이름): 그룹장 전용(비멤버·member 42501 — 존재 누설 없음), 그룹 이름 정확 일치 필수(불일치 22023). 성공 시 그룹 그래프 전체 삭제. UI 노출 방식은 이슈 본문대로 오너 별도 확인 대기 — 이 트랙은 서버 경로까지.
주요 결정과 그 근거
- 트리거 = 단일 관문: 탈퇴 RPC가 아직 없다는 실측(그룹 탈퇴 기능 자체가 미구현)에서, 경로마다 승계를 심는 대신 행 삭제 자체에 걸었다. 미래 탈퇴 기능은 멤버십 행만 지우면 규칙이 저절로 적용된다.
- 확인 절차 = 이름 재입력: 계정 삭제(#663)의 서버 센티널과 같은 원리 — 인증된 우발 호출이 그룹을 지울 수 없다.
작업 중 드러난 것
- 그룹 탈퇴 RPC가 애초에 없다 — G2의 "탈퇴 경로"는 현재 계정 삭제 연쇄뿐이고, 트리거 방식이라 미래 탈퇴 기능도 자동 커버된다.
- 인접 발견(범위 밖, 이슈에 보고): 그룹 보드 행의 작성자(마지막 수정자) 연쇄 삭제 — 그 멤버가 계정을 삭제하면 그룹은 살아남아도 그날의 공유 보드가 함께 지워진다. 같은 소유 원칙을 적용할지 오너 결정 필요.
5. 적용 결과
| 항목 | 전 | 후 |
|---|---|---|
| 그룹장 계정 삭제 시 | 그룹·보드·댓글·공지·채팅 전체 연쇄 소멸 | 그룹 생존 + 자동 승계 (pgTAP 실제 cascade 경로 검증) |
| 그룹장 진실 | 2벌 (컬럼 + role) | 1벌 (role) + DB 강제 (owner 정확히 1/그룹) |
| 그룹 소멸 규칙 | 암묵 (cascade 사고 대기) | 명시 2개: 마지막 멤버 이탈 자동 소멸 + 그룹장 명시 해산 |
| 승계 우선순위 | — | writer 우선 → 가입 오래된 순 (4단계 연쇄 pgTAP 검증) |
| pgTAP | 83파일 1,239건 | 83+1파일 1,267건 전부 통과 (로컬 샌드박스) |
| Production 적용 | — | 리허설 raise-롤백 OK → db push 적용 (사후검사 3종) |
미검증: 시각·감각 품질 해당 없음(서버 전용). 해산 기능은 UI가 없어 실사용 경로 미개통(오너 결정 대기) — RPC 자체는 pgTAP로 검증.
6. 이번 개선으로 향상된 것
공유 데이터가 개인 계정의 운명에서 분리됐다
그룹 멤버 누구의 계정 삭제도 이제 그룹 자체를 지우지 못한다. "소유 주체(그룹)와 기여자/역할(유저)의 분리" 원칙이 스키마에 박혔다.
구조적으로 남는 것
- 그룹 생명주기 단일 관문(departure 트리거) — 탈퇴·추방 등 미래 기능이 행 삭제만으로 규칙을 상속.
- 단일 owner DB 불변식 — 승계 로직의 전제가 코드 약속이 아니라 제약.
- 이름 재입력 확인 게이트 패턴의 그룹판(
disband_group_v1).
남은 것
- 오너 결정: 해산 기능 UI 노출 방식(제안: 커피라운지 그룹장 전용 버튼 + 이름 입력 확인 팝업) → 결정 후 별도 트랙.
- 오너 결정: 보드 작성자 연쇄 삭제(공유 보드가 마지막 수정자의 계정 삭제에 지워지는 문제)를 같은 원칙으로 정리할지.