Streamlit 대시보드와 AI 코딩의 만남: 엔드 투 엔드 (end-to-end) 개인정보 보호 워크플로우
요약
Streamlit 대시보드 개발 시 AI 코딩 도구를 사용할 때 발생할 수 있는 5가지 데이터 유출 경로를 분석하고, 이를 차단하기 위한 엔드 투 엔드 개인정보 보호 워크플로우를 소개합니다.
핵심 포인트
- Streamlit 앱의 코드, 스키마, 샘플 데이터, 비밀 정보, 프레임워크 구조는 모두 유출 위험이 있음
- Claude Code, Codex, Cursor 사용 시 준비 없는 프롬프트는 민감 정보를 외부로 노출함
- PromptCape 워크플로우를 통해 난독화부터 실행까지 전 과정을 보호할 수 있음
- 식별자 이름 변경 등 구체적인 채널별 차단 방법을 통해 보안을 강화함
이전 기사들은 각각 하나의 유출 경로를 차단했습니다. 실제 Streamlit + pandas 대시보드는 한 번에 다섯 가지 경로를 통해 유출됩니다. 이번에는 난독화(obfuscate), 실행(run), 프롬프트(prompt), 적용(apply)에 이르는 전체 세션을 다루며, 모든 채널을 고려합니다.
대시보드가 까다로운 사례인 이유
이 시리즈의 이전 기사들은 각각 하나의 접점을 다루었습니다: 런타임 워크스페이스와 .env, 투명 프록시 (transparent proxy), pandas 컬럼 이름 (column names).
데이터 대시보드는 이 모든 것들이 충돌하는 지점입니다. Streamlit 앱은 다음과 같은 특성을 동시에 가진 드문 Python 프로젝트이기 때문입니다:
- 코드 (Code): AI가 읽고 편집하는 부분 (비즈니스 로직, 서비스 클래스).
- 스키마 (Schema): pandas 컬럼 이름으로 표현되는 구조 (
churn_probability,annual_salary). - 샘플 데이터 (Sample data): AI가 열 수 있는 CSV 파일에 담긴 데이터.
- 비밀 정보 (Secrets):
.env에 포함된 데이터베이스 URL, API 키 등. - 프레임워크 구조 (Framework structure): 앱이 렌더링되기 위해 손상되지 않고 유지되어야 하는 Streamlit의
st.XAPI 및 페이지 탐색 구조.
이 프로젝트에 Claude Code, Codex 또는 Cursor를 아무런 준비 없이 사용한다면, 첫 번째 프롬프트에서 이 다섯 가지 정보가 모두 당신의 기기를 떠나게 됩니다. 이 기사에서는 _"CSV 내보내기 기능이 포함된 지역별 이탈률 막대 차트를 추가해줘"_라는 현실적인 기능 요청을 PromptCape 워크플로우 전체를 통해 실행하며, 모든 채널을 관리하는 방법을 다룹니다.
프로젝트
인력/HR 대시보드입니다. Streamlit 프런트엔드, 집계를 위한 pandas, 소스 행을 위한 SQLAlchemy, 자격 증명을 위한 .env, 그리고 로컬 실행을 위한 고정 CSV 파일로 구성됩니다.
attrition-dashboard/
├── Dashboard.py # Streamlit 진입점
├── pages/
...
약간 다듬어진 Dashboard.py:
import streamlit as st
import pandas as pd
...
이 파일의 다섯 줄은 민감한 정보를 유출하며, 각각 서로 다른 메커니즘을 통해 서로 다른 정보를 유출합니다. 그것이 핵심입니다.
5가지 채널과 각 채널의 차단 방법
| # | 채널 (Channel) | Dashboard.py에서 유출되는 내용 | 차단 방법 |
|---|---|---|---|
| 1 | 비즈니스 식별자 (Business identifiers) | StaffingService, list_assignments, load_frame | 식별자 이름 변경(Identifier rename) → Cls_… / mtd_… (article 1) |
| ... | |||
| 아래의 전체 워크플로우는 단순히 다음과 같습니다: 이 단계들을 순서대로 하나씩 실행하고, 아무것도 망가지지 않았는지 확인하십시오. |
Step 0 — 실제 소스에 대한 상태 점검 (health check)
항상 '그린(green)' 상태에서 시작하십시오. 만약 소스 자체가 실행되지 않는다면, 난독화(obfuscation)로 인한 노이즈를 본인이 만든 버그와 구분할 수 없게 됩니다.
cd ~/projects/attrition-dashboard
streamlit run Dashboard.py # 렌더링, 차트 로드, 트레이스백(traceback) 없음 → 양호한 기준점(baseline)
Step 1 — 난독화 (obfuscate)
promptcape obfuscate --language python --verify .
# -> ~/.promptcape/cache/a1b2c3d4/
엔진이 한 번의 패스(pass)로 수행하는 작업과 5가지 채널 간의 매핑은 다음과 같습니다:
- (1) 식별자 이름 변경 (Identifiers renamed).
StaffingService→Cls_…,list_assignments→mtd_…,load_frame→mtd_…, 로컬 변수df/rows/session→fld_…. 이는Dashboard.py,pages/,staffing/service.py전체에 걸쳐 일관되게 적용됩니다. - (2) 컬럼 등록 (Columns registered).
PandasColumnDetector가 컬럼 위치를 탐색합니다 —df["employment_status"],df["tenure_months"], 할당된df["tenure_years"],df['churn_probability'].mean()— 그리고 각 컬럼을col_…해시(hash)에 매핑합니다.df는 (pd.DataFrame(rows)로부터 할당되어) 증명 가능한 DataFrame이므로 그 하위 인덱스(subscripts)는 스코프(scope) 내에 존재하지만,session["…"]접근은 스코프 내에 있지 않습니다. - (3) 피스처 재작성 (Fixture rewritten).
data/sample_attrition.csv의 헤더 행이 동일한 레지스트리(registry)로 재작성되므로, 난독화된 코드의col_…이름이 피스처(fixture)와 일치하게 되어streamlit run이 샘플 데이터에서도 여전히 작동합니다. - (4)
.env제외 ( .env left behind)..env파일은 복사되지 않습니다. 대신 소스 경로와promptcape run을 사용하라는 메모가 포함된.env.promptcape-pointer파일이 작성됩니다. - (5) Streamlit API 동결 (Streamlit API frozen).
import streamlit as st가 감지되는 즉시, 약 80개의 모듈 이름(set_page_config,title,metric,dataframe,sidebar,selectbox, …)이 프로젝트 전역 제외 목록(exclusion list)에 추가됩니다.StaffingService스타일의 식별자들은 이 목록에 포함되지 않습니다.
--verify 단계는 정적 임포트 분석(static import resolution, 실행은 하지 않음)을 수행하며, 해결(resolve)되지 않는 모든 임포트를 보고합니다. 대표적인 오류 사례는 사용자의 식별자가 충돌하여 표준 라이브러리(stdlib) 임포트가 실수로 이름이 변경되는 경우입니다.
캐시(cache) 내의 Dashboard.py 모습은 다음과 같습니다:
import streamlit as st
import pandas as pd
...
모든 st.X는 온전하게 유지됩니다 (정상적으로 렌더링됨). 모든 컬럼은 해시(hash) 처리되어 스키마 유출이 없습니다 (no schema leak). StaffingService.list_assignments는 사라졌습니다 (비즈니스 로직 유출 방지). 페이지 설정 문자열인 "Attrition"과 메트릭 레이블인 "Avg churn risk"는 그대로 유지됩니다. 이들은 스키마가 아닌 사용자에게 보이는 UI 텍스트이며, '값은 절대 재작성하지 않는다'는 규칙 덕분에 AI가 레이아웃을 추론할 수 있도록 읽기 가능한 상태로 유지됩니다.
2단계 — 난독화된 워크스페이스 실행 (run the obfuscated workspace)
이 단계는 Python을 Java와 구분 짓는 단계입니다. 개발자가 워크스페이스를 직접 _실행(run)_하기 때문에, 비밀 정보(secrets)는 캐시(cache)의 디스크에 한 번도 저장되지 않은 채 실행 시점에 전달되어야 합니다.
cd ~/.promptcape/cache/a1b2c3d4
promptcape run streamlit run Dashboard.py
promptcape run은 다음을 수행합니다:
- 캐시로부터 소스 프로젝트를 해석(Resolves)합니다.
~/projects/attrition-dashboard/.env파일을 파싱(Parses)합니다.cwd(현재 작업 디렉토리)를 캐시로, 자식 환경(child environment)을 OS 환경 변수 + 파싱된.env값으로 설정하여streamlit run Dashboard.py를 생성(Spawns)합니다.- TTY를 상속(Inherits)하여 Streamlit의 브라우저 열기 및 핫 리로드(hot-reload) 기능이 정상적으로 작동하게 합니다.
워크스페이스 내부의 os.getenv("DATABASE_URL")는 실제 URL을 반환하지만, 캐시 디렉토리에는 여전히 비밀 정보가 포함되어 있지 않습니다. 대시보드는 0단계와 동일하게 렌더링됩니다(동일한 차트, 동일한 수치). 하지만 AI가 읽을 수 있는 모든 파일은 난독화(obfuscated)되어 있습니다.
3단계 — AI 연결 (두 가지 동등한 경로)
경로 A — CLI, 명시적 방식. Claude Code를 캐시 디렉토리로 지정하고 그곳에서 작업합니다. 작업이 완료되면 적용(Apply)합니다.
경로 B — Cursor 내 투명 프록시(transparent proxy). PromptCape 프록시를 실행하고, Cursor의 터미널을 열어 pcc(ANTHROPIC_BASE_URL을 내보내는 런처)를 입력합니다. 사용자는 실제 이름(예: "이탈 차트(churn chart)를 추가해줘")을 사용하여 일상적인 언어로 프롬프트를 입력합니다. 프록시는 나가는 과정에서 churn을 해당 매핑 값으로 번역하고, 들어오는 과정에서 모델의 답변을 다시 번역합니다. 개발자는 해시(hash)를 절대 볼 수 없으며, 서비스 제공업체는 실제 이름을 절대 볼 수 없습니다.
어떤 방식을 사용하든, 반대편의 모델은 난독화된 워크스페이스와 난독화된 프롬프트를 전달받게 됩니다.
4단계 — 기능 요청
개발자가 (프록시를 통해 실제 이름을 사용하여) 요청합니다:
지역별 평균 이탈 확률(average churn probability)의 막대 차트를 추가하고, 필터링된 테이블을 CSV로 다운로드할 수 있는 버튼을 만들어줘.
모델이 받는 내용은 col_… 공간의 언어로 표현되며, 난독화된 프레임워크를 대상으로 작동합니다. 모델이 작성하는 내용은 다음과 같습니다:
st.subheader("Churn by region")
fld_chart = (
fld_9c8d7e6f.groupby("col_1f7b3d6a")["col_e2d4b7c9"]
...
모델은 파일에서 확인한 기존의 col_e2d4b7c9 (이탈, churn) 및 col_1f7b3d6a (지역, region) 컬럼을 정확하게 재사용했으며, 손상되지 않은 Streamlit 인터페이스로부터 st.bar_chart / st.download_button을 사용했습니다. 모델은 이 내용들이 무엇을 의미하는지 전혀 몰랐으며, 알 필요도 없었습니다.
5단계 — 적용 및 검증 (apply and verify)
promptcape run streamlit run Dashboard.py # AI의 변경 사항, 여전히 난독화된 공간에 있음 → 렌더링
promptcape apply # col_/fld_/mtd_/Cls_를 실제 이름으로 역매핑 (reverse-map)
streamlit run Dashboard.py # 난독화 해제됨, 실제 소스에서 실행 → 렌더링
apply를 거친 후, 새로운 코드는 개발자 자신의 어휘로 읽힙니다:
st.subheader("Churn by region")
chart = (
df.groupby("region")["churn_probability"]
...
컬럼 레지스트리(column registry)를 통해 col_1f7b3d6a → region, col_e2d4b7c9 → churn_probability로 변환되었습니다. fld_chart는 AI가 만들어낸 것이므로, 개발자가 이름을 다시 지정할 때(여기서는 chart) 원래대로 돌아옵니다.
CSV 라벨, 파일 이름, MIME 타입은 처음부터 끝까지 문자열이었으며, 전혀 손대지 않은 상태로 유지되었습니다.
실패 모드 정리 (The failure modes, consolidated)
각 단계에는 특징적인 실패 방식이 하나씩 있습니다. 이를 알고 있으면 혼란스러운 렌더링 결과도 한 줄의 수정으로 해결할 수 있습니다.
| 증상 | 원인 | 해결책 |
|---|---|---|
AttributeError: module 'streamlit' has no attribute 'fld_xxx' | 사용자 식별자가 제외 목록에 없는 st.X 이름과 충돌함 (error, exception은 의도적으로 제외 목록에서 빠져 있음) | 해당 이름을 ~/.promptcape/mappings/<hash>-exclusions.txt에 추가 |
| ... |
엔드 투 엔드 위협 경계 (The end-to-end threat boundary)
전체 세션 동안 실제로 머신을 떠난 데이터와 떠나지 않은 데이터는 다음과 같습니다:
| 채널 (Channel) | AI 제공업체에 도달했는가? |
|---|---|
비즈니스 클래스/메서드 이름 (StaffingService.list_assignments) | 아니요 — 해싱(hashed)됨 |
| ... | |
PromptCape가 긋는 경계선은 다음과 같습니다: AI는 정확한 코드를 작성할 수 있을 만큼의 충분한 구조(structure)는 보되, 사용자의 비즈니스를 재구성하는 데 사용할 수 있는 정보는 전혀 보지 못합니다. groupby는 눈에 보이는 대로 groupby일 뿐이며, 그것이 *지역별 이직 위험(attrition risk by region)*을 그룹화한다는 사실은 네트워크 상에 노출되지 않습니다. |
결론 (Conclusion)
Streamlit 대시보드는 이 전체 시리즈의 통합 테스트(integration test)입니다. 왜냐하면 단일 기술만으로는 이를 모두 커버할 수 없기 때문입니다. 식별자 이름 변경(identifier renaming), pandas 컬럼 등록(column registration), 픽스처 헤더 재작성(fixture header rewriting), 서브프로세스 실행 시 비밀 정보 주입(subprocess-launch secret injection), 그리고 프레임워크 제외 목록(framework exclusion list)이 서로 충돌 없이 조화롭게 구성되어야 합니다.
이러한 구성을 가능하게 하는 세 가지 요소는 다음과 같습니다:
- 단일 패스(One pass), 5개 채널, 일관된 해싱(consistent hashing). 식별자, 컬럼, 픽스처 헤더가 모두 동일한 레지스트리(registry)를 사용하므로, 메서드, 컬럼, CSV 헤더로 나타나는 이름은 하나의 매핑(mapping)으로 왕복합니다. 독립적인 서브시스템들이 하나의 단일 진실 공급원(source of truth)을 공유하는 것이
apply를 정확하게 유지하는 비결입니다. - 워크스페이스를 단순히 읽지 말고 실행하세요. 대시보드는 매 단계마다 렌더링되어야 하며, 그렇지 않으면 실제 버그와 난독화로 인한 결과물을 구분할 수 없습니다.
promptcape run은 비밀 정보를 디스크에 저장하지 않고도
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기