Back

AI 코드, 왜 자꾸 프로덕션에서 터질까? 디버깅 완전 정복

AI 코드, 왜 자꾸 프로덕션에서 터질까? 디버깅 완전 정복

다들 한 번쯤 겪어보셨죠? AI가 완벽해 보이는 코드를 뽑아냅니다. 문법도 깔끔하고, 구조도 논리적이고, 심지어 주석까지 달아줘요. 복붙해서 로컬에서 테스트하면 잘 돌아갑니다. 근데 프로덕션에 올리고 나면? 몇 시간 만에 모니터링 대시보드가 빨간불 천지가 되죠.

여러분만 그런 거 아닙니다. 최근 조사에 따르면 개발자 84%가 AI 코딩 도구를 쓰고 있는데, 그 중 46%는 AI가 뽑아낸 코드를 온전히 신뢰하지 못한다고 해요. 가장 많은 불만이 뭔지 아세요? "거의 맞는데... 뭔가 좀 아니야"—이 애매한 상태가 오히려 처음부터 짜는 것보다 디버깅을 더 힘들게 만든다는 거예요.

AI 코딩 도구를 깎아내리려는 게 아닙니다. 진짜 혁신적인 도구 맞아요. 근데 한 가지 중요한 지식이 비어있어요: AI 코드가 프로덕션에서 왜 터지는지, 그리고 터지기 전에 어떻게 잡아낼 수 있는지 말이에요. 이 글에서 그 빈틈을 채워드릴게요.

AI 코드는 왜 터지나: 근본 원인 파헤치기

디버깅 기법으로 넘어가기 전에, 먼저 AI 코드가 개발 환경에선 멀쩡한데 프로덕션에선 말썽인지 이해해야 해요. 무작위로 버그가 터지는 게 아닙니다—LLM이 동작하는 원리에서 나오는 예측 가능한 패턴이 있어요.

1. 컨텍스트 윈도우의 한계

AI 모델은 컨텍스트 윈도우가 정해져 있어요. 코드를 생성할 때 여러분의 전체 코드베이스를 다 보는 게 아니라, 일부만 볼 수 있다는 거죠. 그래서 이런 문제가 생겨요:

없는 import를 쓴다: AI가 학습 데이터에서 본 패턴을 바탕으로 "있을 거야"라고 추측한 함수나 라이브러리를 갖다 쓰는데, 정작 여러분 프로젝트엔 없는 경우가 있어요.

// "맞아 보이는" AI 코드 import { validateUserInput } from '@/utils/validation'; import { sanitizeHTML } from '@/lib/security'; async function processUserData(data) { const validated = validateUserInput(data); const safe = sanitizeHTML(validated.content); // ... }

문제가 뭐냐면요? 여러분 프로젝트는 @/utils/validation이 아니라 @/helpers/validation을 쓸 수도 있고, sanitizeHTML 함수 자체가 없을 수도 있어요. 이런 에러는 런타임 되서야 터집니다.

네이밍 컨벤션이 뒤죽박죽: AI가 학습한 여러 코드베이스의 스타일을 섞어 쓰는 경우가 많아요:

# 컨벤션이 섞인 AI 코드 def getUserData(user_id): # camelCase 함수명 user_info = fetch_user_info(user_id) # snake_case 호출 return user_info.getData() # 또 camelCase # 여러분 코드베이스는 snake_case 통일 def get_user_data(user_id): user_info = fetch_user_info(user_id) return user_info.get_data()

2. 학습 데이터가 과거에 멈춰있다

이게 제일 골치 아픈 문제예요. AI는 특정 시점의 코드로 학습됐는데, API랑 라이브러리랑 베스트 프랙티스는 계속 바뀌잖아요.

deprecated API를 쓴다: AI가 학습 기준일 이후로 deprecated된 API를 태연하게 써버리는 경우가 있어요:

// deprecated 패턴 쓰는 AI React 코드 class UserProfile extends React.Component { componentWillMount() { // React 16.3부터 deprecated this.fetchUserData(); } componentWillReceiveProps(nextProps) { // 이것도 deprecated if (nextProps.userId !== this.props.userId) { this.fetchUserData(nextProps.userId); } } } // 요즘 스타일 function UserProfile({ userId }) { useEffect(() => { fetchUserData(userId); }, [userId]); }

옛날 보안 패턴: 여기서 진짜 위험해져요. 보안 베스트 프랙티스는 빠르게 바뀌는데, AI가 이제는 뚫리는 걸로 알려진 패턴을 쓸 수 있거든요:

# 보안 취약한 AI 코드 import hashlib def hash_password(password): return hashlib.md5(password.encode()).hexdigest() # 절대 쓰면 안 됨 # 제대로 된 방법 import bcrypt def hash_password(password): return bcrypt.hashpw(password.encode(), bcrypt.gensalt())

3. 해피 패스만 안다

AI는 주로 예제 코드랑 튜토리얼로 학습하는데, 이런 건 거의 다 "잘 되는 경우"만 보여줘요. 근데 프로덕션은 안 되는 경우를 다 커버해야 하잖아요: 네트워크 끊김, 이상한 데이터, 동시 요청, 메모리 부족, 별별 엣지케이스들.

에러 핸들링이 없다:

// AI 코드: 해피 패스에선 완벽 async function fetchAndProcessData(url: string) { const response = await fetch(url); const data = await response.json(); return data.items.map(item => item.name.toUpperCase()); } // 프로덕션 현실: 다 터질 수 있음 async function fetchAndProcessData(url: string) { let response; try { response = await fetch(url, { timeout: 5000, signal: AbortSignal.timeout(5000) }); } catch (error) { if (error.name === 'TimeoutError') { throw new DataFetchError('타임아웃', { url, cause: error }); } throw new DataFetchError('네트워크 에러', { url, cause: error }); } if (!response.ok) { throw new DataFetchError(`HTTP ${response.status}`, { url, status: response.status }); } let data; try { data = await response.json(); } catch (error) { throw new DataFetchError('JSON 파싱 실패', { url, cause: error }); } if (!data?.items || !Array.isArray(data.items)) { throw new DataFetchError('응답 구조가 이상함', { url, data }); } return data.items .filter(item => item?.name != null) .map(item => String(item.name).toUpperCase()); }

null 체크를 안 한다:

// AI 코드: 데이터가 항상 완전하다고 가정 function getUserDisplayName(user) { return `${user.firstName} ${user.lastName}`; } // 프로덕션: 불완전한 데이터도 처리 function getUserDisplayName(user) { if (!user) return 'Unknown User'; const parts = [user.firstName, user.lastName].filter(Boolean); return parts.length > 0 ? parts.join(' ') : user.email || 'Unknown User'; }

4. 동시성? 그게 뭔데?

AI가 배우는 코드 대부분이 싱글 스레드 동기 코드예요. 그래서 AI 코드에는 프로덕션 트래픽 받으면 터지는 레이스 컨디션이 숨어있는 경우가 많아요.

# AI 코드: 괜찮아 보이지만 레이스 컨디션 있음 class Counter: def __init__(self): self.count = 0 def increment(self): self.count += 1 # 원자적 연산 아님! return self.count # 동시에 접근하면 터짐 # 스레드 두 개가 동시에 count=5 읽고, 둘 다 6을 씀 # 제대로 된 버전 import threading class Counter: def __init__(self): self.count = 0 self._lock = threading.Lock() def increment(self): with self._lock: self.count += 1 return self.count

JavaScript async 레이스 컨디션:

// AI 코드: 미묘한 레이스 컨디션 let cachedUser = null; async function getUser(id) { if (!cachedUser || cachedUser.id !== id) { cachedUser = await fetchUser(id); } return cachedUser; } // 다른 id로 연달아 호출하면: // 호출 1: id=1, fetch 시작 // 호출 2: id=2, fetch 시작 (cachedUser 아직 null) // 호출 2가 먼저 끝남, cachedUser = user2 // 호출 1이 끝남, user1으로 덮어씀 // 호출 2 한 쪽이 user1을 받아버림! // 고친 버전 const pendingRequests = new Map(); async function getUser(id) { if (cachedUser?.id === id) { return cachedUser; } if (pendingRequests.has(id)) { return pendingRequests.get(id); } const promise = fetchUser(id).then(user => { cachedUser = user; pendingRequests.delete(id); return user; }); pendingRequests.set(id, promise); return promise; }

AI 코드 디버깅, 이렇게 하세요

이제 왜 터지는지 알았으니, 어떻게 잡을지 알아볼게요.

전략 1: 머지 전 체크리스트

AI 코드가 메인 브랜치에 들어가기 전에 이것들 확인하세요:

import 검증:

# JS/TS 프로젝트 npx tsc --noEmit 2>&1 | grep "Cannot find module" # Python 프로젝트 python -c "import ast; ast.parse(open('file.py').read())" python -m py_compile file.py

deprecated API 체크:

// package-audit.js const fs = require('fs'); const content = fs.readFileSync(process.argv[2], 'utf8'); const deprecatedPatterns = [ { pattern: /componentWillMount/g, message: 'React deprecated 라이프사이클' }, { pattern: /componentWillReceiveProps/g, message: 'React deprecated 라이프사이클' }, { pattern: /findDOMNode/g, message: 'React deprecated API' }, { pattern: /substr\(/g, message: 'deprecated, substring() 쓰세요' }, ]; deprecatedPatterns.forEach(({ pattern, message }) => { const matches = content.match(pattern); if (matches) { console.warn(`⚠️ ${message}: ${matches.length}`); } });

에러 핸들링 체크:

# bare except 찾기 import ast import sys class ErrorHandlingChecker(ast.NodeVisitor): def __init__(self): self.issues = [] def visit_ExceptHandler(self, node): if node.type is None: self.issues.append(f"{node.lineno}번 줄: bare except") elif isinstance(node.type, ast.Name) and node.type.id == 'Exception': if not any(isinstance(n, ast.Raise) for n in ast.walk(node)): self.issues.append(f"{node.lineno}번 줄: Exception 잡고 다시 안 던짐") self.generic_visit(node) tree = ast.parse(open(sys.argv[1]).read()) checker = ErrorHandlingChecker() checker.visit(tree) for issue in checker.issues: print(issue)

전략 2: 프로덕션 상황 시뮬레이션

AI가 거의 안 다루는 프로덕션 상황을 테스트하세요:

// stress-test.js class ProductionSimulator { // 네트워크 에러 시뮬 async withNetworkFailure(fn, failureRate = 0.3) { const original = global.fetch; global.fetch = async (...args) => { if (Math.random() < failureRate) { throw new TypeError('Failed to fetch'); } return original(...args); }; try { return await fn(); } finally { global.fetch = original; } } // 느린 응답 시뮬 async withLatency(fn, minMs = 100, maxMs = 5000) { const original = global.fetch; global.fetch = async (...args) => { const delay = minMs + Math.random() * (maxMs - minMs); await new Promise(resolve => setTimeout(resolve, delay)); return original(...args); }; try { return await fn(); } finally { global.fetch = original; } } // 이상한 데이터 시뮬 async withMalformedData(fn) { const original = global.fetch; global.fetch = async (...args) => { const response = await original(...args); return { ...response, json: async () => { const data = await response.json(); return this.corruptData(data); } }; }; try { return await fn(); } finally { global.fetch = original; } } corruptData(data) { if (Array.isArray(data)) { return data.map((item, i) => i % 3 === 0 ? null : this.corruptData(item) ); } if (typeof data === 'object' && data !== null) { const keys = Object.keys(data); const corrupted = { ...data }; keys.forEach(key => { if (Math.random() < 0.2) delete corrupted[key]; }); return corrupted; } return data; } // 동시 요청 시뮬 async withConcurrency(fn, concurrencyLevel = 100) { const promises = Array(concurrencyLevel) .fill(null) .map(() => fn()); const results = await Promise.allSettled(promises); const failures = results.filter(r => r.status === 'rejected'); if (failures.length > 0) { console.error(`${failures.length}/${concurrencyLevel}개 실패`); failures.forEach(f => console.error(f.reason)); } return results; } }

전략 3: 기존 코드랑 비교 테스트

AI가 기존 기능을 대체하는 코드를 짰다면, 동작이 똑같은지 비교하세요:

# differential_test.py import json import random from typing import Any, Callable def differential_test( original_fn: Callable, ai_generated_fn: Callable, input_generator: Callable, num_tests: int = 1000 ) -> list[dict]: """AI 코드가 다르게 동작하는 입력 찾기""" differences = [] for i in range(num_tests): test_input = input_generator() try: original_result = original_fn(test_input) original_error = None except Exception as e: original_result = None original_error = type(e).__name__ try: ai_result = ai_generated_fn(test_input) ai_error = None except Exception as e: ai_result = None ai_error = type(e).__name__ if original_result != ai_result or original_error != ai_error: differences.append({ 'input': test_input, 'original': {'result': original_result, 'error': original_error}, 'ai_generated': {'result': ai_result, 'error': ai_error} }) return differences # 사용법 def generate_random_user_input(): """엣지케이스 포함 랜덤 입력""" edge_cases = [ None, {}, {'name': None}, {'name': ''}, {'name': 'a' * 10000}, {'name': '<script>alert("xss")</script>'}, {'name': '👨‍👩‍👧‍👦'}, {'name': 'O\'Brien'}, {'id': float('nan')}, {'id': float('inf')}, ] if random.random() < 0.2: return random.choice(edge_cases) return { 'name': ''.join(random.choices('abcdefghijklmnop', k=random.randint(1, 50))), 'id': random.randint(-1000, 1000) } differences = differential_test( original_process_user, ai_generated_process_user, generate_random_user_input ) if differences: print(f"{len(differences)}개 차이 발견!") print(json.dumps(differences[:5], indent=2))

전략 4: 로깅부터 제대로

프로덕션에서 터졌을 때 로컬에서 재현하려고 하면 대부분 실패해요. 그 상황의 조건을 똑같이 만들 수가 없거든요. 그래서 처음부터 로깅을 잘 해놔야 해요:

// observability.ts interface CodeExecutionContext { functionName: string; aiGenerated: boolean; inputs: Record<string, any>; startTime: number; } class ObservableWrapper { private context: CodeExecutionContext; constructor(functionName: string, aiGenerated: boolean = true) { this.context = { functionName, aiGenerated, inputs: {}, startTime: Date.now() }; } recordInput(name: string, value: any) { this.context.inputs[name] = this.sanitize(structuredClone(value)); } recordCheckpoint(name: string, data?: any) { console.log(JSON.stringify({ type: 'checkpoint', ...this.context, checkpoint: name, data: this.sanitize(data), elapsed: Date.now() - this.context.startTime })); } recordSuccess(result: any) { console.log(JSON.stringify({ type: 'success', ...this.context, result: this.sanitize(result), duration: Date.now() - this.context.startTime })); } recordError(error: Error, additionalContext?: any) { console.error(JSON.stringify({ type: 'error', ...this.context, error: { message: error.message, name: error.name, stack: error.stack }, additionalContext, duration: Date.now() - this.context.startTime })); } private sanitize(obj: any): any { if (obj === null || obj === undefined) return obj; if (typeof obj !== 'object') return obj; const sensitiveKeys = ['password', 'token', 'secret', 'apiKey', 'authorization']; const result: any = Array.isArray(obj) ? [] : {}; for (const [key, value] of Object.entries(obj)) { if (sensitiveKeys.some(k => key.toLowerCase().includes(k))) { result[key] = '[REDACTED]'; } else if (typeof value === 'object') { result[key] = this.sanitize(value); } else { result[key] = value; } } return result; } } // 사용법 async function aiGeneratedProcessOrder(order: Order) { const obs = new ObservableWrapper('processOrder', true); obs.recordInput('order', order); try { obs.recordCheckpoint('validation_start'); const validated = validateOrder(order); obs.recordCheckpoint('validation_complete', { isValid: true }); obs.recordCheckpoint('payment_start'); const payment = await processPayment(validated); obs.recordCheckpoint('payment_complete', { paymentId: payment.id }); obs.recordCheckpoint('fulfillment_start'); const result = await fulfillOrder(validated, payment); obs.recordCheckpoint('fulfillment_complete'); obs.recordSuccess(result); return result; } catch (error) { obs.recordError(error as Error, { orderState: order.status, retryable: isRetryableError(error) }); throw error; } }

예방이 최선: AI에 강한 개발 파이프라인

디버깅 안 해도 되는 게 최고잖아요. 애초에 문제를 막는 방법을 알아볼게요.

1. AI한테 제대로 시키기

## 프로덕션용 AI 프롬프트 템플릿 [함수 설명]을 다음 요구사항대로 짜줘: **상황:** - 이 코드는 [예상 트래픽]을 받는 프로덕션에서 돈다 - [기존 시스템]이랑 연동해야 해 - 우리 코드베이스는 [네이밍 컨벤션]이랑 [코드 스타일] 씀 **필수:** 1. 에러 핸들링 꼭 넣어: - 네트워크 에러, 타임아웃 - 잘못된 입력 데이터 - null/undefined - 동시 요청 2. 입력 검증 넣어 3. 주요 지점에 로깅 넣어 4. 엣지케이스 다 처리해 5. 이 의존성만 써 (없는 거 가져다 쓰지 마): [사용 가능한 의존성 목록] **하지 마:** - deprecated API 쓰지 마 - 예외 삼키지 마 - 외부 서비스 항상 된다고 가정하지 마 - 데이터 항상 완전하다고 가정하지 마 **스타일:** - [snake_case/camelCase] 써 - async면 타임아웃 처리 필수 - 함수 50줄 넘기지 마

2. CI에서 자동 체크

# .github/workflows/ai-code-review.yml name: AI 코드 리뷰 on: pull_request: paths: - '**.js' - '**.ts' - '**.py' jobs: ai-code-check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: AI 코드 패턴 감지 run: | # async에 try/catch 있는지 grep -rn "async.*{$" --include="*.ts" --include="*.js" | \ xargs -I {} sh -c 'file="{}"; grep -L "try\|catch" "$file" && echo "$file에 try/catch 없음"' # Python bare except grep -rn "except:$" --include="*.py" && echo "bare except 발견" # deprecated React grep -rn "componentWillMount\|componentWillReceiveProps" --include="*.tsx" --include="*.jsx" && \ echo "deprecated React 라이프사이클 발견" - name: 복잡도 체크 run: | npx complexity-report --format json src/ | \ jq '.functions[] | select(.complexity > 15) | {name, complexity}' - name: 보안 패턴 체크 run: | grep -rn "md5\|sha1" --include="*.py" --include="*.js" | grep -i password && \ echo "취약한 해시 함수 발견"

3. AI 코드 격리 패턴

AI 코드를 믿을 수 없는 입력처럼 취급하세요. 격리하고, 검증하고, 천천히 신뢰를 쌓아가세요:

// ai-code-quarantine.ts interface QuarantinedFunction<TInput, TOutput> { implementation: (input: TInput) => TOutput | Promise<TOutput>; validator: (input: TInput) => boolean; sanitizer: (input: TInput) => TInput; fallback: (input: TInput, error: Error) => TOutput; } function createQuarantinedFunction<TInput, TOutput>( config: QuarantinedFunction<TInput, TOutput> ) { return async function quarantined(input: TInput): Promise<TOutput> { if (!config.validator(input)) { throw new Error('입력 검증 실패'); } const sanitizedInput = config.sanitizer(input); try { const result = await Promise.race([ config.implementation(sanitizedInput), new Promise<never>((_, reject) => setTimeout(() => reject(new Error('타임아웃')), 5000) ) ]); return result; } catch (error) { console.error('격리 함수 실패:', error); return config.fallback(sanitizedInput, error as Error); } }; } // 사용법 const processUserData = createQuarantinedFunction({ implementation: aiGeneratedProcessUserData, validator: (input) => input != null && typeof input.id === 'number', sanitizer: (input) => ({ ...input, name: String(input.name || '').slice(0, 100) }), fallback: (input, error) => { return originalProcessUserData(input); } });

사람이랑 AI랑 잘 협업하기

목표는 AI를 안 쓰는 게 아니에요. AI가 속도를 내주고, 사람이 안정성을 챙기는 협업 모델을 만드는 거예요.

리뷰 체크리스트

AI 코드 머지 전에 확인할 것들:

## AI 코드 리뷰 체크리스트 ### 필수 (다 통과해야 함) - [ ] import 다 있음 - [ ] deprecated API 안 씀 - [ ] 에러 핸들링 있음 (네트워크, 타임아웃, null) - [ ] 입력 검증 있음 - [ ] 민감 정보 로깅 안 함 - [ ] 하드코딩된 시크릿 없음 ### 프로덕션 준비 - [ ] 동시성 처리 됨 - [ ] 재시도 로직 있음 - [ ] 서킷브레이커 있음 - [ ] 디버깅용 로그 있음 - [ ] 리소스 정리 보장됨 ### 스타일 - [ ] 네이밍 컨벤션 맞음 - [ ] 복잡도 적당함 - [ ] 엣지케이스 테스트 있음

신뢰 레벨 시스템

AI 코드에 대한 신뢰를 단계적으로 쌓아가세요:

레벨 1 - 격리 (0-10회 사용): 폴백 필수, 로그 많이, 섀도우 테스트
레벨 2 - 관찰 (10-100회): 폴백 준비, 로그 강화
레벨 3 - 신뢰 (100회 이상 무사고): 일반 로그, 폴백 불필요

정리

AI 코드가 프로덕션에서 터지는 건 예측 가능해요: 컨텍스트 한계, 옛날 학습 데이터, 해피 패스만 앎, 동시성 몰라요. 이 패턴들을 알면 배포 전에 잡을 수 있고, 터졌을 때도 빠르게 고칠 수 있어요.

핵심 정리:

  1. AI는 여러분 코드를 모른다 — 패턴 기반으로 추측만 함. import, 네이밍, 의존성 꼭 확인.

  2. AI는 예제로 배웠지, 프로덕션으로 안 배웠다 — 에러 핸들링, 엣지케이스, 동시성 직접 테스트해야 함.

  3. AI 학습 데이터는 과거다 — deprecated API, 옛날 보안 패턴 체크 필수.

  4. 로깅 처음부터 잘 해놔라 — AI 코드 표시해두면 디버깅 빨라짐.

  5. 믿되 검증하라 — 격리 패턴으로 안전하게 통합하면서 프로덕션 안정성 유지.

2026년에 잘 나가는 개발자는 AI 안 쓰는 사람도 아니고, AI 코드 무조건 믿는 사람도 아닐 거예요. 터지는 패턴을 이해하고, 검증 파이프라인 만들고, 사람-AI 협업을 잘 하는 사람들이에요.

AI 코드는 사라지지 않아요. 왜 터지는지, 어떻게 고치는지 아는 게 이제 필수 스킬입니다.

aidebuggingproductioncode-qualitybest-practicesllm

관련 도구 둘러보기

Pockit의 무료 개발자 도구를 사용해 보세요