Antigravity와 TDD로 안전한 AI 쇼핑 어시스턴트 만들기

ADK 쇼핑 어시스턴트를 만들고 pytest, STRIDE, Semgrep pre-commit으로 AI agent 개발 흐름을 보안 중심으로 구성한 실습 기록

Antigravity와 TDD로 안전한 AI 쇼핑 어시스턴트 만들기

이번 글은 Kaggle 5-Day AI Agents Intensive의 Antigravity/TDD 실습을 Windows PowerShell 환경에서 직접 따라 하며 정리한 기록이다. 목표는 단순한 데모 앱을 만드는 것이 아니라, AI agent 개발 과정에 테스트와 보안 게이트를 처음부터 끼워 넣는 흐름을 경험하는 것이다.

실습에서는 Google ADK 기반의 쇼핑 어시스턴트를 만들었다. 사용자는 할인 코드 WELCOME50이나 SUMMER20을 입력하고, agent는 등록된 사용자에게만 코드를 1회 redeem해준다. 여기까지는 평범한 기능 구현처럼 보인다. 하지만 이번 실습의 핵심은 그다음이다. 의도적으로 하드코딩된 mock API key를 넣고, Semgrep과 pre-commit hook이 커밋을 막는지 확인한 뒤, 환경변수 기반 코드로 고쳐 다시 커밋한다.

원문 실습 과정에서는 캡처가 많았지만, 블로그 글에서는 중복되는 터미널 화면을 줄였다. 대신 각 단계의 의미가 달라지는 장면만 남겼다. scaffold 성공, 보안 규칙 작성, Semgrep 실패, pytest 통과, 커밋 차단, remediation, playground 실행 같은 장면이다.

1. 도구와 워크스페이스 준비

먼저 uv, Python, Git, agents-cli가 설치되어 있는지 확인했다. 이 실습은 로컬 개발 guardrail을 만드는 것이 목적이라 GitHub 연결이나 Google Cloud 배포는 필요하지 않다. 다만 마지막 playground에서 Gemini API를 호출하려면 Google AI Studio API key가 필요하다.

무제 1-1781779632004

실습용 폴더는 기존 프로젝트와 분리했다. 나는 C:\Users\sound\secure-agent-lab-practice를 만들고, 그 안에 Git 저장소와 Python 가상환경을 초기화했다. 이렇게 분리해두면 실습 중 pre-commit hook이나 Semgrep rule을 자유롭게 실험해도 기존 작업물에 영향을 주지 않는다.

1
2
3
4
5
6
7
8
9
10
11
cd $HOME

$lab = "$HOME\secure-agent-lab-practice"
New-Item -ItemType Directory -Force -Path $lab | Out-Null

git init $lab
git -C $lab config user.name "Kaggle Student"
git -C $lab config user.email "student@example.com"

uv venv "$lab\.venv"
cd $lab

2. ADK 프로젝트 scaffold

agents-cli로 ADK quickstart 프로젝트를 생성했다.

1
2
3
agents-cli scaffold create shopping-assistant --adk --yes
cd .\shopping-assistant
agents-cli install

정상적으로 생성되면 app, tests, deployment, pyproject.toml, uv.lock 같은 기본 구조가 만들어진다. scaffold 직후의 agent는 weather/time 예제에 가까우므로, 이후 단계에서 쇼핑 어시스턴트로 교체한다.

무제 1-1781779907086

프로젝트가 agents-cli에서 인식되는지도 확인했다. 여기서 Project root, Project name, Agent directory가 보이면 기본 scaffold는 성공이다.

3. 할인 코드 redeem 도구 만들기

기본 agent를 쇼핑 어시스턴트로 바꿨다. 핵심 도구는 redeem_discount(code, user_id)다.

이 도구는 네 가지 규칙을 가진다.

  • 할인 코드는 WELCOME50, SUMMER20만 허용한다.
  • 한 번 redeem된 코드는 다시 사용할 수 없다.
  • guest_ 계정과 등록되지 않은 사용자는 거부한다.
  • 입력값은 Pydantic schema로 정규화하고 검증한다.

처음에는 실습을 위해 하드코딩된 mock API key를 일부러 넣었다. 실제 서비스에서는 절대 하면 안 되는 일이지만, 이번 실습에서는 보안 게이트가 이것을 잡아내는 장면을 만들기 위한 장치다.

무제 1-1781780278400

간단한 함수 호출로 tool 동작도 확인했다.

1
uv run python -c "from app.agent import redeem_discount; print(redeem_discount('WELCOME50', 'user_123'))"

이 단계에서 중요한 점은 LLM prompt만 믿지 않는다는 것이다. 할인 코드 사용 가능 여부, 등록 사용자 여부, 1회성 사용 여부는 prompt가 아니라 tool 내부 로직으로 강제했다.

4. 프로젝트 보안 규칙을 CONTEXT.md로 고정

다음으로 .agents/CONTEXT.md를 만들었다. 이 파일은 agent가 프로젝트 안에서 따라야 할 개발 규칙이다.

핵심 규칙은 다음과 같다.

  • 모든 tool 입력은 Pydantic schema로 검증한다.
  • raw shell 실행은 hook 승인 없이는 금지한다.
  • pre-commit 실패는 무시하지 않고 remediation loop로 처리한다.
  • Plan phase에는 Security Boundaries & Assertions를 포함한다.

무제 1-1781780416975

이 파일은 초보자에게도 꽤 중요한 개념이다. AI coding agent에게 “보안 신경 써줘”라고 매번 말하는 대신, 프로젝트 안에 반복 가능한 기준을 남기는 것이다. 사람이 놓쳐도 agent가 계속 참고할 수 있는 paved road를 만드는 셈이다.

5. Semgrep과 pre-commit으로 secret 커밋 차단

이번 실습의 가장 중요한 guardrail은 pre-commit hook이다. 커밋 전에 자동으로 검사해서 위험한 코드가 저장소에 들어가지 못하게 만든다.

먼저 개발 의존성에 pre-commit, pre-commit-hooks, semgrep를 추가했다.

1
uv add --dev pre-commit pre-commit-hooks semgrep

그다음 Google API key prefix를 잡는 Semgrep rule을 만들었다.

1
2
3
4
5
6
7
rules:
- id: detect-hardcoded-google-api-key
pattern-regex: 'AIzaSy[A-Za-z0-9_\-]*'
message: "Security Issue: Hardcoded Google API key prefix detected."
languages:
- python
severity: ERROR

pre-commit 설정에는 세 가지 hook을 넣었다.

  • end-of-file-fixer
  • trailing-whitespace
  • semgrep --error

무제 1-1781780660592

여기서 --error가 중요하다. Semgrep이 finding을 출력만 하고 exit code 0으로 끝나면 커밋은 막히지 않는다. pre-commit에서 보안 게이트로 쓰려면 finding이 있을 때 실패해야 한다.

6. 실패해야 성공인 Semgrep 검사

하드코딩된 mock key가 들어 있는 상태에서 Semgrep을 직접 실행했다.

1
uv run semgrep --error --config .\.semgrep\rules.yaml .\app\agent.py

결과는 실패였다. 그리고 이 실패가 바로 실습의 목표였다.

무제 1-1781780818271

Security Issue: Hardcoded Google API key prefix detected.가 보인다. 코드가 돌아가느냐보다 먼저, 위험한 코드가 저장소로 들어가지 못하도록 막는 장치가 작동한 것이다.

7. outcome-based pytest 작성

다음으로 redeem_discount의 보안 경계와 비즈니스 규칙을 pytest로 검증했다.

테스트는 내부 구현을 spy하거나 mock하지 않았다. 대신 outcome을 봤다.

  • 첫 redeem은 성공하는가?
  • 같은 코드를 두 번 쓰면 거부되는가?
  • 잘못된 코드는 거부되는가?
  • guest 계정과 미등록 사용자는 거부되는가?
  • 공백과 대소문자는 안전하게 처리되는가?

실행 결과는 6 passed였다.

무제 1-1781780949875

이 테스트는 “AI가 잘 대답하는지”를 검증하는 것이 아니다. agent가 호출하는 tool이 지켜야 할 안전 규칙을 코드로 고정하는 것이다. LLM이 어떤 문장을 생성하든, 할인 코드 상태를 바꾸는 최종 권한은 tool 로직에 있다.

8. STRIDE threat model로 악용 가능성 보기

보안 테스트와 별도로 STRIDE threat model도 작성했다. STRIDE는 다음 여섯 관점으로 시스템을 보는 방식이다.

  • Spoofing: 사용자가 다른 사람인 척할 수 있는가?
  • Tampering: 입력이나 상태를 조작할 수 있는가?
  • Repudiation: 나중에 “내가 안 했다”고 부인할 수 있는가?
  • Information Disclosure: 내부 정보나 token이 새어 나갈 수 있는가?
  • Denial of Service: 반복 호출로 서비스가 마비될 수 있는가?
  • Elevation of Privilege: 권한 없는 사용자가 관리자 기능에 접근할 수 있는가?

이번 실습에서는 .agents/skills/stride-threat-model/SKILL.md를 만들고, 그 결과물로 threat_model.md를 남겼다.

무제 1-1781781133081

초보자에게 STRIDE는 용어가 어렵게 느껴질 수 있다. 하지만 질문은 단순하다. “이 기능이 어떻게 악용될 수 있을까?”를 여섯 방향에서 묻는 것이다.

9. agent hook으로 위험 명령 차단

Git hook은 커밋 직전에 작동한다. 반면 agent hook은 agent가 명령을 실행하기 전 단계에서 개입한다. 이번 실습에서는 run_command를 가로채는 PreToolUse hook을 만들었다.

검증 스크립트는 정상 명령은 승인하고, rm -rf / 같은 destructive command는 차단한다.

무제 1-1781781306593

이 장면은 “로컬 hook이 완전한 보안 장벽이다”라는 뜻은 아니다. 로컬 hook은 우회될 수 있다. 다만 개발자의 기본 습관을 안전한 쪽으로 밀어주는 장치로 의미가 있다.

10. pre-commit 실패와 remediation loop

이제 실제 커밋을 시도했다.

1
2
git add .
uv run git commit -m "feat: implement shopping assistant agent"

결과는 실패였다. End of File FixerTrailing Whitespace가 먼저 파일을 고칠 수도 있고, 최종적으로는 Semgrep이 하드코딩 key 때문에 커밋을 막는다.

무제 1-1781781592652

이 실패를 무시하지 않고 remediation loop로 처리했다. app\agent.py에서 하드코딩된 key를 제거하고, 실제 key는 환경변수에서 읽도록 바꿨다.

1
2
3
os.environ.setdefault("GOOGLE_GENAI_USE_VERTEXAI", "FALSE")
if "GOOGLE_API_KEY" not in os.environ and "GEMINI_API_KEY" in os.environ:
os.environ["GOOGLE_API_KEY"] = os.environ["GEMINI_API_KEY"]

무제 1-1781781668510

수정 후 다시 테스트와 Semgrep을 실행했다. 기능은 깨지지 않았고, Semgrep finding은 0개가 됐다.

무제 1-1781781830125

11. 최종 커밋 성공

수정된 파일을 다시 stage하고 커밋을 재시도했다.

1
2
git add .
uv run git commit -m "feat: implement secure shopping assistant lab"

이번에는 세 hook이 모두 통과했다.

무제 1-1781782013650

이 장면이 이번 실습의 결론이다. “보안 검사에 걸렸다”에서 끝나는 것이 아니라, 문제를 수정하고 테스트를 다시 돌리고, 커밋이 통과하는 상태까지 가져간다.

12. playground 실행과 Windows 우회

마지막으로 로컬 playground를 실행했다. 여기서 Windows 환경에서는 두 가지 문제가 있었다.

첫째, agents-cli playground가 내부적으로 adk web .을 호출하면서 현재 폴더의 여러 파일을 잘못 인자로 넘기는 문제가 있었다. 그래서 agents-cli playground 대신 adk web을 직접 실행했다.

1
uv run adk web .\app --host 127.0.0.1 --port 8080 --reload_agents

둘째, adk web .\app으로 실행하면 ADK가 폴더명 기준으로 app 이름을 app으로 추론한다. 따라서 App(name="shopping_assistant")로 두면 session mismatch가 발생한다. 로컬 playground 실행을 위해 App(name="app")로 맞췄다.

Gemini API key 문제와 일시적인 503 high demand도 겪었다. 실제 API key를 다시 설정하고, 모델을 더 가벼운 gemini-2.5-flash-lite로 바꿔 재시도했다.

최종적으로 playground에서 할인 코드 redeem 요청까지 성공했다.

무제 1-1781782977006

이 실습이 의미하는 것

이번 실습은 단순히 “AI 쇼핑 어시스턴트를 하나 만들었다”는 이야기가 아니다. 더 중요한 것은 AI agent를 만들 때 코드 생성, 테스트, 보안 검사, 수정, 커밋까지 이어지는 전체 개발 흐름을 직접 경험했다는 점이다.

초보자 입장에서 AI agent는 “프롬프트를 넣으면 알아서 답하는 프로그램”처럼 보일 수 있다. 하지만 실제 서비스에 가까워질수록 agent는 단순한 채팅봇이 아니다. 사용자의 요청을 받고, 도구를 실행하고, 상태를 바꾸고, 외부 API를 호출한다. 이번 실습의 할인 코드 redeem 기능처럼 말이다. 이런 기능이 들어가는 순간 보안과 테스트는 선택 사항이 아니라 기본 조건이 된다.

가장 중요한 장면은 오히려 실패였다. 하드코딩된 API key를 일부러 코드에 넣고, Semgrep과 pre-commit hook이 그것을 막도록 만들었다. 즉, 실수하지 않는 개발자를 기대한 것이 아니라, 실수해도 시스템이 잡아주는 구조를 만든 것이다. 이것이 shift left security의 핵심이다. 보안을 배포 직전이나 사고 이후에 확인하는 것이 아니라, 코드를 작성하고 커밋하는 아주 이른 단계에서 확인하는 방식이다.

pytest도 같은 역할을 한다. 유효하지 않은 코드는 거부되는지, 한 번 쓴 코드는 다시 쓸 수 없는지, guest 사용자는 막히는지 확인했다. 이것은 테스트를 많이 쓰는 연습이 아니라, “agent가 어떤 행동을 하면 안 되는지”를 코드로 명확히 선언하는 연습이다. AI가 중간에 어떤 판단을 하든, 중요한 비즈니스 규칙은 테스트와 도구 로직이 지켜야 한다.

STRIDE threat model은 “내 코드가 돌아간다”에서 한 단계 더 나아가 “이 코드가 어떻게 악용될 수 있을까”를 묻는 훈련이었다. 누가 속일 수 있는가, 누가 데이터를 바꿀 수 있는가, 누가 책임을 부인할 수 있는가, 무엇이 새어 나갈 수 있는가, 무엇이 서비스를 멈추게 할 수 있는가, 누가 권한을 우회할 수 있는가를 점검했다.

결국 이번 실습은 “AI로 코드를 빠르게 만드는 법”이 아니라, “AI와 함께 안전하게 개발하는 법”에 대한 연습이었다. AI agent 시대의 개발자는 코드를 직접 쓰는 능력뿐 아니라, agent가 따라야 할 규칙을 만들고, 잘못된 결과를 자동으로 걸러내고, 실패를 개선 루프로 연결하는 능력이 필요하다. 이번 쇼핑 어시스턴트 실습은 그 전체 흐름을 작게 압축해서 경험해본 과정이었다.

Comments

댓글

GitHub 계정으로 의견을 남길 수 있습니다. 댓글은 GitHub Discussions에 저장됩니다.