Workspace 기반 Decision Engine, TraceFlow

의사결정 상태와 접근 제어를 함께 다루는 시스템, 그리고 운영 과정에서 권한 모델과 배포 구조가 어떻게 진화하는지를 기록한 사례

2026-04-06 (월) 02:03:04 조회 1,409
문의하기
Workspace 기반 Decision Engine, TraceFlow
Node.js PostgreSQL Next.js NestJS middleware를 접근 제어 경계로 사용 membership + access_state + decision lifecycle

개요

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를 도입하면서 배포 순서가 깨지면 바로 장애가 발생했다.

정상 순서는 다음과 같다.

  1. DB migration
  2. shared/database build
  3. API deploy
  4. 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에서 수행되어야 한다
  • 배포 구조와 권한 모델은 강하게 연결되어 있다
  • 운영에서는 코드보다 실행 상태가 더 중요하다

이 프로젝트는 기능 소개보다도, "운영 중인 시스템에서 권한 모델과 배포 안정성이 어떻게 함께 진화하는지"를 보여주는 예이다.

댓글 (0)

로그인 후 댓글을 남길 수 있습니다.
아직 댓글이 없습니다. 첫 번째 댓글을 남겨보세요!