동작한다고 맞는 게 아니다: AI 생성 코드가 인프라에서 조용히 망가지는 방식
도입: "잘 돌아가는데요?"가 가장 위험한 말이다
운영하다 보면 진짜 무서운 장애는 에러 로그를 토하는 코드가 아니다. 에러를 내는 코드는 차라리 고맙다. 알아서 자기가 망가졌다고 소리치니까. CI에서 빨갛게 뜨고, 배포가 막히고, 슬랙에 알림이 온다. 피드백 루프가 빠르다.
진짜 무서운 건 아무 소리 없이 잘못된 일을 정확하게 수행하는 코드다. Dev.to에 올라온 Dimitris Kyrkos의 글 "Functional doesn't mean correct"가 바로 이 지점을 정확히 찌른다. 코드는 돈다. 인터페이스도 멀쩡하다. 테스트도 통과한다. 그런데 시스템이 조용히 엉뚱한 문제를 풀고 있다.
이게 지금 화제인 이유는 단순하다. AI가 코드를 뽑아내는 속도가 사람이 검증하는 속도를 압도해버렸기 때문이다. 원문 댓글에 나온 사례처럼 50만 LOC 코드베이스의 82%가 생성 코드라면, 더 이상 한 줄씩 눈으로 리뷰하는 건 불가능하다. 그런데 이 "동작하지만 틀린" 코드는 인프라/백엔드 실무자가 코드 리뷰와 파이프라인에서 매일 마주치는 현실적인 위험이다. 오늘은 이걸 왜, 언제 만나는지 그리고 어떻게 막을지 실무 관점에서 풀어본다.
핵심: 사람이 코드를 짤 때 생기는 '마찰'이 사라졌다
원문에서 가장 핵심적인 통찰은 이거다. 사람이 코드를 짜는 행위 자체가 요구사항과의 마찰(friction)을 만든다는 점.
스펙을 읽고, 생각하고, 로직으로 옮기는 과정에서 "어? 이 요구사항 말이 안 되는데?", "이 엣지 케이스는 스펙에 안 적혀 있네?", "고객이 요청한 거랑 실제로 필요한 게 다른데?" 같은 의문이 자연스럽게 떠오른다. 이 마찰이 바로 오해가 수면 위로 드러나는 지점이다.
AI는 이 단계를 통째로 건너뛴다. 프롬프트를 주면 구조적으로 그럴듯한 출력을 즉시 뱉는다. 문제는 "프롬프트와 구조적으로 일치한다"와 "실제 문제를 푼다"는 완전히 다른 것이라는 점이다.
비유하자면 이렇다. 신입한테 "할인 계산 함수 만들어줘"라고 시켰다고 치자. 눈치 빠른 신입은 "근데 도매 고객이랑 소매 고객 할인율 다른데, 둘 다 처리할까요?"라고 되묻는다. AI는 안 묻는다. 그냥 프롬프트에 적힌 대로 하나의 할인 로직을 깔끔하게 만들어서 던진다. 코드는 완벽하게 돈다. 도매 고객 결제할 때 조용히 틀린 금액을 계산할 뿐이다.
원문이 정리한 세 가지 대표 실패 패턴은 실무에서 거의 그대로 만난다:
- 요구사항을 문자 그대로 해석한다 — "검색 기능"을 요청했더니 정확히 일치하는 문자열만 찾는 함수를 만든다. 사용자는 오타 허용, 유사어 처리를 기대했는데.
- 비즈니스 규칙이 납작해진다 — 팀원 모두가 아는 예외("레거시 계정엔 grandfather 조항 적용", "지원팀 임시 권한 상승")가 코드 어디에도 안 적혀 있으니 AI는 모른다.
- 엣지 케이스가 happy path로 처리된다 — 흔한 케이스는 잘 돈다. 실제 장애를 일으키는 드문 케이스는 "크래시는 안 나지만 조용히 틀린 결과"를 낸다.
실무 관점: 인프라 코드에서 특히 무서운 이유와 가드레일 설계
왜 IaC와 쉘 스크립트에서 더 위험한가
애플리케이션 코드는 그나마 낫다. 틀리면 사용자가 항의라도 한다. 그런데 인프라 코드는 다르다. Terraform이나 쉘 스크립트가 "동작하지만 틀린" 상태로 머지되면, 그 결과가 인프라 전체에 조용히 퍼진다. 보안 그룹이 너무 열려 있어도 트래픽은 잘 흐른다. S3 버킷이 퍼블릭이어도 앱은 멀쩡히 돈다.
실제로 AI가 자주 만드는 위험한 Terraform 패턴을 보자. "보안 그룹에서 웹 트래픽 허용해줘"라고 했을 때 흔히 나오는 결과다:
# AI가 뽑은, 동작하지만 위험한 코드
resource "aws_security_group" "web" {
name = "web-sg"
ingress {
from_port = 0
to_port = 65535
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # 전 세계에 모든 포트 개방
}
}
이 코드는 terraform apply가 깔끔하게 성공한다. 웹 서비스도 잘 뜬다. 데모도 통과한다. 그런데 모든 포트가 인터넷 전체에 열려 있다. 사람이 짰다면 "80, 443만 열면 되지 왜 전체 포트를?"이라고 멈췄을 지점이다.
그래서 우리 팀은 머지 전에 정책 검사를 강제로 통과시킨다. conftest(OPA 기반)로 Terraform plan을 검사하는 예시:
# plan을 JSON으로 추출
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
# 정책 검사 실행
conftest test tfplan.json --policy policy/
policy/security.rego에 "0.0.0.0/0 + 전체 포트"를 막는 규칙을 넣어두면 출력은 이렇게 나온다:
FAIL - tfplan.json - main - Security group 'web-sg' allows ingress from 0.0.0.0/0 on all ports
1 test, 0 passed, 0 warnings, 1 failure
핵심은 이거다. AI가 코드를 짜더라도, AI가 게임할 수 없는 적대적(adversarial) 검증 레이어를 따로 두는 것. 원문 댓글의 UnitBuilds 사례도 같은 결론에 도달했다. AI가 만든 테스트는 "AI 자신의 가정을 테스트"하는 자기만족 시스템(self-gratifying system)이 되기 쉽다. 코드 작성자와 코드 파괴자를 분리해야 한다.
CI/CD 파이프라인 가드레일 구성
실무에서 쓰는 최소 구성은 이렇게 잡는다. AI가 생성했든 사람이 짰든 상관없이 모든 변경이 통과해야 하는 관문이다:
# .github/workflows/guardrails.yml (발췌)
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# 1. 정적 보안 분석 (IaC)
- name: tfsec scan
run: tfsec . --soft-fail=false
# 2. 쉘 스크립트 린트
- name: shellcheck
run: shellcheck scripts/*.sh
# 3. 정책 검사
- name: conftest
run: conftest test tfplan.json --policy policy/
여기서 중요한 건 --soft-fail=false다. 경고만 띄우고 통과시키면 가드레일이 아니라 장식이다. 막을 거면 확실히 막아야 한다.
흔한 함정: AI가 짠 쉘 스크립트의 침묵하는 실패
AI가 뽑은 배포 스크립트에서 진짜 자주 겪는 함정이다. 이런 코드를 받았다고 하자:
#!/bin/bash
cd /opt/app/releases/$VERSION
rm -rf ./cache/*
./deploy.sh
문제가 뭔지 보이는가? $VERSION이 비어 있으면 어떻게 될까. cd /opt/app/releases/로 이동하거나, cd가 실패해도 다음 줄이 그대로 실행된다. 운이 나쁘면 rm -rf ./cache/*가 엉뚱한 디렉토리에서 돈다. AI는 set -e나 변수 검증을 안 넣는 경우가 흔하다. happy path만 봤기 때문이다.
shellcheck를 돌리면 이런 경고가 그대로 나온다:
In deploy.sh line 2:
cd /opt/app/releases/$VERSION
^------^ SC2086: Double quote to prevent globbing and word splitting.
In deploy.sh line 2:
cd /opt/app/releases/$VERSION
^-- SC2164: Use 'cd ... || exit' in case cd fails.
제대로 고치면 이렇게 된다:
#!/bin/bash
set -euo pipefail # 미정의 변수, 에러 시 즉시 중단
: "${VERSION:?VERSION is required}" # 빈 값이면 여기서 멈춤
cd "/opt/app/releases/${VERSION}" || exit 1
rm -rf ./cache/*
./deploy.sh
이 차이가 운영에서 "조용히 망가지는 배포"와 "명확하게 멈추는 배포"를 가른다.
트레이드오프와 대안
물론 가드레일을 다 깔면 속도는 느려진다. 원문 표현대로 "vibe coding은 속도를, validation은 정확성을 준다. 둘은 다른 것이고 하나가 다른 하나를 대체하지 못한다."
현실적인 선택은 이거다. 코드의 영향 범위에 따라 검증 강도를 다르게 가져가는 것. 인프라/권한/결제 관련 코드는 가드레일을 빡세게, 내부 도구나 일회성 스크립트는 느슨하게. 모든 곳에 똑같은 강도를 적용하면 팀이 검증 자체를 우회하기 시작한다. 그게 가장 나쁜 결말이다.
코드 리뷰 체크리스트: AI 생성 코드를 볼 때
원문이 제안하는 검증의 핵심 질문은 "프롬프트가 아니라 실제 요구사항을 푸는가"다. 이걸 실무 체크리스트로 옮기면:
- 프롬프트 말고 진짜 요구사항을 봤는가 — AI가 푼 건 내가 적은 프롬프트지, 비즈니스가 필요로 하는 게 아닐 수 있다.
- 이 코드가 조용히 틀린 결과를 낼 수 있는가 — 크래시 말고, 에러 없이 그냥 잘못된 값을 뱉을 가능성. 이게 가장 중요하다.
- 팀의 암묵지 예외가 반영됐는가 — 코드에 안 적혀 있고 사람 머릿속에만 있는 비즈니스 규칙들.
- 테스트가 진짜 요구사항을 검증하는가, AI의 해석을 검증하는가 — 97% 커버리지가 "코드가 실행됐다"는 말이지 "올바르다"는 말은 아니다.
정리: 한 줄 요약과 누가 언제 써야 하는가
한 줄 요약: AI는 구조적으로 그럴듯한 코드를 잘 만든다. 그리고 당신이 풀 필요 없는 문제를 자신 있게 푸는 코드도 잘 만든다. 그 둘 사이의 간극이 바로 엔지니어링 판단이 사는 곳이다. AI는 그 판단의 필요성을 없앤 게 아니라, "코드가 돈다"와 "시스템이 실제로 작동한다" 사이에 남은 유일한 방어선으로 만들었다.
누가 언제 써야 하나:
- AI 코드 생성을 적극 도입한 팀이라면, 생성량이 많을수록 자동 가드레일은 선택이 아니라 필수다. 수동 리뷰로 33만 줄을 못 본다.
- 인프라/결제/권한처럼 "조용히 틀리면 치명적"인 도메인일수록 적대적 검증 레이어(정책 검사, 퍼저, 정적 분석)를 코드 작성자와 분리해서 둬야 한다.
- 반대로 일회성 스크립트나 프로토타입이라면 속도를 택하는 게 맞다. 검증 강도는 영향 범위에 비례시켜라.
마지막으로 원문이 던진 질문을 그대로 옮긴다. "당신의 팀은 AI 생성 코드가 아무 문제가 아니라 올바른 문제를 푼다는 걸 어떻게 검증하는가?" 이 질문에 자동화된 답을 가지고 있지 않다면, 지금 파이프라인부터 손볼 때다.