
잘 짜던 AI가 왜 갑자기 헛소리를 시작하지?
Claude Code, Cursor, GitHub Copilot 같은 AI 코딩 도구를 써 보신 분이라면 한 번쯤 이런 경험이 있을 거예요. 처음에 "이 API에 인증 미들웨어 붙이고, 요청 본문 검증하고, 에러는 표준 포맷으로 반환해줘"라고 요청하면 꽤 그럴듯하게 만들어줍니다. 그런데 대화가 길어지면서 "여기 캐시 추가해줘", "이 부분 트랜잭션 처리 좀", "로깅 일관되게" 같은 요구가 쌓이다 보면, 어느 순간부터 처음에 분명히 정해뒀던 규칙들이 슬슬 무너지기 시작해요. 인증 미들웨어를 빼먹는다든지, 에러 포맷이 바뀐다든지, 처음에 쓰던 ORM 대신 갑자기 raw SQL을 쓴다든지요.
이번에 arXiv에 올라온 한 논문이 이 현상에 "Constraint Decay(제약 붕괴)" 라는 이름을 붙였습니다. LLM 에이전트가 백엔드 코드를 생성할 때 시간이 흐르거나 작업이 누적될수록 처음에 주어진 제약 조건을 지키는 능력이 점점 떨어진다는 거예요. 단순한 "환각(hallucination)"과는 다른, 더 구조적인 실패 패턴입니다.
제약은 어떻게 무너지는가
논문이 분석한 실패 패턴은 크게 몇 가지로 나뉩니다.
첫 번째는 암묵적 제약의 망각이에요. "이 프로젝트는 PostgreSQL을 쓴다"는 정보를 첫 메시지에서 줬다고 칠게요. 그러면 처음 몇 번의 코드 생성에서는 잘 반영되지만, 30번째 작업쯤 가면 갑자기 MongoDB 문법이 섞여 들어옵니다. 컨텍스트 윈도우 안에 정보는 분명히 있는데도요. 사람으로 치면 "읽긴 읽었는데 머리에 안 남는" 상태가 되는 거예요.
두 번째는 암묵적 vs 명시적 제약의 비대칭입니다. "보안상 SQL 인젝션을 막아야 한다"는 건 어떤 백엔드 프로젝트에서도 당연한 제약이에요. 하지만 사용자가 매번 "prepared statement 써"라고 명시하지 않으면, 에이전트는 작업이 복잡해질수록 문자열 연결로 쿼리를 짜는 위험한 코드를 만들어내곤 합니다. 명시적으로 매번 적어준 제약은 비교적 잘 지켜지지만, "굳이 말 안 해도 알 만한" 암묵적 제약일수록 더 빨리 무너져요.
세 번째는 제약 사이의 충돌 처리 실패입니다. "빠르게 응답해야 한다"와 "정확해야 한다"가 충돌할 때, 사람은 둘 사이의 트레이드오프를 의식하면서 결정합니다. 그런데 에이전트는 둘 중 하나를 슬그머니 포기해버리고, 그 사실을 사용자에게 알려주지도 않아요. 결과적으로 "왜 캐시가 갱신이 안 되지?" 같은 디버깅 지옥이 시작됩니다.
왜 백엔드에서 특히 심한가
프론트엔드 코드는 보통 결과가 눈에 보입니다. 버튼 색이 이상하면 바로 알 수 있고, 레이아웃이 깨지면 새로고침만 해도 보이죠. 그래서 LLM이 실수해도 빠르게 잡아낼 수 있어요.
반면 백엔드는 다릅니다. 제약 조건 자체가 보이지 않는 곳에 잠복해 있거든요. 트랜잭션이 제대로 안 걸려도 평소엔 잘 돌아가다가 동시 요청이 몰리는 순간 데이터가 깨지고, 인덱스를 안 타는 쿼리는 데이터가 적을 땐 멀쩡하다가 운영 환경에서 폭발합니다. LLM이 제약을 흘려도 단위 테스트는 통과하고 로컬에서는 잘 돌아가요. 그러다 운영에서 터지는 거죠.
또 하나, 백엔드는 상태(state) 가 핵심입니다. DB 스키마, 캐시 정책, 인증 토큰의 생명주기, 메시지 큐의 순서 보장 같은 것들이 모두 상태와 제약의 조합이에요. LLM은 본질적으로 상태가 없는(stateless) 시스템 위에서 컨텍스트라는 임시 메모리에 기대고 있어서, 이런 상태 기반 제약을 다루는 데 구조적으로 약합니다.
업계가 시도 중인 해법들
이 문제에 대한 대응은 크게 세 갈래로 나뉩니다.
하나는 에이전트에 외부 메모리/지식 베이스를 붙이는 방식이에요. AGENTS.md, CLAUDE.md, Cursor Rules 같은 파일에 "이 프로젝트의 제약 조건"을 적어두면 매 호출마다 다시 주입되는 구조입니다. 효과는 있지만 파일이 너무 길어지면 또 다른 망각이 시작돼요.
둘째는 검증기(validator)를 별도로 두는 접근입니다. 코드 생성 결과를 곧장 받아들이지 않고, 정적 분석이나 별도 LLM 호출로 "제약 위반 여부"를 점검하는 거예요. 최근 Anthropic이 강조하는 "sub-agent" 패턴이 이쪽에 가깝습니다.
셋째는 타입 시스템과 스키마로 제약을 코드화하는 방법이에요. TypeScript의 엄격한 타입, Zod 같은 런타임 검증, Prisma 같은 타입 안전 ORM을 활용하면, LLM이 제약을 잊어도 컴파일러가 잡아주거든요. "AI가 잊을 만한 건 도구가 강제하게 한다"는 발상입니다.
한국 개발자에게 주는 시사점
바이브 코딩이 일상이 된 지금, 이 논문이 주는 실용적 교훈은 분명합니다.
첫째, 중요한 제약은 코드 바깥이 아니라 코드 안에 박아 넣으세요. 주석으로 "여기 트랜잭션 필요"라고 적어두는 것보다, 함수 시그니처에 트랜잭션 객체를 필수 파라미터로 받게 만드는 게 훨씬 강력합니다. 타입 시스템과 린터를 "AI의 망각을 막는 가드레일"로 적극 활용하세요.
둘째, 긴 대화보다 짧은 대화 + 명확한 문서가 낫습니다. 한 세션에서 끝없이 요청을 쌓지 말고, 작업 단위를 끊고 매번 핵심 제약을 다시 정리한 프롬프트로 시작하는 게 제약 붕괴를 늦춥니다.
셋째, AI가 작성한 백엔드 코드는 사람보다 더 꼼꼼히 리뷰해야 합니다. 특히 트랜잭션, 인증/인가, 입력 검증, 동시성 처리 같은 "조용히 무너지는" 영역은 더더욱요. 운영에서 터지면 회복이 어렵습니다.
마무리
LLM은 시간이 갈수록 똑똑해지지만, 한 세션 안에서는 오히려 시간이 갈수록 흐려집니다. 우리의 일은 그 흐려짐을 견디는 시스템을 만드는 거예요.
여러분은 AI 코딩 도구를 쓰면서 "분명히 처음엔 잘했는데 갑자기 이상해진다" 같은 경험이 있으셨나요? 어떻게 대응하고 계신가요?
🔗 출처: Hacker News
"비전공 직장인인데 반년 만에 수익 파이프라인을 여러 개 만들었습니다"
실제 수강생 후기- 비전공자도 6개월이면 첫 수익
- 20년 경력 개발자 직강
- 자동화 프로그램 + 소스코드 제공