"고객 A를 위한 코드를 복사해 B 버전을 만들다 아찔한 '데이터 누출'을 경험했습니다. 이 위기를 계기로 흩어져 있던 앱을 하나로 통합하고, UI 전환 안정성을 확보했으며, 쿼리 레벨 데이터 격리를 통해 신뢰할 수 있는 SaaS로 재탄생시켰습니다. 95%의 성능 개선은 그 과정에서 얻은 보너스 였죠."
배경: 쉽게 생각한 Ctrl+C, Ctrl+V
초기 버전의 앱은 성공적이었습니다. 너무 성공적인 나머지, 새로운 고객이 생겼을 때 가장 원시적으로 큰 효과를 볼 수 있는 작업을 택했습니다. 기존업체 앱 을 그대로 복사해 신규업체 앱을 만드는 것이었죠.
겉보기엔 문제가 없어 보였습니다. 하지만 통계 페이지에서 '신규업체'의 매출 데이터가 '기존업체'에 보이는 순간을 마주하고 나서야 이것은 더 이상 '복사-붙여넣기'로 해결할 수 있는 문제가 아님을 깨달았죠. 앱이 아마추어의 티를 벗고, 진짜 '어른'이 되어야 할 시간이었습니다.
복붙은 만능이야!
접근: 통합, 전환, 격리의 3축 전략
위기를 극복하기 위해, 저희 AI 어벤져스 팀은 세 가지 핵심 축을 중심으로 프로젝트를 재설계했습니다.
통합 (Unification): 여러 코드베이스를 하나로 통합하여 유지보수 비용을 줄인다.
전환 (Transition): 불안정한 URL 쿼리 파라미터 방식 대신, session_state를 이용해 고객사(테넌트) 전환 경험을 안정적이고 즉각적으로 만든다.
격리 (Isolation): 가장 중요한 목표. 모든 데이터베이스 조회 경로에 customer_id를 강제하여, 데이터가 섞일 가능성을 원천적으로 차단한다.
🤖 코딩 파트너, Claude: VS Code 워크플로우의 혁신
이번 멀티테넌트 전환처럼 복잡한 작업을 진행할 때, 저의 개발 워크플로우 중심에는 Anthropic의 공식 'Claude' VS Code 확장 프로그램이 있었습니다. 이것은 여러 AI와 번갈아 대화하는 방식이 아니라, 마치 제 코드 편집기 안에 지능을 가진 '페어 프로그래머'가 짝이 되어 함께하는 경험과 같았습니다. 특히 최신 모델인 'Claude 4.5 Sonnet' 의 작업 능력은 상상을 초월했습니다.
데이터베이스 설계의 가속화: tenant_id 개념을 설명하자, Claude는 복잡한 관계와 보안 정책(RLS)까지 포함된 완성형 SQL 스크립트 초안을 즉시 생성해주었습니다. 덕분에 DB 설계 시간을 획기적으로 단축할 수 있었습니다.
대규모 리팩토링 자동화: "모든 통계 함수에 customer_id를 파라미터로 추가해줘" 같은 전문적인 지시가 아니더라도, "통계 부분을 업체별로 나누어줘" 라는 쉬운 말로 요청하자 수백 줄의 코드를 분석하고 일관된 패턴으로 수정해주었습니다. 자칫 실수가 발생하기 쉬운 반복 작업을 완벽하게 대신 처리해준 것입니다.
문제 해결의 실마리 제공: 불안정한 URL 전환 로직을 session_state 기반으로 바꾸는 과정에서 막혔을 때, Claude는 Streamlit의 st.rerun() 메커니즘에 대한 정확한 코드 예시와 설명을 제공하여 길을 열어주었습니다.
물론 단점도 있었습니다. 무료 플랜의 메시지 제한은 정말 사람 감질맛 나게 만드는 재주를 가졌더군요. 하지만 복잡한 앱의 기능을 물 흐르듯 코딩해주는 그 능력은, 유료 결제를 하더라도 전혀 아깝지 않은 경험이었습니다.
창만 열어두어도 믿음직하네요.
개발자가 문제의 본질과 방향성을 정의하고, VS Code 안의 AI가 구체적인 코드 구현과 리팩토링을 돕는 방식은
'1인 개발'의 생산성을 '1개 팀'의 수준으로 끌어올리는 강력한 시너지 효과를 만들어냈습니다.
설계와 구현: 문제 해결의 핵심 디테일
1. 핵심 전환 로직: URL을 버리고 Session State를 택하다
문제: URL 쿼리 파라미터(?tenant=고객사) 방식은 새로고침 타이밍이 꼬이며 간헐적으로 오류를 발생시켰습니다.
해결: 사이드바에서 고객사를 선택하면, 그 값을 st.session_state['tenant_id']에 저장하고 즉시 st.rerun()을 호출했습니다. 이제 앱의 모든 상태는 session_state라는 단 하나의 '진실의 원천'에 의해 관리되어 완벽하게 안정적인 전환이 가능해졌습니다.
2. 통계 페이지 누출 해결: 모든 함수에 '꼬리표' 달기
문제:get_sales_statistics() 같은 일부 통계 함수에 고객사를 구분하는 필터가 누락되어 있었습니다.
해결: 모든 통계 및 데이터 조회 함수에 customer_id를 필수 파라미터로 추가했습니다. UI단에서부터 DB 쿼리의 마지막 WHERE 절에 이르기까지, 모든 데이터 요청에 '이 데이터의 주인은 누구인가'라는 꼬리표가 따라다니도록 강제했습니다.
성과: 숫자로 증명된 성장
지표
개선 전 (v0.7)
개선 후 (v0.9)
효과
데이터 신뢰도
통계 데이터 누출 발생
완벽한 데이터 격리 보장
SaaS로서의 기본 신뢰 확보
운영 효율
고객사별 앱 파일 분리
단일 통합 앱
유지보수 비용 80% 절감
사용자 경험(UX)
URL 기반의 불안정한 전환
사이드바를 통한 즉각적이고 안정적인 전환
전환 오류 0건
성능
초기 10초 재조회 20초
초기 10~20초, 재조회 1초 미만
체감 성능 95% 개선
3가지 멀티테넌트 개발 원칙
이번 프로젝트를 통해 저희 팀은 세 가지 중요한 원칙을 배웠습니다.
상태(State)가 우선, URL은 후순위. 사용자의 상태(tenant_id 등)는 오직 session_state라는 단일 진실의 원천에서만 관리되어야 합니다. URL 쿼리 파라미터는 그저 외부에서 보내는 '요청'일 뿐, 시스템의 주축이 되면 위험해요.
격리의 시작은 UI가 아닌 쿼리다. "이 고객에게는 이 페이지만 보여주자"는 생각은 위험합니다. "해당 쿼리는 반드시 해당 고객 ID를 포함해야 한다"는 원칙이 DB 계층부터 강제되어야 데이터 누수를 완벽히 막을 수 있습니다.
성능은 캐시와 인덱스 조율. 캐시는 사용자가 느끼는 '평균 속도'를 높여주고, 인덱스는 데이터가 많아졌을 때의 '최악의 속도'를 방어합니다. 둘 중 하나만으로는 결코 안정적으로 만들 수 없습니다.
다음 단계
이제 견고한 멀티테넌트 기반을 갖춘 만큼, 다음 단계로는 Supabase RLS(행 수준 보안) 정책을 도입하여 앱 외부에서의 데이터 접근까지 완벽히 통제하고, 고객사별 사용량 모니터링을 위한 운영 대시보드를 구축할 계획입니다.