본문으로 건너뛰기

작동한다는 건 안전하다는 뜻이 아니다 — AI가 쓴 코드를 지키는 5가지 원칙

홍승아9

1. 이 글은 무엇에 관한 글인가?

IBM의 보안 아키텍트 Jeff Crume이 IBM Technology 채널에 올린 How Developers Secure AI-Generated Code: 5 Security Best Practices를 보고 정리한 글입니다. 한국어 자막본은 여기에 있습니다.

11분짜리 영상이고, 주장은 한 문장으로 줄어듭니다.

AI는 코드를 만드는 속도 는 바꿨지만, 코드를 검증하는 속도 는 바꾸지 않았다. 그 격차를 메우는 방법은 검증을 왼쪽으로 옮기는 것뿐이다.

익숙한 이야기처럼 들립니다. 시프트 레프트는 10년 넘은 단어니까요. 그런데 이 영상이 짚는 건 왜 하필 지금 그게 필수가 되었는가 입니다. 사후 보안 검토라는 방식은 원래 "사람이 쓴 코드의 양" 을 전제로 설계된 것이었습니다. 그 전제가 깨지면 방식도 같이 깨집니다.

지난 글에이전트에게 무엇을 시킬 것인가 였다면, 이 글은 그 반대편입니다 — 에이전트가 만들어낸 것을 무엇으로 붙잡을 것인가.


2. 왜 "나중에 보안 검토"가 더 이상 안 통하는가

전통적인 파이프라인에서 보안은 대체로 오른쪽 끝에 있었습니다.

기획 → 설계 → 개발 → 테스트 → [보안 검토] → 배포

이 배치가 성립했던 이유는 단순합니다. 개발이 느렸기 때문입니다. 한 스프린트에 나오는 코드의 양이 보안팀이 감당할 수 있는 양보다 적었고, 그래서 마지막에 한 번 훑는 것으로 충분했습니다.

AI 지원 개발은 이 수식의 한쪽 항만 몇 배로 키웁니다.

사람이 쓰던 시절AI가 쓰는 지금
코드 생산량스프린트당 수백~수천 줄하루에도 수천 줄
리뷰 역량같음같음
보안 검토 지점배포 직전 1회배포 직전 1회

생산만 10배가 되고 검증은 그대로면, 오른쪽 끝의 게이트는 병목이 되거나, 형식이 되거나 둘 중 하나입니다. 병목이 되면 팀이 우회하고, 형식이 되면 아무것도 못 잡습니다. 어느 쪽이든 결과는 같습니다.

그리고 여기에 비용 곡선이 겹칩니다. 결함을 늦게 발견할수록 고치는 비용이 커진다는 건 오래된 관찰인데, 이 비대칭이야말로 시프트 레프트의 원래 논거입니다. 코딩 중에 잡으면 편집기 안에서 끝나고, 운영에서 잡으면 인시던트가 됩니다.

요점은 "AI 코드가 특별히 위험하다" 가 아닙니다. 검토받지 않은 코드가 위험한 것 이고, AI는 검토받지 않은 코드를 만들기 아주 쉽게 만들어줬을 뿐입니다.


3. 원칙 1 — 과정이 아니라 결과를 검증하라

첫 번째 원칙이 가장 반직관적입니다. "AI가 어떻게 만들었는지"를 붙잡으려 하지 말라 는 것입니다.

프롬프트를 다듬고, 모델을 바꾸고, "보안을 지켜서 짜줘"를 시스템 프롬프트에 박는 건 전부 생성 과정 에 거는 베팅입니다. 확률적으로 나아질 수는 있지만, 보장이 되지는 않습니다. 같은 프롬프트가 내일 다른 코드를 뱉을 수 있고, 모델 업데이트 한 번에 조용히 달라질 수 있습니다.

그래서 붙잡아야 하는 건 산출물입니다.

❌ "이 모델은 안전한 코드를 쓴다" → 신뢰
✅ "이 코드가 안전한지 확인했다" → 검증

특히 조심해야 할 착각이 하나 있습니다.

테스트가 통과한다 ≠ 안전하다

테스트는 의도한 동작 을 검사합니다. 보안 결함은 대체로 의도하지 않은 동작 입니다. SQL 인젝션이 되는 코드도 정상 입력에는 완벽하게 잘 돌아갑니다. 권한 체크를 빠뜨린 엔드포인트도 권한 있는 사용자의 요청에는 정확히 200을 돌려줍니다. 기능 테스트 스위트가 초록이라는 사실은 그 자리에서 아무 말도 해주지 않습니다.

영상이 강조하는 검증 축은 실제 운영 환경에서의 동작과 권한 입니다. 로컬에서 되는 것과, 실제 자격증명·실제 네트워크 경계·실제 데이터 접근 권한을 가진 채로 도는 것은 다른 이야기입니다.


4. 원칙 2 — 검사를 코딩하는 자리로 끌어와라

두 번째가 시프트 레프트의 본체입니다. 보안 검사를 파이프라인의 단계가 아니라 편집 중의 피드백 으로 만드는 것.

옮기기 전옮긴 후
배포 직전 SAST 리포트저장할 때마다 정적 분석
분기별 취약점 스캔PR마다 의존성 스캔
사고 후 시크릿 로테이션커밋 훅에서 시크릿 탐지
보안팀이 티켓으로 통보작성자가 그 자리에서 빨간 줄로 확인

여기서 중요한 건 도구 목록이 아니라 누가 언제 보는가 입니다. 같은 SAST라도 2주 뒤 티켓으로 오면 컨텍스트 스위칭 비용이 붙고, 대개 "나중에"로 밀립니다. 코딩 중에 뜨면 그냥 고칩니다. 도구가 바뀐 게 아니라 비용이 바뀐 것 입니다.

에이전트를 쓰는 환경에서는 이 원칙이 한 단계 더 나갑니다. 검사를 에이전트의 루프 에 두면, 에이전트가 자기가 만든 결함을 자기 턴에 고칩니다. 훅이나 하네스로 린트·타입체크·테스트를 강제로 돌리는 구성은 이미 이 블로그에서 여러 번 다룬 이야기인데(코드 품질은 제약에 있다), 보안 스캔도 그 목록에 들어가야 할 항목이라는 게 이 절의 결론입니다.


5. 원칙 3 — AI가 끌고 들어온 의존성을 의심하라

세 번째가 실무에서 제일 자주 놓치는 지점입니다. 생성된 코드는 읽는데, 생성된 package.json은 안 읽습니다.

에이전트에게 "이미지 리사이즈 기능 붙여줘"라고 하면 코드만 오지 않습니다. 패키지가 딸려 옵니다. 그리고 그 패키지는 대개 리뷰 없이 들어옵니다 — diff에서 한 줄이고, 설치가 성공했고, 테스트가 통과했으니까요.

확인해야 할 축은 셋입니다.

질문
평판·유지보수실제로 쓰이는 패키지인가? 마지막 커밋이 언제인가? 이름이 유명 패키지와 한 글자 다르지 않은가(타이포스쿼팅)?
라이선스우리 배포 형태에서 쓸 수 있는 라이선스인가?
공급망 위험전이 의존성까지 포함해 알려진 CVE가 있는가? 설치 스크립트가 붙어 있는가?

특히 첫 번째 칸에 AI 고유의 위험이 하나 있습니다. 모델은 존재하지 않는 패키지 이름을 그럴듯하게 지어내기도 하고, 이 환각된 이름을 공격자가 선점해 실제 패키지로 올려두는 수법이 알려져 있습니다. 에이전트가 제안한 이름을 그대로 pnpm add 하는 습관은 그래서 위험합니다. 패키지 이름은 코드가 아니라 신뢰 결정입니다.

실천은 의외로 싱겁습니다 — 락파일 diff를 리뷰 대상에 포함시키고, 의존성 추가를 사람이 승인하는 지점으로 두는 것. 에이전트 스펙에 "새 의존성 추가는 먼저 물어볼 것"을 ⚠️ 등급으로 박아두는 것만으로도 대부분 걸립니다.


6. 원칙 4 — 구현이 아니라 의도를 검증하라

네 번째가 가장 사람 손이 필요한 자리입니다.

정적 분석은 코드가 잘못 쓰였는지 를 찾습니다. 찾지 못하는 건 코드가 잘못된 것을 하고 있는지 입니다. 후자는 문법의 문제가 아니라 요구사항 해석의 문제 라서, 스캐너에게는 완벽히 정상으로 보입니다.

전형적인 모양은 이렇습니다.

요구: "사용자가 자기 주문 내역을 조회할 수 있어야 한다"
 
에이전트 구현: GET /orders → 전체 주문 반환
  - 인젝션 없음 ✅
  - 인증 통과 ✅        (로그인은 확인함)
  - 인가 누락 ❌        (누구의 주문인지는 확인 안 함)
  - 테스트 통과 ✅      (본인 주문으로만 테스트했으니까)

여기서 실패한 건 코딩이 아니라 "자기"라는 단어의 해석 입니다. 에이전트는 요구사항을 문자 그대로 만족시켰고, 모든 자동 검사를 통과했습니다. 이걸 잡으려면 누군가 "이 코드가 요구사항이 의도한 범위 안에서만 움직이는가" 를 물어야 합니다.

체크할 만한 질문들:

  • 이 코드가 접근 권한 밖의 데이터 에 닿을 수 있는가?
  • 비즈니스 규칙(한도·상태 전이·소유권)이 코드에 실제로 들어가 있는가, 아니면 프론트엔드에만 있는가?
  • 에러 경로·경계값·빈 값에서도 같은 규칙이 지켜지는가?

이 절은 스펙 글의 마지막 문단과 정확히 이어집니다. 자가 검증은 에이전트가 자기 작업을 자기 스펙에 대보는 것 이라, 스펙 자체가 애매하면 전부 통과합니다. 의도 검증이 사람의 자리로 남는 이유가 그것입니다.


7. 원칙 5 — 게이트가 아니라 습관으로

마지막은 시간축입니다. 앞의 넷을 다 해도, 그게 한 번뿐이면 유효기간이 짧습니다.

개발 → 테스트 → 배포 → 모니터링 → 다시 개발
______________________________________|
        보안은 이 루프 전체에 붙어 있어야 한다

배포 시점에 깨끗했던 코드가 나중에 취약해지는 경로는 여럿입니다.

경로설명
새 CVE 공개코드는 그대로인데 의존성에 취약점이 생긴다
설정 드리프트권한·시크릿·네트워크 규칙이 운영 중에 느슨해진다
에이전트의 후속 변경다음 턴의 에이전트가 앞의 방어를 지운다

세 번째가 AI 시대의 새 항목입니다. 에이전트는 이전에 왜 그렇게 짜여 있었는지 를 모릅니다. 방어적으로 넣어둔 검증 한 줄이 "불필요한 코드"로 보이면 리팩터링하면서 지웁니다. 그래서 가드레일은 코드가 아니라 코드 밖에 있어야 합니다 — 지워지면 테스트가 빨개지거나, 훅이 막거나, 정책이 거부하는 형태로.

이 지점에서 원칙 1이 다시 돌아옵니다. 에이전트의 행동 을 믿는 대신, 에이전트가 넘지 못하는 경계 를 만드는 것. 프롬프트는 부탁이고, 훅과 테스트와 권한은 제약입니다.


8. 이 저장소에 대입해보면

이 블로그 저장소 기준으로 다섯 원칙이 어디까지 와 있는지 세어봤습니다.

원칙현재 상태
1. 결과 검증부분적 — pnpm test·pnpm e2e가 있고 axe 접근성 검사까지 돌지만, 전부 기능 오라클이다
2. 시프트 레프트있음 — husky pre-commit이 lint → vitest → playwright를 커밋 전에 강제한다
3. 의존성 검증없음 — 자동 감사가 없다. 최근 메이저 업그레이드도 사람이 판단했다
4. 의도 검증사람 — 콘텐츠 규약 테스트가 일부를 맡지만(카테고리 표기·번역 1:1), 나머지는 리뷰
5. 지속 검증부분적 — deploy.yml이 푸시마다 전부 돌리지만, 코드가 안 바뀌면 아무것도 안 돈다

정직하게 보면 3번이 비어 있고, 5번이 이벤트 기반이라 새 CVE를 못 잡습니다. 정적 블로그라 공격 표면이 작긴 하지만, 빈 칸은 빈 칸입니다. 값싼 보강 두 개는 이렇습니다.

# ① pre-commit이나 CI에 감사 한 줄 — 원칙 3
pnpm audit --audit-level=high
 
# ② 코드 변경이 없어도 도는 주기 검사 — 원칙 5
#    refresh-popular.yml처럼 schedule 트리거를 쓰면 된다
on:
  schedule:
    - cron: '0 0 * * 1'

이미 인기글 스냅샷을 갱신하는 크론(refresh-popular.yml)이 매일 도는 저장소라 주기 실행의 배관은 이미 깔려 있습니다. 5번이 비어 있는 건 어려워서가 아니라, 그냥 안 해서입니다. 대개 그렇습니다.


9. 정리

원칙한 줄
1과정이 아니라 결과를 검증하라잘 도는 것과 안전한 것은 다르다. 테스트 통과 ≠ 안전
2검사를 코딩 자리로 끌어와라도구가 아니라 발견 시점의 비용 을 바꾸는 일이다
3생성된 의존성을 의심하라코드는 읽으면서 package.json은 안 읽는다. 환각된 패키지명 주의
4구현이 아니라 의도를 검증하라스캐너가 못 잡는 건 요구사항 해석의 결함이다
5게이트가 아니라 습관으로배포 시점에 깨끗했다는 건 오늘도 깨끗하다는 뜻이 아니다

이 블로그에서 이어온 이야기와 겹쳐보면 자리가 분명해집니다.

다섯 원칙을 다시 보면 새로운 건 사실 하나도 없습니다. 시프트 레프트도, 의존성 검사도, 의도 검증도 전부 예전부터 하라고 하던 것들입니다. 달라진 건 안 해도 버틸 수 있었던 여유가 사라졌다 는 점뿐입니다. 생산 속도만 10배가 되면, 지금까지 관성으로 메우던 간극이 그대로 드러납니다.


참고 문서