Workspace 기반 Decision Engine, TraceFlow
의사결정 상태와 접근 제어를 함께 다루는 시스템, 그리고 운영 과정에서 권한 모델과 배포 구조가 어떻게 진화하는지를 기록한 사례
개요
membership만으로는 부족했다 - 운영에서 무너진 권한 모델
TraceFlow는 workspace 단위로 의사결정을 관리하는 시스템이다.
단순히 decision을 저장하는 CRUD 앱이 아니라
상태 전이와 접근 제어를 함께 다루는 decision engine에 가깝다.
이 글은 기능 소개보다는, 이 시스템을 개발/운용하면서 권한 모델과 배포 구조가 어떻게 바뀌었는지에 대한 기록이다..
TraceFlow는 무엇을 다루는 시스템인가
TraceFlow는 다음 세 가지를 동시에 다룬다..
- workspace 단위 접근 제어
- decision lifecycle 관리
- Web / API 분리 구조에서의 권한 일관성
decision은 단순 데이터가 아니라 상태를 가진 객체다.
draft → proposed → approved / rejected
각 상태 전이는 아무나 할 수 있는 것이 아니라, owner 기반 규칙을 따른다.
즉, 이 시스템은 CRUD가 아니라 상태 머신에 가까운 구조다.
membership 기반 접근 제어의 한계
초기 설계는 단순했다.
membership이 있으면 접근 가능
하지만 운영을 시작하면서 바로 문제가 생겼다.
- 특정 사용자를 "로그인은 가능하지만 workspace 접근만 막고 싶다"
- 계정을 삭제하지 않고 접근만 제한해야 한다
- 초대/온보딩 상태와 접근 상태가 섞이기 시작한다
결국 모델을 다음과 같이 확장하게 된다.
membership + access_state
| 상태 | 의미 |
|---|---|
| active | 접근 가능 |
| blocked | 로그인 가능, workspace 접근 불가 |
이 변경은 단순 enum 추가가 아니었다.
- middleware 로직 수정
- session summary 구조 변경
- onboarding 흐름 변경
- admin API 추가
권한 모델은 항상 한 줄 추가로 끝나지 않는다.
접근 제어는 어디에서 해야 하는가
TraceFlow에서 접근 제어는 Web에서 1차적으로 차단하고, API에서도 동일 규칙을 강제한다
Browser → Web (middleware) → API → DB
Next.js middleware가 다음을 담당한다.
- unauth → /auth redirect
- access_state != active → /access redirect
이 구조 덕분에 API까지 요청이 도달하기 전에 대부분의 접근을 차단할 수 있다. 하지만 middleware는 어디까지나 1차 게이트일 뿐이다. 실제 권한 검증은 API에서도 동일하게 수행된다.
- API는 session을 기반으로 user summary를 구성하고
- access_state가 active가 아니면 실제 decision actor로 인정하지 않는다
즉, TraceFlow의 접근 제어는 "Web에서 차단 + API에서 강제"되는 이중 구조다.
middleware 한 줄 때문에 접근 제어가 깨진 사건
실제 문제는 정규식 자체보다, "소스 코드에서 쓴 escape가 빌드 후 matcher 문자열에 어떻게 반영되느냐"에 있었다.
잘못된 코드:
.*\..*
올바른 코드:
.*\\..*
차이는 \ 하나였지만,
결과적으로 middleware matcher가 깨지면서 보호 경로가 적용되지 않았다.
문제는 단순 escape였다.
하지만 결과는 치명적이었다.
- middleware matcher가 깨짐
- 보호 경로(/decisions)가 적용되지 않음
- 보호 대상 경로가 middleware matcher에서 빠지면서, 인증 체크 없이 서버 컴포넌트가 그대로 실행되었다
이걸 디버깅하면서 발견 된 사실:
Next.js middleware는 "코드"가 아니라 "빌드된 matcher 문자열"이 실제 동작을 결정한다
그래서 소스 코드만 보는 게 아니라 .next/server/middleware-manifest.json까지 확인해야 했다.
systemd와 배포 스크립트가 충돌한 이유
또 하나의 문제는 배포에서 발생했다.
기존 방식:
- 포트 기반 kill
- nohup으로 새 프로세스 실행
운영 환경:
- systemd (Restart=always)
결과:
EADDRINUSE
systemd가 재시작하려는 프로세스와
스크립트가 새로 띄운 프로세스가 충돌했다.
해결 방법:
systemctl restart service
핵심 교훈:
프로세스 제어는 반드시 한 곳(systemd)으로 통일해야 한다
배포 순서는 코드만큼 중요하다
access_state를 도입하면서 배포 순서가 깨지면 바로 장애가 발생했다.
정상 순서는 다음과 같다.
- DB migration
- shared/database build
- API deploy
- Web deploy
이유:
- API는 새 컬럼을 읽는다
- Web은 새 session summary를 기대한다
이 순서를 어기면:
- API가 죽거나
- Web이 전체 사용자를 /access로 보내는 상황이 발생한다
배포 순서도 일종의 계약이다.
배포 순서는 단순 절차가 아니라, DB schema → API contract → Web expectation이 연결된 "런타임 계약 순서"다
코드가 맞는데 동작이 틀린 이유
이메일 HTML 템플릿을 추가했는데 실제 메일은 계속 텍스트로만 전송된 적이 있었다.
원인은 코드가 아니라 다음이었다.
- 프로세스가 옛 dist를 계속 실행 중
- 새 빌드가 로드되지 않음
결론:
문제 해결에서는 코드 자체보다, 실제 어떤 빌드가 실행 중인지 확인하는 것이 더 중요하다
권한 모델은 단순 role 수준을 넘어서면, 시스템 전 레이어에 영향을 미치기 시작한다.
access_state 하나 추가하면서 영향을 받은 영역:
- DB schema
- API response (user summary)
- Web middleware
- UI 흐름 (/access 페이지)
- onboarding 로직
- admin API
즉, 권한 모델은 단순 데이터가 아니라 시스템 전체 계약이다.
정리
TraceFlow를 만들면서 얻은 결론은 다음과 같다.
- decision lifecycle은 상태 머신으로 다뤄야 한다
- 접근 제어는 membership만으로 끝나지 않는다
- middleware는 진입 경로를 제어하는 1차 게이트이며, 최종 권한 검증은 반드시 API에서 수행되어야 한다
- 배포 구조와 권한 모델은 강하게 연결되어 있다
- 운영에서는 코드보다 실행 상태가 더 중요하다
이 프로젝트는 기능 소개보다도, "운영 중인 시스템에서 권한 모델과 배포 안정성이 어떻게 함께 진화하는지"를 보여주는 예이다.
이 개발자의 다른 프로젝트
그누보드7 예약 모듈 - glitter-reservation
코어 수정 없이 붙여 사용하는 그누보드7 예약 모듈로, 신청·인증·조회·취소·관리까지 예약 흐름 전체를 모듈 단위로 제공합니다.
그누보드7 상담예약 템플릿 - Glitter Academy Core
GnuBoard7 Template 엔진 기반으로 상담예약 사이트 구성을 위한 초기 템플릿 예제로 설계되었습니다.
그누보드7 커뮤니티 템플릿 - SIRsoft Community
쇼핑몰(이커머스 의존)을 제거하고 커뮤니티 기능에 집중한 경량 사용자 템플릿, ID : sirsoft-comm