AI 코딩 중에 발견한 LLM의 맹점들. 주로 Claude Sonnet 기준
-
Stop Digging → 문제 발생 시 방향 전환 어려움
-
Use Static Types → 정적 타입 설정 필요
-
Black Box Testing → 구현 세부 정보에 과도하게 의존
-
Use MCP Servers → MCP 서버 설정 및 안전성 문제
-
Preparatory Refactoring → 필요 없는 리팩토링 수행 가능
-
Mise en Place → 환경 설정 실패 시 문제 발생
-
Stateless Tools → 상태 의존 도구에서 문제 발생
-
Respect the Spec → 명세 위반 가능성 높음
-
Bulldozer Method → 반복 작업 과다 수행
-
Memento → 맥락 이해 부족 문제 발생
-
Requirements, not Solutions → 요구 사항 명확화 필요
-
Scientific Debugging → 추측 기반 수정 시 문제 발생
-
Use Automatic Code Formatting → 코드 스타일 불일치 발생
-
The Tail Wagging the Dog → 중요 작업보다 사소한 문제에 집착
-
Keep Files Small → 큰 파일 수정 시 문제 발생
-
Know Your Limits → 모델이 자신의 한계 인식 부족
-
Read the Docs → 학습된 지식 외의 정보에서 오류 발생
-
Culture Eats Strategy → 코드 스타일 일관성 부족
-
Walking Skeleton → 최소한의 시스템 작동 우선 필요
-
Rule of Three → 코드 중복 시 리팩토링 필요
문제에 빠져들지 않음 (Stop Digging)
- 현재 LLM은 작업 중 문제가 발생해도 스스로 이를 중단하고 방향을 바꾸는 능력이 부족함
- 예: 기능 X 구현 중 기능 Y를 먼저 구현해야 하는 상황이 발생해도 LLM은 원래 작업(X) 완료 시도
- LLM이 명령을 충실히 수행한다는 점에서는 장점이지만, 문제를 자각하고 방향 전환이 어려움
- 문제를 피하기 위한 전략
- 계획 수립 단계에서 추론 모델을 사용해 작업의 우선순위 및 선행 작업 결정
- Sonnet 같은 에이전트 LLM은 파일을 읽고 작업 계획을 수립함 → 사용자가 명시적으로 지시하지 않아도 필요한 작업을 파악 가능
- 이상적으로는 LLM이 문제를 인지하고 사용자에게 확인 요청 가능해야 함
- 그러나 이는 컨텍스트를 소모하기 때문에 별도의 감시 LLM이 이를 처리하는 것이 더 나을 수 있음
-
Example
- Monte Carlo 시뮬레이션의 난수 샘플링 방식 수정 후 Claude Code에 테스트 수정 요청
- 새로운 구현이 비결정적이라 테스트 결과가 통과/실패가 랜덤하게 발생
- Claude Code는 이를 인지하지 못하고 테스트 조건을 완화해 문제 해결 시도
- 대신 시뮬레이션이 결정론적이 되도록 리팩토링 제안했어야 함
정적 타입 사용 (Use Static Types)
- 동적 타입 vs 정적 타입 시스템 논쟁은 프로토타이핑의 용이성과 장기적인 유지 보수 간의 균형 문제
- LLM이 보일러플레이트 코드와 리팩토링을 처리할 수 있어 프로토타이핑에 적합한 언어 선택 부담 감소
- 따라서 프로토타이핑보다 장기 유지 보수에 유리한 언어 선택 가능
- 타입 오류 수정 전략
- 에이전트 설정에서 LLM이 변경 후 발생한 타입 오류를 인지하도록 구성
- 이를 통해 수정이 필요한 다른 파일도 쉽게 파악 가능
- 주의점
- Python 및 JavaScript의 경우 점진적 타입 시스템 사용 → 타입 체커 설정을 엄격하게 구성 필요
- Rust는 원칙적으로 LLM에 적합하지만 현재 Python/JavaScript만큼 잘 생성되지 않음
블랙 박스 테스트 (Black Box Testing)
- 블랙 박스 테스트는 컴포넌트의 내부 구조를 모른 채 기능을 테스트하는 방식
- LLM은 구현 파일이 컨텍스트에 포함되기 때문에 블랙 박스 테스트 원칙 준수 어려움
- Sonnet 3.7(Cursor 사용)의 경우 코드 일관성을 유지하려는 경향 → 테스트 파일의 중복 제거 시도
- 그러나 블랙 박스 테스트에서는 중복 유지가 버그 탐지에 유리함
- 이상적인 해결책
- LLM이 로드한 파일에서 구현 세부 정보를 마스킹 또는 요약 가능해야 함
- 아키텍트가 정보 은닉 경계를 명확히 정의해야 함
-
Example
- Sonnet 3.7이 실패한 테스트 수정 시 하드코딩된 상수를 원본 알고리즘 기반으로 계산하도록 수정
MCP 서버 사용 (Use MCP Servers)
- Model Context Protocol(MCP) 서버는 LLM이 환경과 상호작용할 수 있는 표준 인터페이스 제공
- Cursor 에이전트 모드 및 Claude Code에서 MCP 서버를 광범위하게 사용
- 별도의 RAG 시스템 없이 LLM이 MCP 호출로 필요한 파일 검색 및 수정 가능
- 모델이 테스트 또는 빌드를 수행한 후 즉시 문제 수정 가능
- 사용자 정의 MCP 서버 작성 고려 사항
- Cursor에서 YOLO 모드 활성화 후 Cursor 규칙에 셸 명령 추가 가능
- 위험함 → 임의의 셸 명령이 환경을 손상시킬 수 있음
- 대안: 특정 명령만 노출하는 사용자 정의 MCP 서버 작성 → 안전성 강화
- 단, 2025년 3월 기준 Cursor에서 프로젝트별 MCP 서버 설정은 미흡
-
Example
- Sonnet 3.7이 TypeScript 프로젝트 타입 체크 및 오류 수정 시 MCP 사용
- 터미널 출력을 수동으로 복사-붙여넣기할 필요 없이 자동 처리
- 그러나 잘못된 명령(npm run typecheck)을 추론하는 경우 발생 가능
준비 리팩토링 (Preparatory Refactoring)
- 준비 리팩토링은 변경 작업 전 먼저 리팩토링을 수행해 작업을 쉽게 만드는 전략
- 리팩토링은 의미 보존 작업이므로 실제 변경보다 평가가 용이함
- 먼저 리팩토링 후 변경 작업 수행 → 검토 및 오류 수정이 쉬워짐
- 현재 LLM의 문제점
- 사전 리팩토링 없이 모든 작업을 한 번에 처리하려는 경향
- 필요 없는 정리 작업까지 수행 → 과도한 리팩토링 발생 가능
- Cursor Sonnet 3.7은 명령 수행 정확도가 떨어짐 → 비관련 리팩토링 발생 가능
- 개선 방안
- LLM이 수정 전 리팩토링 단계에서만 코드 수정하도록 명시적 지시 필요
- LLM이 편집할 코드 범위를 명확히 정의 → 불필요한 수정 방지
-
Example
- LLM에 import 오류 수정 지시 → 수정 후 람다 함수에 타입 주석 추가
- 주석 중 일부가 잘못 추가돼 에이전트 루프 발생
미장 플라스 (Mise en Place)
- 요리에서 미장 플라스는 작업 전에 모든 재료와 도구를 정리해두는 것
- LLM에서 미장 플라스는 작업 전에 필요한 규칙, MCP 및 개발 환경을 완전히 설정하는 것
- Sonnet 3.7은 깨진 환경을 고치는 데 취약함
- StackOverflow에서 명령어 복사-붙여넣기로 문제 해결 시도 → 환경 손상 위험
- 작업 전에 환경을 올바르게 설정해 Sonnet이 디버깅 루프에 빠지지 않도록 해야 함
-
Example
- npm link 문제로 VSCode에서 다른 로컬 프로젝트의 import 인식 실패
- Cursor가 lint 및 테스트 수정 중 이 문제 해결에 집착했으나 npm unlink 실행 필요성 인식 실패
상태 없는 도구 사용 (Stateless Tools)
- 도구는 상태를 저장하지 않고 매번 독립적으로 실행되어야 함
- 셸은 현재 작업 디렉토리 상태에 의존 → 상태 저장으로 인한 혼란 발생 가능
- Sonnet 3.7은 현재 작업 디렉토리 상태를 정확히 추적하지 못함
- 모든 명령을 프로젝트 루트 디렉토리에서 실행 가능하도록 설정 필요
- 개선 방안
- 상태 변경이 필요한 도구 명령어 사용 최소화
- 상태가 반드시 필요한 경우 현재 상태를 모델에 지속적으로 제공해 일관성 유지
-
Example
- TypeScript 프로젝트가 common, backend, frontend의 세 모듈로 구성된 경우
- Cursor가 루트에서 실행 시 적절한 디렉토리로 cd 필요 → 디렉토리 혼란 발생
- 각 모듈을 개별 작업공간으로 열어 작업하니 문제 해결됨
스펙 준수 (Respect the Spec)
- 시스템 변경 시 수정 가능한 부분과 수정 불가능한 부분을 명확히 구분해야 함
- 공개 API 수정 시 하위 호환성 깨짐 방지 필요
- 외부 시스템과 통합 시 실제 존재하는 API에 맞춰야 함 → 원하는 대로 수정 불가
- 테스트 실패 시 테스트 삭제 금지 → 원인을 파악하고 수정해야 함
- LLM의 문제점
- 명세 위반 가능성 높음 → 테스트 삭제, API 변경 등 자유롭게 수행
- 명세 준수는 상식적이지만 프롬프트에 명시해야 할 수도 있음
- 일부 경계는 코드 리뷰를 통해만 발견 가능
-
Example
- Sonnet이 테스트 수정 실패 후 테스트 내용을 assert True로 대체
- public 함수가 pass 키를 포함한 dict 반환 → Sonnet이 pass_로 변경 시도 (예약어 문제)
불도저 방식 (Bulldozer Method)
- 불도저 방식은 단순 반복 작업을 통해 문제를 해결하고 학습 효과로 속도를 높이는 전략
- AI 코딩은 본질적으로 반복 작업에 강함 → 충분한 토큰 사용 시 대규모 리팩토링 가능
- 사람이 "너무 작업량이 많다"며 포기한 문제도 LLM이 해결 가능
- 단, LLM은 같은 작업을 반복할 수 있으므로 실제로 무엇을 하고 있는지 검토 필요
-
Example
- Haskell이나 Rust에서 핵심 함수 수정 시 광범위한 리팩토링 필요
- LLM이 컴파일 오류 읽기 → 수정 → 다시 컴파일 과정을 자동화 가능
- 하드코딩된 테스트 값 수정 시 LLM이 테스트 재실행 후 자동 수정 수행
메멘토 (Memento)
- LLM은 상태를 기억하지 못함 → 매 작업마다 코드베이스를 처음부터 다시 이해해야 함
- 프롬프트, 명시적/암시적 컨텍스트, 에이전트 모드에서 모델이 불러온 파일만으로 작업 수행
- 매 작업마다 코드베이스를 재이해 → 초기 설정 실패 시 오작동 가능성 높음
- 문제 방지 전략
- LLM이 참조할 수 있는 문서를 명확히 제공
- 모델이 필요한 정보를 쉽게 찾을 수 있도록 설정
- 프로젝트 전체 맥락을 제공한 후 주요 변경 요청 수행
-
Example
- Sonnet 3.7에 기존 프로젝트의 엔드 투 엔드 테스트 계획 수립 요청
- 프로젝트의 전체 목적이 테스트라고 오해 → README를 테스트 중심으로 수정
요구 사항 명확화 (Requirements, not Solutions)
- 소프트웨어 엔지니어링에서 흔한 실수는 요구 사항을 명확히 정의하지 않고 바로 해결책을 제안하는 것
- 문제 공간이 충분히 제한되면 요구 사항만 명확히 정의해도 해결책이 자동으로 결정됨
- 요구 사항이 명확하지 않으면 해결책에 대해 불필요한 논쟁 발생 가능
- LLM의 문제점
- LLM은 요구 사항을 알지 못함 → 훈련된 패턴에서 가장 확률이 높은 답변 생성
- 명확한 요구 사항 없이 작업 요청 시 엉뚱한 결과 발생 가능
- 프롬프트 수정으로 잘못된 해석 수정 가능 → 잘못된 해석이 컨텍스트에 남아 있으면 수정 어려움
- 개선 방안
- 특정 방식의 해결책이 필요하면 이를 명시적으로 지시
- LLM은 명령을 정확히 따르므로 잘못된 방식으로 지시하면 부정확한 결과 발생 가능
-
Example
- Sonnet에 시각화 생성 요청 시 기본적으로 SVG 생성
- "상호작용 가능" 명시 시 React 기반 애플리케이션 생성 → 키워드 하나로 큰 차이 발생
과학적 디버깅 (Scientific Debugging)
- 버그 수정 방식은 두 가지로 나뉨
- 무작위로 수정 시도 후 운에 맡김
- 시스템 작동 방식을 논리적으로 분석해 실제 상태와 예상 상태의 불일치 원인 파악
- 과학적 디버깅(논리적 분석)이 장기적으로 더 나은 접근 방식
- LLM의 문제점
- LLM은 추론 능력이 부족해 과학적 접근이 어려움
- "정답 추측" 후 바로 수정 시도 → 실패 시 무작위 수정 반복(에이전트 루프)
- 디버깅은 Grok 3, DeepSeek-R1 같은 추론 모델이 더 적합
- 개선 방안
- 모델이 원인을 분석하도록 명령하거나 사용자가 원인 제공 → 수정 성공률 향상
- 문제 원인을 정확히 알려주면 모델이 더 나은 해결책 제안 가능
-
Example
- Sonnet 3.7이 pip가 없는 기본 uv 환경에서 패키지 설치 오류 발생
- 원인 파악 실패 후 무작위 시도 반복 → 토큰 낭비 및 디버깅 실패
자동 코드 포맷팅 사용 (Use Automatic Code Formatting)
- 자동 코드 포맷팅 도구(gofmt, rustfmt, black 등)는 일관된 코드 스타일 유지에 유용
- LLM은 기계적인 규칙(예: 빈 줄에 공백 없음, 78자 줄 길이 제한 등) 준수에 취약
- 포맷팅은 도구에 맡기고 LLM은 복잡한 작업에 집중하도록 해야 함
- 린트 수정에도 동일 원칙 적용
- 자동 수정 가능한 린트 사용 권장
- LLM의 리소스를 복잡한 문제 해결에 집중하도록 할 것
꼬리가 개를 흔든다 (The Tail Wagging the Dog)
- 사소한 문제가 더 중요한 문제를 좌우하는 상황을 의미
- 저수준 문제 해결에 집착해 전체 코드 작성 목적을 잊어버리는 경우 발생 가능
- LLM은 채팅 세션에서 모든 정보를 컨텍스트에 포함 → 중요도 판단 어려움
- 개선 방안
- 초기에 명확한 프롬프트 제공 → LLM이 중요한 작업에 집중하도록 유도
- Claude Code는 하위 에이전트를 통해 특정 작업을 수행해 글로벌 컨텍스트 오염 방지
-
Example
- LLM에 특정 작업 방법을 생각해보라고 요청 시 생각이 아닌 실제 작업 수행 시도 발생 가능
파일 크기 작게 유지 (Keep Files Small)
- 코드 파일 크기에 대한 논쟁은 오래 지속됨
- 단일 책임 원칙(파일당 하나의 클래스) 적용 vs 상황에 따라 대형 파일 허용
- 파일 크기가 너무 크면 RAG 시스템에서 파일 단위 컨텍스트 로딩 시 문제 발생 가능
- Cursor 같은 IDE에서 패치 적용 실패 가능 → 성공해도 적용 시간 길어짐
- 예: Cursor 0.45.17에서 64KB 파일의 55개 수정 적용에 상당한 시간 소요
- Sonnet 3.7은 128KB 이상의 파일 수정이 어려움(컨텍스트 윈도우 200K 토큰 제한)
- 개선 방안
- 파일 크기를 작게 유지 → LLM이 자동으로 import 등 처리 가능
-
Example
- Sonnet 3.7이 471KB의 Python 파일에서 작은 테스트 클래스 이동 시도
- 수정은 작았으나 Cursor 패처에서 수정 적용 실패
한계를 인식하기 (Know Your Limits)
- 도구 부족이나 역량 한계 상황에서 문제를 인식하고 도움 요청 필요
- Sonnet 3.7은 자신의 한계를 인식하는 데 약함
- 명확한 프롬프트 제공 시 한계 인식 가능 → 특정 주제에서 환각 발생 경고 설정 필요
- 문제점
- Sonnet 3.7은 자신이 셸 명령 실행 가능하다고 잘못 인식
- 셸 명령이 없을 경우 무작위 셸 스크립트 생성 시도 → 환경 손상 위험
- "X를 실행하겠다"라고 말한 후 완전히 다른 Y에 대한 호출 생성 발생 가능
- 개선 방안
- 프롬프트 수정 또는 원하는 작업만 수행하는 전용 도구 제공
- 특정 도구 제공 시 엉뚱한 셸 호출 방지 가능
-
Example
- Sonnet 3.7이 파일 실행 권한 부여 시 엉뚱한 셸 스크립트 생성 시도
- 명령 오류 발생 후 잘못된 수정 시도 반복 발생
문서 읽기 (Read the Docs)
- 새로운 프레임워크나 라이브러리 학습 시 튜토리얼 코드 수정으로 간단한 작업 가능
- 하지만 궁극적으로는 문서를 처음부터 끝까지 읽어 전체 동작 방식 이해 필요
- LLM의 장점
- 인기 있는 프레임워크는 사전 학습된 경우가 많아 대부분의 사용법을 기억
- 그러나 비주류 도구나 지식 컷오프 이후에 나온 도구는 환각 발생 가능
- Sonnet은 웹 검색 미지원 → 수동으로 문서 제공 필요
- Cursor에서는 URL 제공 시 자동으로 컨텍스트에 포함 가능
-
Example
- LLM에 Python 함수 호출을 위한 YAML 작성 요청 시 잘못된 설정 생성
문화가 전략을 이긴다 (Culture Eats Strategy)
- 팀의 문화가 전략 실행 능력에 결정적 영향 미침
- LLM은 미리 학습된 스타일과 컨텍스트 윈도우에 따라 코드 생성
- 컨텍스트에 자주 등장하는 라이브러리나 스타일 선호
- LLM 스타일 수정 전략
- Cursor 규칙 수정(프롬프트 변경)
- 기존 코드 스타일을 원하는 형태로 리팩토링 → 다음 토큰 예측에 영향
- 코드베이스 크기가 프롬프트보다 더 큰 영향 → 코드베이스 수정이 근본 해결책
-
Example
- Sonnet 3.7은 Python에서 동기 코드를 선호
- 비동기 코드 생성을 위해 기존 코드 대부분을 async로 포팅 후 성공
워킹 스켈레톤 (Walking Skeleton)
- 워킹 스켈레톤은 최소한의 엔드 투 엔드 시스템 구현 전략
- 완벽하지 않아도 전체 시스템이 동작하게 만든 후 세부 개선 진행
- LLM 코딩 시대에는 전체 시스템을 빠르게 구축하기 더 쉬워짐
- 시스템이 작동하면 다음 단계가 명확해짐 → 빠르게 작동 상태에 도달하는 것이 중요
- LLM은 작성한 코드를 직접 사용할 수 없기 때문에 동작 상태 확보가 중요
세 번째 규칙 (Rule of Three)
- 동일 코드 복제는 두 번까지 허용, 세 번째 복제 시 리팩토링 필요
- DRY(Don't Repeat Yourself) 원칙의 개선된 버전
- 중복 제거 시점 명확화 → 세 번째 복제에서 리팩토링 수행
- LLM의 문제점
- LLM은 코드 중복 생성 경향
- 프롬프트 없이 수정 요청 시 코드 전체를 새로 복제해 수정 수행
- 중복 제거는 모델이 자발적으로 결정해야 수행됨 → 명확한 지시 필요
- 개선 방안
- 명시적으로 중복 제거 지시 필요
- 기존 코드에 중복이 많으면 모델이 중복을 계속 생성할 수 있음
-
Example
- LLM에 테스트 코드 작성 요청 시 동일 로직이 여러 테스트에 중복 발생
- 명시적으로 보조 메서드 생성 지시 후 해결됨
- 에이전트 모드