본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 09. 12:14

어제 제출한 PR들에 4개의 AI 에이전트를 실행했습니다. 그 결과 두 개의 실제 보안 버그가 발견되었습니다.

요약

개발자가 PR 제출 후 4개의 특화된 AI 에이전트를 병렬로 실행하여 코드 감사를 수행하는 워크플로우를 소개합니다. 정리, 보안, 테스트 무결성, 운영 환경 검증 역할을 가진 에이전트들이 실제 보안 버그를 찾아낸 사례를 다룹니다.

핵심 포인트

  • 범용 에이전트보다 명확한 역할을 가진 전문가 에이전트가 효과적임
  • 보안 에이전트가 PyJWT의 iat 검증 누락 문제를 발견하여 보안 사고 예방
  • 테스트 무결성 및 실제 운영 환경 검증 에이전트를 통한 다각도 감사
  • 에이전트별로 충분한 실행 예산(15~20분)을 할당하여 정밀도 향상

매 코딩 세션이 끝날 때마다 저는 방금 배포한 diff(차이점)에 대해 4개의 에이전트가 병렬로 수행하는 감사를 실행합니다. 최근의 한 세션에서는 제가 운영하는 오픈 소스 LMS에 새로운 데일리 챌린지(daily-challenge) 기능을 추가하는 7개의 PR(Pull Request)이 반영되었습니다. 감사 결과 중 두 개는 제가 직접 검토했을 때 놓쳤던 실제 보안 또는 무결성 버그였습니다. 이것이 저의 플레이북(playbook)입니다.

4개의 에이전트

저는 감사를 네 가지의 좁은 역할로 나누었습니다. '좁은' 역할인 이유는 범용적인 에이전트는 모든 것이 괜찮다고 말하지만, 명확한 권한을 가진 전문가 에이전트는 무엇이 잘못되었는지를 말해주기 때문입니다.

  • 정리 에이전트 (Cleanup agent). 방금 수행한 작업에서 남겨진 패턴을 찾습니다: 삭제된 역할에 대한 죽은 참조(dead references), 사용되지 않는 i18n 키, 고아 테스트 픽스처(orphan test fixtures), 하나가 삭제되었어야 할 중복 이름 스크립트 등.
  • 보안 에이전트 (Security agent). 인증(Auth), 토큰(tokens), 로테이션(rotation), CSP, RLS, 코드 내의 비밀 정보(secrets), 구조를 유출하는 에러 메시지 등을 확인합니다. 모든 새로운 엔드포인트(endpoint)를 입증되기 전까지는 적대적인 것으로 간주합니다.
  • 테스트 무결성 에이전트 (Test integrity agent). 추가되거나 변경된 각 테스트를 읽고 더 어려운 질문을 던집니다: '만약 테스트 대상 코드가 잘못되었다면 이 테스트가 실패할 것인가?' 동어반복(tautologies), 삽입 순서에 따라 통과하는 테스트, 요구사항 대신 구현 사항에 맞춰진 단언(assertions) 등을 찾아냅니다.
  • 실제 운영 환경 검증 에이전트 (Live prod verification agent). 실제 운영 데이터를 가져오고, 실제 인증(auth)을 사용하여 새로운 엔드포인트에 접속하며, 변경 사항이 운영 환경에 도달했는지와 diff가 암시하는 대로 동작하는지 확인합니다.

각 에이전트는 6분간 훑어보는 것이 아니라 15~20분의 예산을 가지고 병렬로 실행됩니다. 출력물은 심각도 태그가 붙은 결과 목록이며, file:line 참조와 제안된 수정 사항을 포함합니다.

감사의 성과: 두 개의 실제 버그

심각: PyJWT의 decodeiat를 검증하지 않음

해당 diff에는 365일의 토큰 TTL(Time To Live)을 가진 iCal 구독 기능과 "토큰 로테이션(rotate token)" 버튼이 포함되어 있었습니다. 로테이션은 새로운 iat를 가진 새로운 JWT를 생성했지만 그 외의 다른 것은 아무것도 업데이트하지 않았습니다. 암묵적인 약속은 이전 토큰이 더 이상 작동하지 않을 것이라는 점이었습니다.

그렇지 않았습니다. 보안 에이전트(security agent)는 이를 단 한 줄로 지적했습니다: "pyjwt.decode는 기본적으로 iat를 검증하지 않습니다. 원래의 iat를 가진 오래된 토큰은 로테이션(rotation) 후에도 365일의 전체 TTL 동안 유효하게 유지됩니다."

# 이전 — 로테이션이 유용한 작업을 수행하지 못함
payload = jwt.decode(token, SECRET, algorithms=["HS256"])
# iat가 페이로드(payload)에 있지만 무엇과도 비교되지 않음
...

스키마 변경(Schema change): profiles 테이블에 하나의 새로운 컬럼인 calendar_ical_min_iat BIGINT NULL을 추가했습니다. 로테이션 시 새로운 토큰의 iat를 해당 하한값(floor)에 기록합니다. 검증(Verification) 단계에서는 iat가 이 하한값보다 낮은 모든 토큰을 거부합니다. 세 가지 테스트가 이 계약(contract)을 확인합니다: 로테이션이 이전 토큰을 무효화하는지, 다른 비밀키(secret)로 서명된 토큰이 실패하는지, 그리고 첫 번째 로테이션에서 하한값 읽기가 정상 작동하는지 확인합니다.

PyJWT는 기본적으로 expnbf를 검증합니다. 하지만 iat는 검증하지 않습니다. options={"verify_iat": True}를 전달하더라도 iat가 유효한 타임스탬프(timestamp)인지만 확인할 뿐, 충분히 최신인지 확인하지는 않습니다. 만약 당신의 인증(auth) 설계가 "로테이션이 그보다 오래된 모든 것을 무효화한다"라고 가정하고 있다면, 이를 직접 강제해야 합니다.

High: 삽입 순서 덕분에 통과하고 있었던 테스트

테스트 무결성 에이전트(test integrity agent)가 test_prefers_live_over_archive_attempt라는 이름의 테스트를 포착했습니다. 테스트 대상 함수는 결과값을 is_archive ASC 순으로 정렬했습니다. 테스트는 라이브 시도(live attempt)를 먼저 삽입한 다음, 아카이브 시도(archive attempt)를 삽입하고, 라이브 결과가 반환되는지 확인(assert)했습니다.

그 확인(assertion)은 잘못된 이유로 통과되었습니다. SQLite는 유효한 ORDER BY가 없는 경우 행을 삽입 순서대로 반환합니다. 만약 프로덕션 코드에서 ORDER BY가 삭제되었더라도, 해당 테스트는 계속 통과(green)했을 것입니다. 에이전트는 이를 "현재 백엔드 하에서 동어반복적(tautological)임; 오직 ORDER BY만이 테스트를 통과시킬 수 있도록 재작성할 것"이라고 지적했습니다. 저는 삽입 순서를 뒤집었습니다. 이제 테스트는 올바른 이유로 통과합니다.

이것이 저를 가장 두렵게 만든 발견이었습니다. 다른 하나는 보안 버그였지만, 적어도 그것은 문제가 터졌을 때 제가 배울 수 있는 버그였습니다. 잘못된 이유로 통과하는 테스트는 결코 배울 수 없는 버그입니다. 왜냐하면 구현부가 밑바닥에서 썩어가고 있는 동안 모든 것이 괜찮다고 말해주기 때문입니다.

왜 전문화된 에이전트(Specialist Agents)가 범용 에이전트(Generalist)를 이기는가

이 워크플로우(Workflow)의 첫 번째 버전은 "보안 이슈를 찾고, 정리 이슈를 찾고, 테스트 이슈를 찾고, 프로덕션(Prod)과 대조하여 검증하라"는 긴 프롬프트(Prompt)를 가진 하나의 에이전트를 사용했습니다. 그 결과물은 누군가 블로그 포스트에서 복사해 온 체크리스트처럼 보이는 15개의 "잠재적 우려 사항" 목록이었습니다. 그중 절반은 거짓 양성(False Positives)이었고, 심각도 순위(Severity-ranked)가 매겨진 것도 없었으며, 실제로 중요한 PyJWT 관련 사항은 9번째 항목에 파묻혀 있었습니다.

동일한 감사(Audit)를 좁은 권한과 명확한 심각도 기준(Severity Rubrics)을 가진 4개의 전문가로 나누자 결과가 바뀌었습니다. 정리(Cleanup) 에이전트는 철저해 보이기 위해 4개의 항목을 지어내는 대신, 아무것도 없을 때는 "항목 없음"이라고 보고합니다. 보안(Security) 에이전트는 두 개의 발견 사항을 가져오는데, 둘 다 실제 문제이며, 둘 다 심각도 태그가 붙어 있고, 둘 다 파일:라인(file:line) 참조를 포함하고 있습니다. 테스트 무결성(Test Integrity) 에이전트는 테스트를 작성한 사람이라면 절대 볼 수 없는 동어반복(Tautology)을 찾아냅니다. 왜냐하면 인간 작성자는 해당 테스트가 버그를 잡아낼 수 있을지 물어보기에는 정확히 잘못된 대상이기 때문입니다.

이것이 아닌 것

이것은 인간의 리뷰를 대체하는 코드 리뷰 봇(Code-review bot)이 아닙니다. 이것은 인간의 리뷰와 머지(Merge)가 완료된 후, main에 존재하는 변경 사항에 대해 실행됩니다. 이것은 디프(Diff)를 작성하는 동안 고민하는 과정을 대신하는 것이 아닙니다. 이것은 익숙함 때문에 보지 못하는 것들을, 디프가 실제로 건드리는 표면 영역에서 잡아내는 두 번째 패스(Second pass)입니다.

만약 인간의 리뷰가 30분 동안 진행되었음에도 PyJWT 문제를 놓쳤다면, 이를 잡아내는 4개 에이전트의 감사는 워크플로우에서 가장 저렴한 보안 투자입니다. 비용은 에이전트 예산(Agent budget)이며, 가치는 1년 동안 프로덕션에서 유출되었을 버그를 막는 것입니다.

저는 다시는 4개 에이전트의 사후 세션 감사(Post-session audit) 없이 프로덕션 코드를 작성하지 않을 것입니다.

Equip은 github.com/ArVaViT/equip에서 MIT 라이선스 하에 오픈 소스(Open source)로 제공됩니다. PR #626은 위의 버그들을 해결하고 방어적 테스트(Defensive tests)를 추가합니다.

GitHub logo
ArVaViT / equip

성서 학교, 사역 및 비영리 교육 프로그램을 위한 무료 오픈 소스 학습 관리 시스템 (LMS). React + FastAPI + Supabase.

Equip logo

Equip

성서 학교, 교회 사역 및 비영리 교육 프로그램을 위해 구축된 무료 오픈 소스 학습 관리 시스템 (LMS)

MIT License
Backend CI
Frontend CI
Good first issues
Code coverage
OpenSSF Scorecard

라이브 데모 (Live demo) · 로드맵 (Roadmap) · 기여하기 (Contributing) · 지원 (Support) · 변경 이력 (Changelog)

스크린샷 (Screenshots)

스크린샷 (Screenshots)

<table><tbody><tr><td width="50%"><br><a rel="noopener noreferrer" href="https://github.com/ArVaViT/equip/.github/assets/screenshots/login-desktop.png"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FArVaViT%2Fequip%2FHEAD%2F.github%2Fassets%2Fscreenshots%2Flogin-desktop.png" alt="Equip 로그인 페이지 — 왼쪽에는 성경 구절이, 오른쪽에는 깔끔한 로그인 양식이 있는 2단 레이아웃"></a><br><br>로그인 (라이트 모드)<br></td><td width="50%"><br><a rel="noopener noreferrer" href="https://github.com/ArVaViT/equip/.github/assets/screenshots/login-desktop-dark.png"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FArVaViT%2Fequip%2FHEAD%2F.github%2Fassets%2Fscreenshots%2Flogin-desktop-dark.png" alt="다크 모드의 Equip 로그인 페이지"></a><br><br>로그인 (다크 모드)<br></td></tr><tr><td width="50%"><br><a rel="noopener noreferrer" href="https://github.com/ArVaViT/equip/.github/assets/screenshots/register-desktop.png"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FArVaViT%2Fequip%2FHEAD%2F.github%2Fassets%2Fscreenshots%2Fregister-desktop.png" alt="학생 또는 교사 역할 선택 기능이 있는 Equip 계정 생성 페이지"></a><br><br>계정 생성 — 학생/교사 역할 선택기<br></td><td width="50%"><br><a rel="noopener noreferrer" href="https://github.com/ArVaViT/equip/.github/assets/screenshots/login-mobile.png"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FArVaViT%2Fequip%2FHEAD%2F.github%2Fassets%2Fscreenshots%2Flogin-mobile.png" alt="390px 모바일 뷰포트에서의 Equip 로그인 화면" width="240"></a><br><br>모바일 (390px)<br></td></tr></tbody></table>

equipbible.com에서 라이브 중입니다. 교사 및 관리자 뷰(성적부, 코스 에디터, 분석)는 로그인 후에 이용 가능합니다 — 무료 계정을 생성하여 탐색해 보세요.

이 프로젝트를 시작한 이유

전 세계 수백 개의 작은 성경 학교, 가정 교회, 선교 훈련 프로그램들이 여전히 종이, WhatsApp, 또는 스프레드시트로 코스를 관리하고 있습니다. 상용 LMS (Learning Management System) 플랫폼은 비용이 많이 들거나, 과도한 기능을 제공하거나, 자원봉사로 운영되는 조직들이 갖추지 못한 기술적 전문 지식을 요구합니다.

Equip은 이를 변화시키기 위해 설계되었습니다:

  • 영구 무료 — MIT 라이선스이며, 유료 결제 장벽이나 "프리미엄" 등급이 없습니다.
  • 간편한 배포 — 무료 Supabase 데이터베이스를 사용하여 Vercel에서 클릭 한 번으로 배포할 수 있습니다. Docker나 관리해야 할 서버가 필요 없습니다.
  • 소규모에 최적화 — ...이 아닌, 20~100명의 학생 규모에 최적화되었습니다.

GitHub에서 보기

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0