
AI 협업 안티패턴 모음 ── 50건 이상의 트러블에서 추출한 9가지 함정
요약
50건 이상의 실제 트러블 사례를 바탕으로 AI 협업 시 반복되는 9가지 안티패턴을 정리한 가이드입니다. AI가 범하기 쉬운 기술적 실수와 이를 예방하기 위한 프롬프트 및 검증 메커니즘을 제시합니다.
핵심 포인트
- AI 협업 시 발생하는 9가지 주요 안티패턴(AP-001~009) 정의
- 스키마 불일치 등 AI가 놓치기 쉬운 기술적 함정 분석
- 프롬프트 템플릿 및 사후 검지 메커니즘을 통한 예방 전략
- 테스트 환경(H2)과 운영 환경(MySQL) 간의 차이로 인한 오류 주의
서론
지난 기사 [AI 협업에서 「이해보다 판단」에 치우쳤을 때, 품질을 어떻게 유지할 것인가]에서, AI 협업 개발에 있어 「판단 8 : 이해 2」로 치우쳤을 때의 품질 보증 메커니즘을 소개했습니다. 그 핵심에 있는 것이 바로 본 기사에서 다루는 AI 협업 안티패턴 모음 (AP-001~009) 입니다.
50건 이상의 실제 트러블을 기록한 docs/troubles/로부터, AI에게 맡기면 반복해서 빠지기 쉬운 함정을 9가지로 승화시켰습니다. 하나하나가 특정 트러블 번호와 연결된 출처가 명시된 안티패턴 입니다.
본 기사는 그것을 「하나하나」 소개하는 카탈로그입니다. 같은 구덩이에 빠졌던 사람, 앞으로 AI 협업을 시작하려는 사람을 위해 지뢰밭의 지도(Map)로서 사용해 주시기 바랍니다.
전체상 (일람표)
| 번호 | 함정 | 출처 트러블 |
|---|---|---|
| AP-001 | 기존 스키마를 읽지 않고 신규 DDL을 추가 | 044, 045, 049 |
| ... | ||
| 「분류하여 나열하는」 효과는 생각보다 컸으며, 새로운 트러블이 발생했을 때 「이것은 AP-006 계열의 파생인가, 아니면 신규 패턴인가」라고 즉시 어휘화할 수 있게 됩니다. AI에게 전달할 때도 「이번 작업은 AP-001을 밟기 쉽다」라고 미리 언급할 수 있으므로, 계획 단계에서의 정밀도가 달라집니다. |
그럼 하나씩 살펴보겠습니다.
AP-001: 기존 스키마를 읽지 않고 신규 DDL을 추가
증상: Core 기동 시의 schema.sql 실행 시, 기존의 users.id (BIGINT UNSIGNED)와 신규 테이블의 FK 열 (signed BIGINT)의 타입 불일치로 인해 operation_logs 테이블 생성이 실패. continue-on-error=true로 인해 WARN으로 묵인되었고, 운영 MySQL에 해당 테이블이 존재하지 않는 상태로 가동되어 API 호출 시 500 에러가 연발되었습니다.
왜 AI가 밟기 쉬운가: H2 테스트 환경에서는 UNSIGNED 개념이 없기 때문에, FK 호환성 체크를 통과해 버립니다. AI는 H2에서 테스트가 통과되면 「OK」라고 판단하기 쉬우며, 운영 환경 고유의 타입 불일치는 CI에서도 CD에서도 검지할 수 없습니다. 게다가 continue-on-error가 DDL 실패를 WARN으로 격하시키기 때문에, 기동 로그를 자세히 보지 않는 한 알아차릴 수 없습니다.
예방 레버: 신규 테이블 추가 시 「참조 대상이 되는 기존 테이블(특히 users.id)의 타입을 반드시 확인한다」는 내용을 프롬프트 템플릿 (TPL-001)에 포함했습니다. 또한 사후 검지로써 ops/healthcheck/required_tables.txt와 운영 MySQL의 information_schema.TABLES를 대조하는 메커니즘을 CD에 추가했습니다. 「DDL 실패를 WARN으로 뭉개지 않는다」가 바람직한 모습이지만, 운영 가드(사후 검지)도 이중으로 설치함으로써 인간의 주의력에 의존하지 않는 구조로 만들었습니다.
AP-002: Entity 제약을 읽지 않고 테스트 헬퍼를 하드코딩
증상: ProductSku의 @UniqueConstraint(product_id, color, size)를 확인하지 않고, 테스트 헬퍼에서 color/size를 하드코딩된 값으로 고정. 동일한 product에 여러 SKU를 생성하는 테스트에서 일제히 제약 위반이 발생하여 관련 테스트가 전멸했습니다.
왜 AI가 밟기 쉬운가: 테스트 헬퍼는 「돌아가기만 하면 된다」고 생각되기 쉬우며, Entity의 제약 어노테이션 (Annotation)까지 읽지 않고 작성되는 경우가 많습니다. 게다가 H2가 JPA Entity 정의로부터 UNIQUE 제약을 자동 생성하기 때문에, 수동으로 한 번 테스트를 작성한 시점에는 문제가 나타나지 않습니다. 다른 테스트가 동일한 헬퍼를 사용하기 시작하는 순간 폭발한다 ── 영향 범위가 넓은 것도 특징입니다.
예방 레버: test_insights.md에 「@UniqueConstraint를 가진 Entity의 헬퍼는 제약 컬럼을 인자화하거나, nanoTime으로 유일화한다」는 관점을 명문화했습니다. TPL-003 (테스트 헬퍼 작성 시)에서, Entity를 읽고 제약 어노테이션을 보고하게 한 뒤 구현에 들어가도록 운영을 변경했습니다. 테스트 헬퍼는 「구현보다 제약에 얽매여 있다」고 인식을 바꾸는 것이 포인트입니다.
AP-003: 존재하지 않는 기술 스택을 가정 (Flyway 오인)
증상: Flyway를 사용하지 않는 프로젝트에서 db/migration/ 디렉토리가 존재한다는 이유만으로 AI가 "Flyway 사용 중"이라고 오인했습니다. V6~V11.sql을 작성했으나 운영 환경에서는 실행되지 않았고, 필요한 테이블 6개가 생성되지 않은 상태에서 phase14의 주문 확정 API가 500 에러를 다수 발생시켰습니다.
왜 AI가 빠지기 쉬운가: "db/migration/이 있다 = Flyway"라는 것은 일반적인 프로젝트에서는 올바른 연상입니다. AI는 이러한 종류의 **관습적 연상 (Conventional Association)**에 강합니다. 반대로 말하면 "관습에서 벗어난 구성"을 간파하는 데 약합니다. 실제 스키마 관리는 schema.sql + spring.sql.init.mode=always로 이루어지고 있었으나, AI는 pom.xml의 dependency를 확인하지 않고 디렉토리 구조의 외형만으로 판단했습니다.
예방 레버: 새로운 페이즈 착수 시 "마이그레이션 메커니즘의 확정"을 첫 번째 단계로 요구합니다. pom.xml의 dependency와 기동 로그 양쪽을 AI에게 확인시켜 현재의 메커니즘을 명문화하도록 합니다. db/migration/V*.sql에는 "운영 미실행·잔재 파일"임을 나타내는 README를 배치하여, 후속 AI/인간이 동일한 오인을 하지 않도록 했습니다. 관습적 연상을 의심하는 메커니즘이 필요합니다.
AP-004: API/SDK의 시그니처를 확인하지 않고 외삽
증상: Market의 Checkout 화면에서 Core API 응답의 getter 명(skuId)과 다른 가정된 필드(id)를 참조하여 sku_id=undefined로 인해 전환에 실패했습니다. 유사한 사례는 Console/Core 간에도 빈번하게 발생했습니다 (Guzzle CookieJar 기본값 비활성화 간과, JJWT의 키 길이 제약 간과 등).
왜 AI가 빠지기 쉬운가: AI는 "리스트 API와 상세 API는 동일한 DTO 구조를 가진다", "라이브러리는 추측한 대로의 기본 동작을 한다"와 같은 **상식적인 생략 (Common-sense Omission)**을 적용하기 쉽습니다. 평소에는 그것이 효율을 낳지만, 시스템 경계(프론트 ⇄ 백, 라이브러리 ⇄ 앱)에서는 배신당하기 쉽습니다. 게다가 이러한 종류의 실수는 수동 E2E 테스트로만 검지할 수 있는 경우가 많으며, CI의 프론트 테스트는 모크(Mock)를 사용하기 때문에 검지가 불가능합니다.
예방 레버: TPL-002(라이브러리·API 채택 시)에서 "해당 DTO 클래스의 모든 필드명 / 리스트 API와 상세 API의 필드 차이 / 라이브러리의 공식 문서 인용"을 반드시 보고하게 한 뒤 구현에 들어가도록 운영을 변경했습니다. 또한 페이즈 완료 판정에 "실제 브라우저에서의 관통 테스트"를 필수화했습니다. 경계를 넘나드는 구현은 추측이 아닌 인용으로 진행하는 것이 철칙입니다.
AP-005: 쉘 / SQL의 쿼트·이스케이프를 읽지 않고 구현
증상: CD 파이프라인의 헬스 체크 SQL이 docker exec ... sh -c '<INNER>' 내부에서 싱글 쿼트(Single Quote)가 이스케이프되지 않은 채 삽입되었습니다. 이로 인해 SQL 리터럴의 '가 바깥쪽을 닫아버려, MySQL이 amazia를 컬럼명으로 해석하여 Unknown column 에러를 발생시켰습니다. 직전의 수정(046: 호스트 측 $MYSQL_ROOT_PASSWORD 전개 누락) 직후에 동일 유형의 문제로 재실패했습니다.
왜 AI가 빠지기 쉬운가: 다층 쿼트 전개(호스트 bash → 컨테이너 sh → SQL 리터럴)는 AI가 어려워하는 영역입니다. 각 층에서 무엇이 전개되는지를 순서대로 쫓지 않으면 정답에 도달할 수 없는데, AI는 "겉보기에 잘 작동할 것 같은 명령어"를 내놓고 맙니다. 게다가 이러한 종류의 버그는 운영 환경의 SSM RunShellScript 고유의 동작에서만 온전히 재현되며, 로컬이나 CI에서는 통과되어 버립니다.
예방 레버: TPL-005(쉘 / SQL 문자열 구축 시)에서 "호스트 bash → 컨테이너 sh의 각 층에서 무엇이 전개되는지"를 정리하게 한 뒤 명령어를 제시하도록 하는 운영으로 변경했습니다. 또한, CD 단계에서 set -x
상당한 쉘 전개 결과 덤프를 남기고, 실제로 흐른 SQL을 로그로 확인할 수 있도록 했습니다. 고의 실패 테스트 배포 (schema.sql에서 테이블 1개를 삭제하고 exit 1을 확인하는 것)을 PR 병합 전에 필수화한 것도 효과적이었습니다.
AP-006: 대증 요법으로 본질 근원을 남기다
증상: 공개 기간 판정이 초 단위(LocalDateTime)와 날짜 단위(LocalDate JST 0:00)의 이중 기준으로 유지되어, Market에서는 '예약 가능'이지만 Checkout 확정 시에는 '공개일 미도달'로 400 에러가 발생했습니다. 020 (Cookie 도메인 덮어쓰기 누락)이나 030 (DuckDNS 단일 도메인 문제)도 유사하며, '작동한 부분만 고치고 근원은 남긴' 전형적인 사례입니다.
AI가 빠지기 쉬운 이유: AI는 **'증상 해소 = 해결'**로 간주하는 경향이 있습니다. 같은 개념의 로직이 여러 Service/Entity에 분산되어 있을 경우, 눈앞의 불具合만 고치고 '수정 완료'라고 보고해 버립니다. 가로 전개 확인을 하지 않기 때문에, 동근(同根) 버그가 시간차를 두고 다른 곳에서 터져 나옵니다. 이것이 개인 개발일 경우, 트러블 번호만 늘어나 피로감을 느끼는 원인이 됩니다.
예방 레버: TPL-006 (버그 수정 시 가로 전개 확인)을 통해 '동 개념의 로직이 다른 파일에 없는지 반드시 grep으로 확인'하고, '수정 완료 판단 기준을 '증상 해소'가 아닌 '근원 단일화 및 가로 전개 확인 완료'로 설정'하는 것을 의무화했습니다. 게다가 수정 후에는 '고친 부분'과 '고치지 않은 부분의 근거'를 병기하여 보고하도록 운영했습니다. '고치지 않은 부분'을 명시하는 것만으로도 AI의 사고 과정이 완전히 바뀝니다.
AP-007: 설계서 기능을 부분 구현 완료로 보고하기
증상: phase10의 설계서 'SKU 가격 관리 기능'은 백엔드 API/DB/Service가 완벽하게 구현되었음에도, Vue 컴포넌트 (SkuPriceList.vue)가 플레이스홀더 상태로 남아있었습니다. 화면을 열면 '페이즈 10에서 구현 예정입니다'라는 빈 템플릿이 표시됩니다. 011 / 013 / 039도 유사하며, 페이즈 완료의 정의가 모호했던 것이 공통 근본 원인이었습니다.
AI가 빠지기 쉬운 이유: AI는 'API가 구현되었다 = 기능 완료'라고 판단하기 쉽습니다. CI가 초록색으로 변하면 페이즈 완료로 오인하고, 브라우저에서의 실제 기기 확인을 건너뜁니다. 배경에는 '설계서의 기능 전체'와 '구현층의 분업(프론트 / 백 / DB)'을 구분하는 개념이 없는 경우가 있습니다.
예방 레버: 페이즈 완료의 정의를 **'CI 녹색 + 실제 브라우저에서의 전 화면 조작 확인'**으로 명시하고, TPL-007 (페이즈 완료 확인 시)로 대응했습니다. 플레이스홀더 컴포넌트에는 TODO 주석 + 페이즈 번호를 필수화하고, 페이즈 완료 시 grep으로 잔존 여부를 검출하는 운영을 했습니다. 설계서에는 '프론트 / 백 / DB의 구현 기한' 대응표를 마련하여, 부분 구현만으로 완료 선언을 구조적으로 막았습니다.
AP-008: CI/CD 및 운영 메커니즘 검증 부족으로 부차적 트러블 유발
증상: SSM 명령어 전송이 PingStatus=Online임에도 Undeliverable이 되는 '좀비 Online' 현상이 발생했습니다. 기존 복구 메커니즘(PingStatus 체크 → stop/start)으로는 감지할 수 없었고, 여러 번 배포 실패를 겪었습니다. 공통 근본 원인은 003 (--build 제거 후 검증 누락)・023 (--remove-orphans 누락)・008 (restart 정책 누락)・025 (복구 후 Pending 체류)에서도 반복되었습니다.
AI가 빠지기 쉬운 이유: CI/CD나 운영 메커니즘은 **'무엇을 검증하고, 무엇을 보장하지 않는지'**가 암묵적으로 구현되기 쉽습니다. PingStatus=Online은 'Agent가 응답했다'는 것만으로 '명령어 전송 경로가 건전하다'를 의미하지 않는데도, AI는 'Online = OK'라고 단정하기 쉽습니다. 게다가 실제 운영 SSM의 동작은 실제 운영 환경에서만 재현할 수 있기 때문에, CI 검증에서는 빠져나갑니다.
예방 레버: TPL-008(CI/CD·운용 기구 도입 시)에서 "이 기구가 무엇을 검증하고 있으며, 무엇을 보장하지 않는지를 명문화할 것", "관련된 과거 트러블(003/008/023/025/026)을 인용할 것", "카나리아 (Canary) 전략의 유무"를 반드시 보고하도록 운영합니다. "동작했다"를 해결로 간주하지 않고, 보장 범위를 명시한 후 구현한다는 자세를 철저히 하는 것입니다. 실제 대책으로는, 경량 echo 명령어를 사전에 실행하는 카나리아 (Canary) 방식으로 실제 배포 가능 여부를 확인하도록 했습니다.
AP-009: 테스트 분리 부족 + 단발 PR로 인한 유사 클래스 간과
증상: H2 공유 DB에서 건수 어서션 (Assertion) 실패. ApplyScheduledPricesJobTest.APP_3의 단발 실패에서 시작하여, 클래스 하나씩 @Transactional을 부여하는 대증요법을 4단계 반복한 결과, 최종적으로 46개 클래스에 일괄 부여하고 6개 클래스는 설계상 revert(되돌리기) 하는 대수술로 발전했습니다.
@Transactional(REQUIRES_NEW)를 경유하는 쓰기는 롤백 (Rollback)을 관통하기 때문에, 건수 어서션을 수행하는 Repository 테스트로 자위 코드가 증식하여, "테스트를 통과하는 것"이 "결함을 검지하는 것"을 침식했습니다. 왜 AI가 빠지기 쉬운가: AP-006(대증요법)과 AP-008(검증 부족)의 복합 형태입니다. 첫 번째 케이스를 보았을 때 동일한 형태의 오염을 grep 하지 않고 대증요법의 연쇄에 빠집니다. 게다가 @SpringBootTest의 H2 공유 DB는 단독 실행 시에는 PASS, 전체 실행 시에만 실패하는 가장 알아채기 어려운 형태로 현상화됩니다.
예방 레버: test_insights.md에 "@Transactional을 붙일지 말지에 대한 판단 기준", "건수 어서션 규약", "스냅샷 차이 규약"을 명문화합니다. cleanup.sql + @Sql 방식을 규약화하고, @Transactional을 의도적으로 붙이지 않는 경우에는 클래스 Javadoc 서두에 이유를 명기하는 운영을 합니다. 나아가 주간 cron으로 mvn test -Dsurefire.runOrder=random을 실행하고, 실패 시 Issue를 자동 기표하는 메커니즘을 도입하여, 테스트 순서 의존적인 버그를 조기에 검지하도록 했습니다. 최종적으로는 Testcontainers per-class MySQL로 이행하는 판단을 내렸습니다.
증상 요법의 한계를 감지하고, 구조적 대책으로 페이즈를 전환하는 판단이야말로 인간에게 요구되는 역할이었습니다.
AP 집합을 운영하며 알게 된 것
9개를 갖춘 후 반년 정도 운영해 오면서 몇 가지 수확이 있었습니다.
1. 신규 트러블의 "분류"가 즉시 가능해졌다
결함이 발생했을 때 "이것은 AP-006의 파생이구나"라고 어휘화할 수 있습니다. Claude Code에 "AP-006의 파생으로 기록해줘"라고 전달하는 것만으로도, 대응 프롬프트 스니펫 (Prompt Snippet)도 자동으로 호출됩니다. 사고의 입구가 정형화된다는 점이 큽니다.
2. "동근(同根) 트러블"의 연쇄가 멈췄다
AP-001을 명시한 후, users.id 참조 계열의 동일 형태 트러블(045 → 049)은 사전에 grep으로 추출하여 동시에 수정하게 되었습니다. 단발 버그를 단발로 끝내지 않는다는 운영이 정착된 순간입니다.
3. AI 스스로도 AP 집합을 읽으러 오게 되었다
CLAUDE.md에 "실제 구현 착수 전에 해당 AP의 대응 프롬프트 스니펫을 확인할 것"이라고 적음으로써, Claude가 자발적으로 AP 집합을 읽어 들여 자기 체크리스트로 사용하게 되었습니다. 가드레일 (Guardrail)이 컨텍스트 (Context)에組み込まれている(組み込まれている, 포함되어 있는) 느낌입니다.
한편 과제도 있습니다. AP가 9개로 늘어난 시점부터 AI 스스로도 전모를 파악하지 못하게 되는 징후가 나타나기 시작하여, "어떤 AP가 이 작업에 해당할지"에 대한 판정이 AI의 추측에 의존하고 있습니다. 향후에는 **파일 패턴으로부터 TPL을 기계적으로 끌어낼 수 있는 판정 트리 (Decision Tree)**와 같은 결정론적인 트리거가 필요할 것으로 보입니다.
마치며
안티패턴 집합은 "실패의 기록"이 아니라 **"다음에 같은 구덩이에 빠지지 않기 위한 선제적 판단 자료"**입니다. 50건 이상의 트러블을 9개로 응축한 본 기사가, AI 협업 개발에서 같은 구덩이에 빠질 뻔한 분들에게 참고가 되기를 바랍니다.
다음 기사에서는 각 AP(Anti-Pattern)에 대응하는 프롬프트 템플릿 (Prompt Template, TPL-001~009) 운영 ── 즉, "매번 프롬프트를 생각하는 것"에서 "템플릿을 호출하는 것"으로의 전환 ── 에 대해 다루고자 합니다.
리포지토리(Repository)와 AI 컨텍스트(Context)의 전체 모습은 여기에서 확인할 수 있습니다:
Discussion

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