하루스페이스 랩
버튼보다 먼저 경계를 만들었다 — 플랫폼 권한·그룹 멤버십·사람 승인
하루스페이스가 기능을 늘리기 전에 플랫폼 관리와 그룹 참여를 분리하고, 거버넌스 이력과 AI 초안·사람 적용 경계를 세우면서 겪은 두 번의 실패를 기록했습니다.

Summary
한눈에 보기
- 최고관리자의 플랫폼 관리 보기와 실제 그룹 멤버십을 분리해, 관리 권한이 채팅 참여나 그룹 활동 주체 권한으로 번지지 않게 했습니다.
- 그룹 안건과 투표는 덮어쓰지 않는 이력으로 남기고, AI는 제한된 공간 초안만 제안하며 미리보기 뒤 사람이 적용하도록 만들었습니다.
- 최고관리자 안건 조건과 스키마 버전 누락에서 실패하며, 권한의 뜻과 데이터 변경 절차를 코드·화면·운영에서 함께 맞춰야 한다는 점을 배웠습니다.
이 글은 2026년 7월 15–21일의 Git 기록과 당시 설계·운영 문서를 바탕으로, 하루스페이스가 기능보다 권한 경계를 먼저 고쳐 세운 일주일을 복기해 쓴 회고다.
서비스를 만들다 보면 권한은 늘 버튼의 모습으로 먼저 보인다.
누가 만들 수 있는가. 누가 수정할 수 있는가. 승인과 거부 버튼은 누구에게 보여야 하는가. 화면만 놓고 보면 역할 이름에 따라 버튼을 숨기거나 보여 주면 끝날 것 같다. 나도 처음에는 그렇게 생각했다.
하지만 그룹이 늘고, 한 사람이 여러 그룹을 오가고, 펫이 AI 역할을 맡기 시작하자 질문이 달라졌다.
그 버튼을 볼 수 있는 사람과, 그 데이터의 주체가 될 수 있는 사람은 정말 같은가.
7월 셋째 주의 하루스페이스는 기능을 많이 더한 시기였다. 그룹 거버넌스, 복수 그룹 참여, 첫 공간 온보딩, 세 언어 UI와 공통 피드백 방식이 이어졌다. 동시에 기능보다 더 오래 남는 경계를 다시 세운 시기이기도 했다. 플랫폼을 관리하는 권한과 그룹에 참여하는 자격, 사람과 AI 펫의 정체성, 초안을 만드는 일과 실제로 적용하는 일을 분리했다.
그 과정은 매끄럽지 않았다. 최고관리자가 필요한 안건을 만들 수 없었고, 새 컬럼까지 작성해 놓고 스키마 버전을 올리지 않아 운영의 그룹 승인만 실패하기도 했다. 두 문제 모두 작은 조건문이나 숫자 하나로 고칠 수 있었지만, 원인은 더 깊었다. 권한과 변경의 의미를 시스템 전체에서 같은 문장으로 설명하지 못하고 있었다.
최고관리자라면 모든 그룹의 구성원일까
초기에는 최고관리자 역할이라는 이름이 너무 많은 뜻을 품고 있었다. 플랫폼 전체를 점검하는 계정이니 모든 그룹을 열어 볼 수 있었고, 현재 선택한 그룹도 가질 수 있었다. 그러다 보니 코드에서는 ‘현재 그룹이 있다’는 사실을 ‘그 그룹의 구성원이다’라는 뜻으로 받아들이기 쉬웠다.
둘은 달랐다.
최고관리자가 플랫폼 운영을 위해 그룹의 설정과 상태를 확인하는 것은 관리 보기다. 채팅방에 들어가 메시지와 첨부를 읽고, 접속 인원과 읽음 상태에 포함되며, 알림을 받는 것은 그룹 참여다. 전자는 플랫폼 책임에 필요한 범위이고, 후자는 그룹 구성원으로서의 관계다.
우리는 최고관리자가 모든 그룹을 탐색할 수 있다는 이유만으로 자동 멤버십을 갖지 않도록 경계를 다시 그었다. 참여 신청이 승인되거나 명시적으로 배정된 활성 멤버십이 있는 그룹에서만 채팅 활동의 주체가 되게 했다. 멤버십이 없으면 메시지·첨부·읽음·실시간 연결·방 초대와 공개방 자동 참여를 서버에서 막았다. 그룹 선택 화면에서도 단순히 모두 ‘참여 중’으로 보이지 않게 관리 보기와 참여를 구분했다.
이 구분이 중요한 이유는 화면에 보이는 버튼보다 데이터에 남는 흔적 때문이다. 관리 점검을 한 계정이 채팅 인원으로 집계되거나, 읽지 않은 메시지의 수신자로 들어가거나, 그룹 알림을 받는 순간 플랫폼 관리 권한은 조용히 그룹 활동 권한으로 번진다. 나중에 화면에서 버튼 하나를 숨겨도 이미 섞인 관계를 되돌리기 어렵다.
그래서 판단 기준을 이렇게 바꿨다.
- 플랫폼 역할은 플랫폼 정책과 보안, 그룹 생성·중단을 관리한다.
- 그룹 멤버십은 특정 그룹 안에서의 참여와 역할을 나타낸다.
- 현재 화면의 그룹은 탐색 문맥일 수 있으며, 그 자체로 멤버십의 증거가 아니다.
- 채팅과 알림, 인원 집계는 활성 승인 멤버십을 서버에서 다시 확인한다.
관리자가 더 편하게 볼 수 있게 하는 것보다, 그룹이 누가 실제 구성원인지 스스로 설명할 수 있게 하는 편이 중요했다.
계정 승인과 그룹 참여도 같은 일이 아니었다
그 전까지 가입 승인은 계정과 그룹 참여가 한 덩어리처럼 움직였다. 사용자가 가입하면 어느 그룹에 속할지까지 한 번에 결정되는 흐름은 그룹이 하나일 때는 단순했다. 복수 그룹과 신규 그룹 신청이 생기자 이 구조는 곧 한계에 닿았다.
우리는 첫 사용자를 제외한 새 계정을 활성 상태로 만들되, 자동으로 그룹 멤버십을 만들지 않는 방향을 택했다. 로그인 세션은 존재하지만 승인된 멤버십이 없다면 그룹 온보딩만 사용할 수 있다. 기존 그룹 참여와 신규 그룹 생성은 별도의 요청이고, 각각 누가 검토했는지와 승인·거부 시각을 남긴다.
여기서도 화면만 숨기는 것으로는 충분하지 않았다. 멤버십이 없는 일반 계정이 홈 주소나 API 주소를 직접 호출하면 서버가 온보딩 필요 상태로 거부해야 했다. 그룹관리자는 자기 그룹의 참여 요청만 보고, 신규 그룹 생성 요청은 플랫폼 책임자만 다루도록 조회 결과 자체를 나눴다.
탈퇴 역시 행을 지우는 대신 상태로 남겼다. 본인이 나간 경우와 관리자가 내보낸 경우를 구분하고, 재가입 때 이전 역할을 복원할지 일반 구성원으로 시작할지를 다르게 결정했다. 사람의 관계는 ‘있음 또는 삭제됨’ 두 상태로 설명하기 어려웠기 때문이다.
이 작업을 하며 계정은 신원을, 멤버십은 관계를, 역할은 그 관계 안의 권한을 뜻해야 한다는 기준이 생겼다. 세 가지를 하나의 역할 값으로 해결하려 했을 때 생기는 편리함보다, 나중에 왜 접근이 허용됐는지 설명할 수 있는 구조를 택했다.
사람 멤버와 AI 펫을 같은 칸에 넣지 않았다
AI 펫이 채팅방에 들어오기 시작하면서 또 다른 유혹이 생겼다. 펫도 메시지를 남기니 사람 사용자와 같은 멤버 테이블에 넣으면 구현이 쉬워 보였다. 그러나 펫은 가입하고 동의한 사람이 아니며, 독립적인 그룹 구성원도 아니다.
펫은 그룹이 획득하거나 할당받아 설정하는 AI 도구에 가깝다. 그룹마다 표시 이름, 역할, 사용할 모델과 노출 여부가 다를 수 있고, 어느 채팅방에 배치할지도 관리자가 정한다. 사용자는 현재 그룹이 허용하고 현재 방에 배치한 펫에게만 질문할 수 있어야 했다.
그래서 사람의 그룹 멤버십과 펫의 그룹별 설정·방 배치를 분리했다. AI 답변을 저장할 때도 사람 계정인 것처럼 위장하지 않고 전용 봇 정체성을 사용하도록 했다. 서버는 질문한 사람의 방 접근권한, 펫의 그룹 노출과 방 배치를 다시 확인했다.
이 경계는 단지 테이블을 정리하는 일이 아니었다. 펫이 답했다고 해서 그 펫이 그룹 문서 전체를 읽을 권한을 얻는 것은 아니다. 최고관리자가 펫을 설정할 수 있다고 해서 그 펫의 대화가 모든 그룹을 가로지를 수도 없다. 사람과 펫을 같은 ‘멤버’라는 단어로 부를 수는 있어도, 같은 권리와 책임을 가진 주체로 저장해서는 안 됐다.
투표 결과만이 아니라 결정 과정도 남겼다
그룹관리자 권한을 누가 가질지는 플랫폼 운영자가 임의로 덮어쓰는 값으로만 두고 싶지 않았다. 현재 그룹의 안건을 만들고, 승인된 구성원이 승인 또는 거부를 한 번 선택하며, 과반에 따라 결과를 확정하는 작은 거버넌스 흐름을 만들었다.
중요한 것은 최종 approved나 rejected 값만 남기는 일이 아니었다.
안건 생성 시점의 투표 대상자를 고정하고, 한 사람의 한 표를 데이터베이스에서도 강제했다. 생성, 승인표, 거부표와 최종 결정은 순서대로 추가되는 이력으로 저장했다. 제출한 표를 수정하거나 삭제하는 API는 만들지 않았다. 권한 부여·상실 안건이 통과하면 최종 상태와 역할 변경, 적용 시각을 한 트랜잭션에서 기록했다. 결정 직전에 대상 멤버십이 비활성 상태라면 안전한 쪽으로 거부 종료했다.
이런 방식은 불편하다. 실수한 표를 운영자가 간단히 고쳐 줄 수 없고, 데이터 행도 더 많이 생긴다. 대신 “현재 누가 관리자다”뿐 아니라 “누가, 어떤 구성원 범위에서, 어떤 결정을 거쳐 관리자가 됐는가”를 설명할 수 있다.
그룹 주권은 거창한 선언보다 이런 복구 가능한 설명에 가까웠다. 플랫폼이 결정 결과를 보관하더라도, 그룹의 결정을 보이지 않는 곳에서 다시 쓰지 않는다는 약속이었다.
첫 번째 실패: 최고관리자의 안건 제안이 막혔다
경계를 세분화하자 곧 반대 방향의 버그가 나왔다.
일반 회원이 그룹관리자 권한 안건을 제안하려면 현재 그룹의 활성 승인 멤버십이 필요하다. 이 검사는 맞았다. 문제는 같은 멤버십 검사를 플랫폼 범위에서 권한 부여·상실 안건을 등록해야 하는 최고관리자에게도 중복 적용했다는 점이었다. 최고관리자는 일부 그룹을 관리 보기로 열 수 있지만 실제 멤버십은 없을 수 있으므로, 필요한 안건 등록이 거부됐다.
해결은 최고관리자를 모든 그룹 구성원으로 되돌리는 것이 아니었다. 안건 등록의 플랫폼 관리 권한에만 명시적인 예외를 두고, 투표와 채팅 참여에는 계속 실제 멤버십을 요구했다.
이 버그는 권한 검사를 많이 넣는다고 자동으로 안전해지는 것은 아니라는 사실을 보여 줬다. 서로 다른 행위에 같은 멤버십 조건을 복사하면 과도하게 막히거나, 반대로 한 번의 예외가 너무 넓게 퍼질 수 있다.
그 뒤로는 ‘관리자니까 허용’이나 ‘멤버가 아니니까 거부’라는 문장만으로 조건을 만들지 않으려 했다. 어떤 자원에 대해, 어떤 행위를, 어느 범위에서 수행하는가를 먼저 적고 검사했다. 이번 예외는 안건 제안에만 적용되며 그룹 참여 자격을 만들지 않는다는 한계도 문서에 함께 남겼다.
AI는 공간을 직접 적용하지 않고 제한된 초안을 제안했다
같은 주에 신규 그룹의 첫 공간을 AI 펫 코코와 함께 꾸미는 온보딩도 만들었다. 긴 제품 온보딩 전체를 한 번에 구현하지 않고, 그룹 승인 뒤 빈 공간에서 이탈하지 않도록 하는 작은 수직 흐름부터 시작했다.
사용자는 공간의 목적, 먼저 볼 메뉴와 분위기를 고른다. AI 호출은 사용자가 AI와 함께 꾸미기를 명시적으로 눌렀을 때만 일어난다. 결과는 임의의 React 코드나 CSS가 아니라 기존 렌더러가 이해하는 제한된 매니페스트다. 허용된 블록과 내부 경로만 남기고, 외부 URL·HTML·JavaScript·CSS와 실행 코드는 제외했다.
여기서 가장 중요한 단계는 생성이 아니라 그 다음이었다.
AI 결과는 바로 그룹 공간에 적용하지 않았다. 같은 렌더러로 미리보기를 보여 주고, 그룹관리자가 이 공간으로 시작하기를 눌렀을 때만 적용했다. 저장할 때는 검증된 초안과 현재 적용본, 초안 리비전, 적용 버전과 이력을 한 트랜잭션에서 함께 갱신했다. 기대한 리비전이 달라졌다면 오래된 화면이 최신 설정을 덮지 못하게 했다.
모델 입력도 제한했다. 그룹 채팅 원문과 문서를 자동으로 보내지 않고 사용자가 고른 목적·메뉴·분위기와 짧은 메모만 전달했다. 모델 연결이 없거나 실패하면 실행을 멈추는 대신 같은 허용 구조의 결정적 로컬 가이드를 사용했다. 원문 전체를 감사용이라는 이유로 다시 저장하지 않고, 입력 해시·문자 수·모델과 프롬프트 버전·토큰 수·결과와 처리 시간을 남겼다.
이 시점에 AI가 한 일은 프로젝트를 만들거나 코드를 배포하는 일이 아니었다. 제한된 그룹 공간 초안을 제안하는 일이 전부였다. 그러나 작은 범위였기 때문에 이후에도 재사용할 원칙을 실제 흐름으로 확인할 수 있었다.
AI는 제안할 수 있다. 사람은 실제 결과를 보고 적용한다. 초안과 적용본은 같은 것이 아니다.
두 번째 실패: 컬럼은 있었지만 운영에는 없었다
온보딩에는 그룹 승인 뒤 코코가 첫 안내를 한 번만 보여 주기 위한 멤버십 상태가 필요했다. 코드에는 새 컬럼을 추가했고, 기존 행에는 기본값이 적용되며, 새 승인과 재가입 때만 안내가 켜지도록 경로도 작성했다.
그런데 운영에서 신규 그룹 승인이 실패했다. 거부는 정상적으로 처리됐다. 차이는 승인 트랜잭션만 새 멤버십 컬럼을 사용한다는 점이었다.
원인은 스키마 변경 내용이 아니라 스키마 버전이었다. 새 컬럼을 만드는 멱등 마이그레이션을 작성해 놓고 코어 버전을 기존 값에 그대로 뒀다. 운영 데이터베이스는 이미 그 버전을 적용한 상태였으므로 초기화 절차가 변경을 건너뛰었다. 애플리케이션은 존재한다고 믿는 컬럼을 읽거나 쓰려 했고, 승인 경로에서만 실패했다.
버전을 올려 이미 있는 컬럼을 해치지 않는 마이그레이션이 다시 실행되도록 했다. 당시 기록에는 인증 경계 확인과 신규 그룹 승인 재시도가 이어졌다. 수정 자체는 짧았지만 교훈은 오래 남았다.
- 애플리케이션 코드와 데이터베이스 변경은 같은 커밋에 있어도 같은 시점에 적용된다고 가정할 수 없다.
- 새 필드를 사용하지 않는 거부 경로의 성공은 승인 경로의 정상 동작을 증명하지 않는다.
- 멱등 DDL을 작성하는 것만으로 충분하지 않고, 운영이 그 변경을 실행하도록 버전 게이트도 함께 바뀌어야 한다.
- 배포 검증은 화면 하나가 열리는지보다 변경된 데이터 경로를 실제로 통과해야 한다.
권한 오류와 스키마 오류는 겉으로 모두 “승인할 수 없음”처럼 보일 수 있다. 그래서 사용자에게는 수정할 수 있는 메시지를 주고, 운영자에게는 어느 경계에서 거부됐는지 관찰할 수 있는 기록을 남기는 일이 함께 필요했다.
권한은 이해할 수 있어야 지킬 수 있었다
이 시기에 한국어·영어·일본어 UI와 공통 토스트도 정리했다. 처음에는 번역과 알림이 권한 설계보다 가벼운 마감 작업처럼 보였다. 실제로는 권한을 사용자가 이해하게 만드는 마지막 경계였다.
승인, 거부, 저장과 삭제 결과가 화면마다 다른 위치와 문장으로 나타나면 사용자는 자신의 선택이 반영됐는지 확신하기 어렵다. 특히 팝업 뒤에 알림이 가려지거나, 한 언어에서는 대상과 결과가 빠지면 잘못된 동작을 반복할 수 있다. 그래서 잠깐 알리면 되는 변경 결과는 공통 showToast() 흐름으로 모으고, 사용자가 바로 고쳐야 하는 필드 오류와 재시도가 필요한 장애는 해당 화면에 남겼다.
정적 UI 문구는 세 언어 사전에 함께 등록하되, 사용자 이름·그룹명·문서와 채팅 원문을 UI 사전이 임의로 번역하지 않게 했다. 언어 설정 API도 언어 값만 바꾸고 표시 이름이나 권한에는 손대지 않도록 범위를 좁혔다.
권한 정책은 서버에만 정확히 적혀 있어서는 부족했다. 사용자가 어느 그룹을 관리 보기로 연 것인지, 실제 참여 중인지, 자신의 승인이 저장됐는지 이해할 수 있어야 같은 실수를 줄일 수 있었다. 다국어와 공통 피드백은 장식이 아니라 설명 가능한 권한의 일부였다.
Codex 개발 동료는 아직 설계도에만 있었다
7월 19일에는 AI 펫이 실제 개발 작업을 제안하는 흐름을 검토했다. 당시 완성된 실행 시스템이 있었다고 쓰면 사실이 아니다.
그 주에 정한 것은 구현보다 경계였다. 펫은 작업을 제안할 수 있지만 실행 권한을 직접 갖지 않는다. 개발 작업은 하루스페이스의 승인 흐름과 격리된 실행 환경을 거치고, 사람이 실행 전과 결과 반영 전에 각각 확인한다. 코드가 반영됐다는 이유만으로 운영 배포 권한까지 이어지지도 않는다.
구체적인 실행기와 배포 연결은 이후 과제로 남았다. 이 단계에서 얻은 기준은 단순했다. AI의 제안, 코드 변경, 반영과 운영 배포는 서로 다른 권한이며, 결과의 영향이 커질 때마다 사람의 승인 지점이 다시 나타나야 한다.
이 일주일 뒤에 남은 기준
기능을 더하기 전에 경계를 나눈다고 개발이 느려지기만 하는 것은 아니었다. 무엇이 잘못됐는지 찾는 범위가 줄었고, 다음 기능이 재사용할 언어가 생겼다. 그 주의 결정을 실전 기준으로 정리하면 다음과 같다.
1. 역할 이름보다 자원과 행동을 먼저 적는다
admin이라는 이름 하나로 허용하지 않는다. 플랫폼 설정 보기, 그룹 안건 제안, 투표, 채팅 참여와 배포는 서로 다른 자원과 행동이다.
2. 계정, 멤버십과 역할을 분리한다
로그인할 수 있다는 사실은 특정 그룹에 속했다는 뜻이 아니다. 현재 그룹을 화면에 열었다는 사실도 활동 권한을 만들지 않는다.
3. 화면 숨김 뒤에 서버 검사를 둔다
버튼 노출은 사용성을 위한 안내다. 실제 경계는 세션, 활성 멤버십, 그룹과 자원의 소속을 서버가 다시 확인할 때 생긴다.
4. AI 결과는 초안·미리보기·적용으로 나눈다
모델 출력은 허용 목록으로 정규화하고, 사람이 실제 렌더링 결과를 확인한 뒤 명시적으로 적용한다. 공개나 실행처럼 결과가 큰 행동은 별도 승인으로 다시 나눈다.
5. 결정을 덮어쓰지 말고 이력을 추가한다
누가 승인했는지뿐 아니라 당시 누가 대상이었고 어떤 순서로 결정됐는지 남긴다. 되돌림도 과거를 삭제하는 대신 새 사건으로 설명할 수 있어야 한다.
6. 스키마 변경은 버전과 경로까지 검증한다
컬럼과 마이그레이션 코드만 확인하지 않는다. 운영 버전이 실제로 올라가는지, 새 필드를 사용하는 승인·저장 경로를 통과하는지 확인한다.
7. 권한 결과를 사용자의 언어로 설명한다
승인·거부와 오류 문구는 세 언어에서 같은 의미를 가져야 한다. 짧은 결과는 공통 피드백으로, 수정이나 재시도가 필요한 문제는 문맥 안에서 보여 준다.
버튼보다 먼저 만든 것
이 주가 끝났을 때 하루스페이스에는 더 많은 버튼이 생겼다. 그룹 참여와 생성 요청, 승인과 거부, 안건과 투표, AI 공간 생성과 적용이 화면에 놓였다.
하지만 실제로 만든 것은 버튼의 수가 아니었다.
플랫폼이 볼 수 있는 것과 그룹 구성원이 할 수 있는 것을 나눴다. 사람의 관계와 AI 펫의 배치를 나눴다. AI가 제안한 초안과 사람이 적용한 버전을 나눴다. 현재 상태와 그 상태에 도달한 결정 이력을 나눴다.
두 번의 실패도 이 경계를 더 선명하게 했다. 최고관리자에게 필요한 예외는 그룹 멤버십 전체가 아니라 특정 안건 제안에만 있어야 했다. 새 컬럼은 코드에 존재하는 것만으로 운영에 존재하지 않았고, 버전과 실제 승인 경로가 함께 움직여야 했다.
돌아보면 그룹 주권은 플랫폼이 아무것도 하지 않는다는 뜻이 아니었다. 플랫폼이 무엇을 관리하고 무엇을 대신 결정하지 않는지, 그룹 안의 관계가 어디서 시작되고 끝나는지를 설명할 수 있다는 뜻에 가까웠다.
좋은 권한 설계는 강한 관리자 한 명을 만드는 일이 아니라, 각 결정이 제자리에서만 힘을 갖게 하는 일이었다.
다음 기록에서는 이 경계 위에서 말하는 AI 펫을 실제 개발 동료로 연결하려 했을 때 무엇이 실패했는지, 작업 제안·격리 실행·사람 승인·커밋과 배포 사이를 어떻게 하나씩 이어 갔는지 돌아보려 한다.
반응을 남기면 중복 방지를 위한 무작위 식별자가 이 브라우저에 저장될 수 있습니다.

