Skip to content

그룹 소유 구조 정합화 — 그룹장 역할 분리·승계·소멸 (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.sql 28건 (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 검증)
pgTAP83파일 1,239건83+1파일 1,267건 전부 통과 (로컬 샌드박스)
Production 적용리허설 raise-롤백 OK → db push 적용 (사후검사 3종)

미검증: 시각·감각 품질 해당 없음(서버 전용). 해산 기능은 UI가 없어 실사용 경로 미개통(오너 결정 대기) — RPC 자체는 pgTAP로 검증.

6. 이번 개선으로 향상된 것

공유 데이터가 개인 계정의 운명에서 분리됐다

그룹 멤버 누구의 계정 삭제도 이제 그룹 자체를 지우지 못한다. "소유 주체(그룹)와 기여자/역할(유저)의 분리" 원칙이 스키마에 박혔다.

구조적으로 남는 것

  • 그룹 생명주기 단일 관문(departure 트리거) — 탈퇴·추방 등 미래 기능이 행 삭제만으로 규칙을 상속.
  • 단일 owner DB 불변식 — 승계 로직의 전제가 코드 약속이 아니라 제약.
  • 이름 재입력 확인 게이트 패턴의 그룹판(disband_group_v1).

남은 것

  • 오너 결정: 해산 기능 UI 노출 방식(제안: 커피라운지 그룹장 전용 버튼 + 이름 입력 확인 팝업) → 결정 후 별도 트랙.
  • 오너 결정: 보드 작성자 연쇄 삭제(공유 보드가 마지막 수정자의 계정 삭제에 지워지는 문제)를 같은 원칙으로 정리할지.