개인의 매출 관리에서, 매월 '매출 명세서(PDF)'와 '납품서(xlsx)'를 눈으로 대조하는 작업이 있었습니다.
이를 Slack에서 한마디로 요청할 수 있도록 하려고 이번에 툴을 만들었습니다.
- Slack에 '조회 부탁드립니다'라고 전송
- Google Drive에 있는 최신 매출 명세서 PDF를 읽음
- 납품서에서 만든 상품 테이블과 대조함
- 판매된 상품(품번이나 명칭, 단가)을 스레드에 회신
어려운 점은, 두 파일 모두 주소나 성명 등의 개인정보가 포함되어 있다는 것입니다.
이 글에서는 다음 3가지 판단을 중심으로 전체 설계를 소개합니다.
- 당초 안에서 생성 AI를 포기한 이유 - Google Drive로의 출입구를 **하나의 컨테이너(PEP: Policy Enforcement Point)**로 통합하여 개인정보가 그곳을 벗어나지 않도록 구성 - 쓰기를 '금지'하는 것이 아니라, 쓰는 수단을 애초에 갖지 못하게 하는 생각
자세한 구현은 다른 글에서 주제별로 다룰 예정입니다.
첫 번째 안과 변경 이유
처음에는 다음과 같은 구성을 생각했습니다.
- Slack에 PDF를 게시
- 생성 AI로 품번을 추출
- MCP Server 경유하여 Google 스프레드시트의 행에 색칠하기
설계를 진행하면서 다음 사실을 알게 되었습니다.
| 알게 된 것 | 설계에 미치는 영향 |
|---|
| 매출 명세서는 텍스트 PDF이며, '상품명'란에는 품번의 숫자 부분만 적혀 있다. 숫자 부분만으로도 유일함 | pdftotext로 단어 좌표를 추출하여, '상품명'란만 잘라내고 정규 표현식으로 추출할 수 있음 |
| 납품서의 실체는 xlsx (Office 편집 모드) | Sheets API로는 처리할 수 없음 |
| 결과는 Slack에 반환하는 것만으로 충분 | Google 측에 쓰기가 필요하지 않음 |
AI를 사용하면 PDF의 전체 내용(고객명이나 주소 포함)을 외부 API로 전송하게 됩니다.
또한, 파일에 적힌 문장으로 모델의 동작이 바뀌는 간접 프롬프트 인젝션에도 대비해야 합니다 (OWASP에서는 'LLM01: Prompt Injection'으로 웹사이트나 파일 등 외부 입력에 의한 동작 변화를 언급하고 있습니다).
이번 추출은 결정적인 코드로 작성할 수 있으므로, AI를 사용하는 장점보다 이러한 위험이 더 크다고 판단했습니다.
AI를 포기하면 다음과 같습니다.
- 개인정보를 외부 AI 서비스로 전송하지 않음
- PDF에 무엇이 적혀 있어도, 그것은 '단순 문자열'이 됩니다. 대조 후보로 통하는 것은 숫자만으로 된 토큰에 한정됩니다
- API 키도 토큰 비용도 필요 없어집니다
AI를 포기했으므로 MCP도 포기했습니다.
MCP는 'LLM 애플리케이션과 외부의 데이터나 툴을 연결'하는 프로토콜이며, Tools는 AI 모델이 실행하는 함수로 정의됩니다.
이번 호출 측은 결정적인 워커(worker)만 사용하며, AI가 Tool을 선택하여 호출하는 일이 없습니다.
따라서 일반 내부 HTTP API를 사용했습니다.
MCP를 포기해도 안전성은 떨어지지 않습니다.
안전성을 담당하는 것은 MCP나 HTTP 같은 프로토콜이 아니라, 프로세스의 분리·조작의 제한·Policy·감사 로그이기 때문입니다.
미래에 AI 클라이언트에서 호출하게 된다면, 이 내부 API 앞에 MCP 창구를 추가하기만 하면 되고, Policy와 감사 로그는 그대로 사용할 수 있습니다.
전체 구성과 신뢰 경계(Trust Boundary)
컨테이너는 2개입니다.
| 서비스 | 역할 | 보유하는 비밀 정보 |
|---|
| drive-gateway | Google Drive로의 유일한 출입구. 읽기 전용 엔드포인트 4개만 공개하고, 분석 및 최소화된 값만 반환 | Google 서비스 계정 키 |
| worker | Slack을 폴링하며 정해진 절차로 처리함 (납품서 차분 동기 → 대조 → 회신) | Slack Bot Token |
drive-gateway는 NIST SP 800-207(Zero Trust Architecture)에서 말하는 PEP(Policy Enforcement Point)[1]의 역할을 합니다. 리소스(Drive)에 대한 접근은 모두 이곳을 통하며, 매번 Policy로 판정됩니다.
신뢰 경계(Trust Boundary)를 세 가지로 나누었습니다.
- 파일 내용: PDF나 xlsx의 내용은 외부 입력으로 간주합니다. 형식, 길이, 패턴을 검증하고 분석은 자식 프로세스에 격리시킵니다.
- 컨테이너 분리: Google 키와 개인정보가 포함된 원본 데이터는 drive-gateway 내부에만 존재합니다.
- Google 권한: 서비스 계정은 '뷰어(Viewer)'이며, 스코프는
drive.readonly입니다.
따라서 쓰기나 삭제는 애초에 할 수 없습니다.
데이터 최소화: 어디서 무엇을 버릴 것인가
처리별로 외부에 노출할 정보를 구체적으로 결정했습니다.
| 처리 | 담당 | 외부 노출 정보 |
|---|
| 매출 명세 PDF에서 '상품명' 열만 추출하여 숫자 후보를 가져오는 작업 | drive-gateway | 숫자 후보와 행 번호만 (PDF도 본문도 제외) |
| 납품서 명세표 범위에서 결정된 열만 추출하는 작업 | drive-gateway | 명세표의 결정된 열만 (주소・성명은 제외) |
| 차이 동기화・대조・회신 | worker | Slack에는 판매 상품(품번이나 명칭)과 건수, 대조할 수 없었던 숫자 후보와 행 번호만 |
핵심 포인트는 다음과 같습니다.
매출 명세 파일명은 'YYYY년 M월 <이름>님.pdf' 형태입니다. API 응답, 감사 로그, Slack 어디에도 파일명을 노출하지 않고, 연월(예: 2026-09) 같은 값만 사용합니다.
'12행: 단가가 숫자가 아님'처럼 위치와 종류만 작성합니다. 값을 넣게 되면 경고를 거치면서 유출될 수 있기 때문입니다.
- 원본 데이터를 반환하는 엔드포인트를 만들지 않습니다.
가령 worker가 탈취되더라도, API에 PDF나 xlsx를 돌려줄 통로가 없으므로 파일 본체는 가져갈 수 없습니다.
drive-gateway 프로세스 내부에는 개인정보가 포함된 원본 데이터가 일시적으로 존재합니다.
PDF의 바이트열은 메모리와 tmpfs 상의 임시 파일에 저장되며, xlsx도 분석 라이브러리가 시트를 메모리에 읽어 들입니다.
지키는 것은 'gateway 밖으로 나가지 않게 하는 것'이며, gateway 안에서 절대 보지 않는 것을 의미하지 않습니다.
그렇기 때문에 gateway에는 키 외의 비밀 정보를 가지고 있지 않고, 상태도 가지지 않으며, 공개하는 작업을 최소화하고 있습니다.
쓰기를 '금지'하기보다 수단을 갖지 않게 하기
Google에 대해서는 여러 겹으로 '쓸 수 없게' 만들었습니다.
| 계층 | 조치 |
|---|
| Google 권한 | 공유 폴더에 서비스 계정을 '뷰어'로 추가합니다 |
| ... | |
// Google의 액세스 토큰. 스코프는 drive.readonly만 요구한다
pub const DRIVE_READONLY: &str =
- 어디에서 실패하든, 그곳에서 중지하고 감사 로그에 기록한다。
감사 로그에 기록할 수 없을 때는,
**Drive API를 호출하기 전에** 중지한다 (Fail Closed) - 클라이언트로부터 받은 `file_id`는 신뢰하지 않고, 매번 Drive의 메타데이터로 「허용된 폴더 바로 아래인지」「이름 패턴」「mimeType」「휴지통에 들어가지 않았는지」「크기」를 확인한다.
이러한 '순서'와 'Fail Closed'의 상세 내용은 다른 글로 다룰 예정이다.
## 요약
- 추출을 결정적인 코드로 작성할 수 있다면, AI를 사용하지 않음으로써 개인정보의 외부 전송과 프롬프트 인젝션(Prompt Injection) 둘 다를 **근본적으로 없앨** 수 있다. - 개인정보를 처리하는 과정을 하나의 컨테이너(PEP)에 가두고, 외부에 노출되는 항목을 처리별로 결정한다. 파일명이나 warnings와 같은 '곁가지'도 포함하여 결정한다.
- '쓰기를 금지'하는 것이 아니라, 권한・스코프・코드・API의 모든 계층에서 **쓸 수 있는 수단 자체를 없애는** 것이다.
## 참고
각 링크 아래에는 본문의 어느 절에서 참조했는지 적어두었습니다.
-
OWASP Top 10 for LLM Applications: LLM01 Prompt Injection
- 참조: '첫 번째 안과, 바꾼 이유'(간접 프롬프트 인젝션)
-
Model Context Protocol Specification (최신 버전)
- 참조: '첫 번째 안과, 바꾼 이유'(MCP를 포기한 이유)
-
NIST SP 800-207 Zero Trust Architecture
- 참조: '전체 구성과 신뢰 경계(Trust Boundary)' (PEP)
-
Google Drive API: Choose Google Drive API scopes
- 참조: 『쓰기를
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34