깨진 조직 데이터 위에 전사 ERP 를 올리기

2026/07/13사내 시스템 · ERP 구축Next.js · TypeScript · PostgreSQL · Supabase

중고차 유통 업무 전반을 다루는 사내 ERP 입니다. 매입·상품화·영업·탁송·재무· 인사·전자결재까지 203 개 화면이 있습니다.

화면 수가 어려웠던 게 아닙니다. 어려웠던 건 이미 돌아가고 있는 데이터가 깨져 있었다는 점입니다.

부서 트리가 매번 새로 만들어지고 있었다

조직 개편이 있을 때마다 부서 정보가 새 세대로 다시 생성되고 있었습니다. 그런데 사람과 부서를 잇는 관계 데이터는 옛 세대를 가리킨 채 남아 있었습니다.

여기서 결재선을 부서 참조로 저장하면 어떻게 될까요. 다음 조직 개편 때 과거 결재 문서의 결재선이 통째로 깨집니다. 누가 결재했는지 알 수 없게 됩니다.

두 가지로 풀었습니다.

참조를 세대가 바뀌어도 유지되는 식별자로 바꿨습니다. 새로 생성된 부서도 원래 어느 부서에서 온 것인지는 남아 있으므로, 그것을 기준으로 잇습니다.

결재선에 조직·직위 정보를 값으로 복사해 넣었습니다. 결재 문서는 시점의 기록입니다. "이 사람이 그때 이 직위로 결재했다"는 사실은 나중에 조직이 바뀐다고 달라지면 안 됩니다. 참조가 아니라 스냅샷이어야 합니다.

기존 테이블을 하나도 고치지 않았다

전자결재를 붙이면서 기존 테이블 변경은 0 건이었습니다. 결재 관련 테이블 12 개를 새로 만들었습니다.

돌아가는 시스템의 스키마를 건드리면 그 순간부터 배포가 위험해집니다. 새로 만들고 옆에 붙이면, 잘못돼도 새 테이블만 문제입니다. 되돌릴 수 있는 경계를 미리 그어 두는 것이 이 결정의 목적이었습니다.

앱이 데이터베이스 소유자로 붙어 있었다

접근 제어를 데이터베이스 행 수준 정책으로 걸려고 보니, 앱이 소유자 권한으로 접속하고 있었습니다. 소유자는 그 정책을 그냥 통과합니다. 정책을 아무리 잘 써도 효력이 없습니다.

접속 방식을 바꾸는 건 운영 중이라 위험했습니다. 대신 불변성을 데이터베이스 트리거로 강제했습니다. 결재가 끝난 문서는 무슨 권한으로 붙든 못 고칩니다. 정책은 우회 가능하지만 트리거는 아닙니다.

그리고 신규 테이블에 기본으로 부여되는 익명 접근 권한을 명시적으로 회수했습니다. 플랫폼이 편의로 주는 것인데, 결재 문서에는 있으면 안 되는 권한입니다.

알림은 트랜잭션 안에서

결재가 올라가면 알림이 나갑니다. 그런데 알림을 바로 보내면, 결재 저장이 롤백돼도 알림은 이미 나간 뒤입니다.

알림을 보내는 대신 같은 트랜잭션 안에서 알림 요청을 테이블에 넣습니다. 별도 작업자가 그것을 꺼내 보냅니다. 결재가 롤백되면 알림 요청도 함께 사라지므로, 없던 일의 알림이 나가지 않습니다.

여러 작업자가 같은 것을 두 번 보내지 않도록 잠금을 걸어 꺼냅니다.

권한을 토큰에 담기

권한이 화면·기능 단위로 수십 개입니다. 이걸 토큰에 목록으로 넣으면 토큰이 비대해지고, 매 요청마다 그 크기를 실어 나릅니다.

각 권한에 번호를 주고 비트 하나로 표현했습니다. 권한 수십 개가 짧은 16 진수 문자열이 됩니다. 어느 비트가 무슨 권한인지는 별도 카탈로그가 알고, 토큰에는 카탈로그 버전이 함께 들어갑니다. 버전이 어긋나면 클라이언트가 그걸 알아채고 카탈로그를 다시 받습니다.

권한이 추가돼 번호 대응이 바뀌어도, 옛 토큰이 엉뚱한 권한으로 해석되는 사고가 생기지 않습니다.

역할

6 인 팀으로 만들었습니다. 저는 커밋 기준 단일 최다이고 소스 라인 기준 41% 로 1 위입니다. 위에 적은 권한 체계와 전자결재 설계가 제가 맡은 부분입니다.

혼자 만든 것이 아니고, 그렇게 적을 생각도 없습니다.