
AI 생성 코드를 30분 만에 리뷰하는 절차 — 보는 순서와 도구 사용법
요약
AI가 생성한 코드를 효율적으로 리뷰하기 위한 30분 절차와 우선순위 가이드를 제공합니다. 보안 취약점과 기밀 정보 유출 등 실질적 피해가 큰 항목을 중심으로 리뷰 순서를 정립하는 방법을 다룹니다.
핵심 포인트
- 작동 여부보다 '어떻게 망가지는가'에 집중하여 리뷰할 것
- 기밀 정보 유출, 인가 누락 등 보안 리스크를 최우선으로 검토
- 리뷰 전 README와 의존 관계를 통해 코드의 지도를 먼저 파악
- grep 및 git log를 활용해 시크릿 노출 여부를 기계적으로 점검
서론
지난 기사 「AI 생성 코드를 리뷰하며 발견한 전형적인 버그 5선」에서는 어떤 버그가 발생하는지를 카탈로그 형태로 정리했습니다. 이번에는 그 후속편으로, 실제로 어떻게 리뷰할 것인가 — 보는 순서, 시간 배분, 사용하는 도구에 대해 이야기합니다.
리뷰에서 가장 흔히 발생하는 실수는 위에서부터 순서대로 전부 읽으려고 하는 것입니다. AI 생성 코드는 양이 많고, 게다가 겉보기에는 깔끔하게 작성되어 있습니다. 처음부터 읽어 내려가다 보면 사소한 명명 규칙(Naming)이나 작성 방식에 시간을 다 써버려서, 정작 중요한 보안 취약점(Security Hole)에 도달하지 못한 채 끝나버립니다.
한정된 시간 내에 최대의 리스크를 제거하려면, 우선순위를 고정해 두는 것이 효과적입니다. 이 기사에서는 제가 실제로 사용하고 있는 30분 리뷰 절차를 공개합니다.
대원칙: 실질적인 피해가 큰 순서대로 보기
AI 생성 코드 리뷰에서는 "작동하는지 여부"는 거의 확인할 필요가 없습니다. AI는 작동하는 코드를 쓰는 데 능숙하므로 대부분 잘 작동합니다. 우리가 봐야 할 것은 "어떻게 망가지는가" 입니다.
실질적인 피해의 크기는 대략 다음과 같은 순서입니다.
기밀 정보 유출 — 한 번 유출되면 돌이킬 수 없음 (키 재발급·부정 이용) -
인가(Authorization) 누락 — 타인의 데이터를 보거나 삭제할 수 있음. 신뢰를 즉시 상실함 -
입력 유효성 검사(Input Validation) 결여 — XSS·인젝션(Injection)의 입구 -
에러 처리와 로그 — 장애를 인지하지 못하거나 원인을 추적할 수 없음 -
의존성 라이브러리의 알려진 취약점 — 내 코드는 안전해도 공격당할 수 있음 -
성능 지뢰 — 지금은 작동하지만, 데이터가 늘어나면 무너짐 -
가독성·명명·설계 — 중요하지만, 공개 전 체크 우선순위로는 마지막
위에서부터 순서대로 시간을 사용하는 것. 이것만으로도 리뷰의 비용 대비 효과는 크게 달라집니다.
30분 배분
| Step | 내용 | 시간 |
|---|---|---|
| 0 | 전체 파악 | 3분 |
| ... |
Step 0. 전체 파악 (3분)
코드를 읽기 전에, 지도를 만듭니다. 이 단계를 건너뛰면 나중에 "이것은 어디서 호출되는가"를 매번 찾아봐야 하는 상황이 발생합니다.
보는 것은 딱 3가지뿐입니다.
README — 어떤 앱이며, 무엇을 다루는가 (=지켜야 할 데이터는 무엇인가) -
의존 관계 (package.json 등) — 어떤 프레임워크/DB/인증을 사용하는가 -
엔트리 포인트(Entry Point)와 루트(Route) 목록 — 외부에서 호출할 수 있는 입구의 전체상 -
루트 목록은 손으로 찾지 않고 기계적으로 추출합니다.
# Next.js: App Router의 API 루트를 열거
find app -name "route.ts" -o -name "route.js" | sort
# Express: 루트 정의를 열거
...
이 "외부에서 호출할 수 있는 입구의 목록"이 이후 모든 공정의 작업 대상이 됩니다.
Step 1. 시크릿 (5분)
최우선 사항입니다. 우선 기계적으로 찾습니다.
# 흔히 쓰이는 키 형태를 모아서 검색
grep -rnE "(sk-[a-zA-Z0-9]{10,}|AKIA[0-9A-Z]{16}|ghp_[a-zA-Z0-9]{30,})" . \--exclude-dir={node_modules,.git,dist,build}
...
현재 파일만 보고 안심하지 마세요. 중요한 것은 Git 이력입니다.
# 이력 전체에서 시크릿 같은 문자열을 찾기
git log -p --all | grep -nE "sk-[a-zA-Z0-9]{10,}|AKIA[0-9A-Z]{16}"
# 전용 도구를 사용하는 것이 확실함
...
이력에 단 한 번이라도 포함되어 있었다면, 해당 키를 무효화하고 재발급하는 것이 유일한 정답입니다. 이력을 수정하는 것(filter-repo 등)은 포크(Fork)나 클론(Clone)된 곳에는 적용되지 않습니다.
함께 .gitignore에 .env가 포함되어 있는지, .env.example에 더미(Dummy) 값만 들어있는지도 확인합니다.
Step 2. 인증·인가 (7분)
이 부분이 가장 배점이 높은 공정입니다. 시간도 가장 많이 사용합니다.
Step 0에서 만든 루트 목록에 대해, 각 엔드포인트(Endpoint)를 표로 만듭니다.
| 엔드포인트 | 인증 (로그인 필수 여부) | 인가 (자신의 리소스만 접근 가능한지) |
|---|---|---|
GET /api/orders/:id | ✅ | ❓ ← 확인 필요 |
DELETE /api/posts/:id | ✅ | ❓ ← 확인 필요 |
AI 생성 코드에서는 인증(Authentication, 누구인지)은 작성되어 있지만, 인가(Authorization, 접근 권한이 있는지)가 누락된 것이 전형적인 패턴입니다. 특히 :id를 받는 엔드포인트는 모두 의심해야 합니다.
// 위험한 패턴: 로그인 여부만 확인하고, 소유자인지는 확인하지 않음
const order = await db.orders.findById(req.params.id)
return res.json(order)
...
효율적인 체크 방법은 req.user.id(또는 세션의 사용자 ID)가 쿼리 조건에 포함되어 있는지를 기계적으로 확인하는 것입니다.
# :id를 받는 핸들러를 열거하고, 사용자 ID 필터링이 있는지 육안으로 확인
grep -rn "params\.\(id\|.*Id\)" src/ -A 10 | grep -c "user"
실제로 타인의 ID를 넣어 호출해 보는 것이 가장 확실합니다. 계정을 두 개 만들어, A의 세션으로 B의 리소스 ID를 호출해 보세요. 이때 데이터가 보인다면 실패한 것입니다.
Step 3. 입력 유효성 검사 (Input Validation, 5분)
외부에서 값이 들어오는 입구를 열거하고, 서버 측에서 검증되고 있는지만 확인합니다.
입구는 주로 다음 4가지입니다.
- 요청 본문 (
req.body) - 쿼리 파라미터 (
req.query) - 경로 파라미터 (
req.params) - 헤더 및 쿠키
# 검증 없이 body를 분해하는 곳을 찾음
grep -rn "req.body" src/ | grep -v "parse\|validate\|schema"
TypeScript의 타입은 런타임(Runtime) 시점에 사라지기 때문에, 타입 주석이 있더라도 검증이 되지 않습니다. zod 등으로 스키마 검증(Schema Validation)을 하고 있는지 확인합니다.
const schema = z.object({
email: z.string().email(),
age: z.number().int().min(0).max(150),
...
이와 함께, 사용자 입력을 그대로 HTML에 삽입하고 있지 않은지 (dangerouslySetInnerHTML, v-html, 템플릿의 생출력), SQL을 문자열 결합으로 생성하고 있지 않은지도 확인합니다.
grep -rn "dangerouslySetInnerHTML\|v-html" src/
grep -rnE "(SELECT|INSERT|UPDATE|DELETE).*\+ *(req|params|query)" src/
Step 4. 에러 처리 및 로그 (Error Handling & Logging, 5분)
장애를 인지할 수 있는지 확인하는 과정입니다.
# 빈 catch 블록(에러를 무시하는 경우)을 찾음
grep -rnA 2 "catch" src/ | grep -B 1 "^\s{*}"
확인 포인트는 3가지입니다.
- 빈(Empty) catch: 실패가 아무런 메시지 없이 사라지는
catch가 없는가 - 에러 상세 내용을 사용자에게 그대로 반환하는가: 스택 트레이스(Stack Trace)나 SQL 에러는 정보 유출의 원인이 됩니다.
- 로그가 남는가: 최소한 언제, 어떤 처리가, 무엇 때문에 실패했는지는 기록되어야 합니다.
try {
await saveToDatabase(data)
} catch (error) {
...
Step 5. 의존성 라이브러리의 취약점 (Dependency Vulnerabilities, 3분)
자신의 코드가 완벽하더라도 의존성 라이브러리를 통해 공격받을 수 있습니다. 이 부분은 도구에 맡깁니다.
npm audit --omit=dev # 프로덕션 의존성의 알려진 취약점 확인
npm outdated # 크게 오래된 버전이 있는지 확인
critical 또는 high 등급의 취약점이 있다면, 배포 전에 반드시 해결해야 합니다. GitHub를 사용한다면 Dependabot을 활성화하여 지속적으로 탐지할 수 있습니다.
Step 6. 성능 지뢰 (Performance Pitfalls, 2분)
깊게 파고들지는 않습니다. 루프 내 쿼리(N+1 문제)만 확인합니다.
# 루프 안에 await가 포함된 곳
grep -rnB 3 "await.*find\|await.*query\|await.*select" src/ | grep -E "for |\.map\(|forEach"
10건일 때는 눈에 띄지 않다가 1만 건일 때 시스템이 무너지는 것이 이 패턴입니다. 발견하면 일괄 조회(findByIds 또는 JOIN, ORM의 eager loading) 방식으로 변경합니다.
자동화할 수 있는 부분은 자동화한다
수동으로 검토하기 전에 기계로 처리할 수 있는 것들을 미리 처리해 두면, 인간의 시간을 상위 공정으로 돌릴 수 있습니다.
| 목적 | 도구 |
|---|---|
| 시크릿 탐지 (Secret Detection) | gitleaks, trufflehog |
| 의존성 취약점 (Dependency Vulnerability) | npm audit, Dependabot, Renovate |
| 정적 분석 (Static Analysis, 보안 관점) | semgrep, eslint-plugin-security |
| 타입 오류 (Type Holes) | tsc --noEmit, strict: true |
semgrep --config=auto는 설정 없이도 어느 정도 탐지해 주기 때문에 첫걸음으로 적합합니다.
리뷰 결과 전달 방법
찾아내는 것만으로는 가치가 되지 않습니다. 수정 순서가 결정된 형태로 전달해야 합니다. 저는 4단계로 나누고 있습니다.
- Critical— 공개 전 반드시 수정 (시크릿 유출, 인가 누락)
- High— 공개 전 수정해야 함 (입력 검증 결여, 알려진 고위험 취약점)
- Medium— 공개 후 즉시 대응 (에러 처리, 로그 부족)
- Low— 여유가 있을 때 (네이밍, 중복, 경미한 성능 문제)
각 항목에는 "어디가 (파일:행) / 무엇이 문제인가 / 어떻게 수정하는가"의 3가지를 반드시 첨부합니다. 지적만 하고 수정 방법이 없는 리포트는 받는 사람이 움직일 수 없습니다.
마치며: 자신의 코드는 스스로 허점을 발견하기 어렵다
이 절차는 자신의 리포지토리(Repository)에도 그대로 사용할 수 있습니다. 우선 Step 1(시크릿)과 Step 2(인가)만이라도 돌려보세요. 이 두 가지만으로도 실질적인 피해가 큰 사고의 대부분을 방지할 수 있습니다.
그렇다고는 해도, 자신이 작성한 (혹은 AI에게 작성하게 한) 코드는 아무래도 전제를 공유하고 있기 때문에 허점이 보이기 어렵습니다. 공개 전에 제삼자의 눈을 거치고 싶을 때를 위해, AI가 작성한 코드의 리뷰 및 버그 수정을 현역 엔지니어에게 스팟(Spot)으로 의뢰할 수 있는 창구인 "HeyGeek"를 시험 운영 중입니다. 리포지토리 URL과 고민 사항을 보내는 것만으로, 이 절차에 따라 사람이 체크합니다 👉 HeyGeek (공개 전 체크)
Discussion

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