AI가 만든 코드의 리뷰 기준은 사람이 만든 코드와 같습니다. 요구사항을 만족하는지, 권한을 지키는지, 실패했을 때 되돌릴 수 있는지를 증거로 확인해야 합니다. 생성 속도가 빠르면 diff가 커지기 쉬우므로 변경 범위와 검증 범위를 먼저 고정하는 일이 더 중요해집니다.

문서 소유자만 제목을 바꿀 수 있어야 하는 기능을 가상 사례로 살펴보겠습니다. 수정 버튼을 숨긴 코드와 서버에서 변경을 거부하는 코드의 차이가 리뷰의 출발점입니다.

배포 전 코드 리뷰의 흐름
  1. 범위요청과 실제 변경 파일, 설정·스키마 변경을 맞춥니다.
  2. 실패잘못된 구현이 깨져야 하는 테스트를 먼저 적습니다.
  3. 수정서버 경계에서 권한·입력을 검증하고 최소 데이터만 반환합니다.
  4. 운영관찰 지표와 중단·롤백 조건까지 확인합니다.

실행 가능성과 배포 가능성을 구분합니다

문법 검사와 테스트 통과는 각각 제한된 사실만 말합니다. 화면이 열려도 직접 만든 HTTP 요청이 권한을 우회할 수 있고, 테스트가 통과해도 위험한 조건이 애초에 테스트에 없을 수 있습니다.

핵심 비교
구분역할설명예시
diff요청한 범위인지 확인관련 없는 리팩터링, 새 의존성, 환경 변수와 데이터 변경을 먼저 찾습니다.파일 목록과 요구사항을 한 줄씩 연결
실패 테스트결함이 실제로 드러나는지 확인수정 전에는 실패하고 수정 후에는 통과해야 할 조건을 적습니다.다른 사용자의 문서 수정 요청은 403
운영 증거배포 뒤 판단할 신호를 확인오류율·거부율·로그와 되돌릴 변경 단위를 함께 봅니다.권한 거부 증가 시 중단 기준

가상 diff: 버튼은 숨겼지만 서버 권한 검사가 없습니다

문서 소유자만 제목을 바꾸게 해 달라는 요청을 AI에게 맡겼다고 가정하겠습니다. 제안된 코드는 화면에서 다른 사용자의 수정 버튼을 숨겼지만, Server Function은 로그인 여부만 확인합니다.

TypeScript
'use server'

export async function renameDocument(documentId: string, title: string) {
  const session = await verifySession()
  if (!session) throw new Error('Unauthorized')

  return db.document.update({
    where: { id: documentId },
    data: { title },
  })
}

문제는 documentId가 현재 사용자의 문서인지 서버에서 확인하지 않는다는 점입니다. UI에서 버튼을 감춰도 Server Function은 직접 POST 요청으로 호출될 수 있습니다. Next.js의 데이터 변경 문서와 인증 가이드는 Server Function과 Route Handler를 공개 API처럼 다루고 민감한 동작 안에서 인증과 인가를 확인하라고 안내합니다. OWASP Authorization Cheat Sheet는 기본 거부와 모든 요청의 권한 검증을 권고합니다.

같은 문서 제목 변경 요청이 서버의 소유자 확인 전에는 적용되고 확인 후에는 거부되는 가상 사례
수정 버튼을 숨기는 것과 서버에서 문서 소유자를 확인하는 것은 서로 다른 보호입니다. 작동 방식을 설명한 재구성입니다.

실패 테스트가 리뷰의 기준점을 만듭니다

이 diff를 검토할 때는 구현 설명보다 먼저 아래 기대값을 고정합니다.

Text
가상 입력
- 로그인 사용자: user-a
- 대상 문서: doc-b, 소유자 user-b
- 요청: doc-b 제목 변경

수정 전 드러나야 할 실패
- 요청이 성공해 다른 사용자의 문서가 바뀐다

수정 후 기대값
- 서버가 403 또는 애플리케이션의 동등한 거부 결과를 반환한다
- doc-b 제목은 바뀌지 않는다
- 거부 로그에는 토큰·문서 본문을 남기지 않는다

테스트는 정상 사용자뿐 아니라 미인증 사용자, 존재하지 않는 문서, 다른 테넌트, 동시에 들어온 중복 요청을 포함해야 합니다. 수정 전 코드에서도 통과하는 테스트만 추가했다면 결함을 검출했다는 증거가 되지 않습니다. 실행했다면 명령·환경·성공·실패·미실행 항목을 기록하고, 실행하지 않았다면 이 문서처럼 기대값이라고 분명히 표시합니다.

테스트가 초록색인데도 놓치는 경우

AI가 구현과 테스트를 함께 고쳤다면 테스트 diff도 따로 읽어야 합니다. 다른 사용자의 요청을 거부하라는 검사를 삭제하거나, 기대값을 성공으로 바꾸거나, 실패 테스트를 건너뛰면 화면에는 통과만 남습니다. 테스트 이름이 그대로여도 검증하는 내용은 바뀔 수 있습니다.

이 사례에서는 응답의 거부 값과 저장된 doc-b의 제목이 그대로인지를 함께 확인합니다. 응답만 거부하면서 이미 데이터는 바꿨을 수도 있기 때문입니다. 같은 실패 테스트를 수정 전 코드에 적용했을 때는 결함을 드러내고 수정 후에는 통과하는지 확인하면, 검사 자체가 느슨해진 경우를 구분하기 쉬워집니다.

수정 기대값은 서버 경계에 둡니다

권한 검사는 클라이언트가 보낸 ownerId나 role 값을 믿지 않고, 서버 세션과 저장된 리소스 관계로 판단해야 합니다. 실제 프로젝트의 인증 라이브러리와 데이터 계층에 맞춰 다음 구조를 구현합니다.

TypeScript
'use server'

// Prisma 7 계열 API 형태의 예시. id는 고유 키라고 가정합니다.
export async function renameDocument(documentId: unknown, title: unknown) {
  const session = await verifySession()
  if (!session) return { ok: false, code: 'UNAUTHORIZED' }

  if (typeof documentId !== 'string' || documentId.length === 0 ||
      typeof title !== 'string') {
    return { ok: false, code: 'INVALID_INPUT' }
  }
  const normalizedTitle = title.trim()
  if (normalizedTitle.length < 1 || normalizedTitle.length > 120) {
    return { ok: false, code: 'INVALID_INPUT' }
  }

  const result = await db.document.updateMany({
    where: { id: documentId, ownerId: session.user.id },
    data: { title: normalizedTitle },
  })
  if (result.count !== 1) return { ok: false, code: 'FORBIDDEN' }
  return { ok: true }
}

TypeScript의 문자열 타입만 적는 것으로 외부 입력 검증이 끝나지는 않습니다. 예시는 실행 중 타입과 길이를 확인하고, 서버가 검증한 세션의 사용자 ID를 사용합니다. Next.js 서버 함수 지침도 민감한 서버 동작 전에 인증과 인가를 확인하도록 안내합니다.

소유자를 읽어 확인한 뒤 ID만으로 갱신하면 두 동작 사이에 소유자가 바뀔 여지가 있습니다. 여기서는 Prisma 7의 updateMany 형태로 ID와 소유자 조건을 갱신 자체에 함께 넣고, 영향받은 행 수를 확인합니다. 존재하지 않는 문서와 다른 사람의 문서는 모두 같은 거부 결과가 됩니다. 세션 검증 함수와 DB 모델은 프로젝트에서 제공해야 하며, 테넌트·역할·공동 편집 정책이나 여러 행을 묶는 동작은 별도로 설계해야 합니다.

반환한 code는 클라이언트가 읽는 애플리케이션 결과입니다. 객체에 status: 403을 적는 것만으로 실제 HTTP 상태가 403이 되는 것은 아닙니다. Server Function의 반환값 검사와 HTTP API의 상태 코드 검사를 구분하세요.

보안 리뷰는 데이터 흐름까지 따라갑니다

권한 검사 한 줄을 추가한 뒤에도 다음 경로를 확인합니다.

  • 사용자·테넌트·리소스 ID를 요청 본문에서 그대로 신뢰하지 않는가
  • 로그와 오류 응답에 세션·토큰·개인정보·문서 본문이 남지 않는가
  • SQL·셸·HTML에 외부 입력이 안전한 방식으로 전달되는가
  • 외부 API 응답과 업로드 파일을 다시 검증하는가
  • 권한 실패가 기본 허용으로 빠지는 분기가 없는가

에이전트의 파일·명령·네트워크 승인은 코드 자체의 권한 검사를 대신하지 않습니다. Claude Code 보안 문서와 Codex 승인·보안 안내는 에이전트 실행 범위와 사람의 승인 경계를 설명합니다. 제품의 승인 UI가 애플리케이션 인가 결함을 자동으로 고친다는 뜻은 아닙니다.

의존성·설정·스키마도 diff에 포함됩니다

새 패키지가 들어왔다면 필요성, 유지보수 상태, 취약점 공지, 라이선스, 설치 스크립트를 확인합니다. 환경 변수나 권한 정책, 데이터베이스 migration이 바뀌었다면 이전 버전과 함께 동작하는 기간과 rollback 순서를 적습니다.

작은 유틸리티 하나를 위해 큰 의존성을 추가했거나, 기능 변경과 무관한 포맷 수정이 수십 파일에 섞였다면 리뷰 가능한 크기로 다시 나누는 편이 좋습니다. 변경량을 줄이는 일은 스타일 취향이 아니라 결함이 숨을 공간을 줄이는 검증 작업입니다.

작은 배포에서 이상을 관찰한 뒤 중단하고 이전 코드로 되돌리되 데이터 복구를 별도로 확인하는 흐름
중단 기준과 되돌릴 변경 단위를 미리 정하고, 데이터 변경은 별도로 확인합니다. 작동 방식을 설명한 재구성입니다.

배포 판단에는 롤백 기록이 필요합니다

리뷰어에게 전달할 증거는 네 묶음이면 충분합니다.

  1. 요청한 동작과 다루지 않은 범위
  2. 핵심 diff와 설정·의존성·스키마 변경
  3. 실패 테스트, 통과한 검사, 실행하지 못한 검사
  4. 배포 관찰 지표, 중단 조건, 코드·데이터 rollback 순서

위 가상 사례라면 다른 사용자의 문서 변경이 거부되는지, 정상 소유자의 변경은 유지되는지, 401·403 비율이 갑자기 늘지 않는지를 봅니다. 오류가 늘면 해당 쓰기 기능을 잠시 중단하거나 안전성이 확인된 revision으로 되돌릴 수 있어야 합니다. 이 사례의 수정 전 코드는 권한 결함이 있으므로 그대로 복원하면 안 됩니다. 데이터 migration이 포함됐다면 코드 rollback만으로 충분한지도 별도로 확인합니다.

리뷰마다 반복하는 검사 명령과 수정 금지 범위는 AGENTS.md·CLAUDE.md 작성법을 참고해 프로젝트 규칙으로 남길 수 있습니다. 규칙을 적어 두는 것과 실제 검사를 통과하는 것은 별도로 확인해야 합니다.

복사해 쓰는 최종 체크리스트

md
- [ ] 요구사항과 변경 파일을 연결했다.
- [ ] 관련 없는 수정과 새 의존성을 분리했다.
- [ ] 수정 전 실패하고 수정 후 통과할 조건을 적었다.
- [ ] 정상·경계·권한·중복·외부 실패를 확인했다.
- [ ] 인증·인가와 입력 검증을 서버 동작 안에서 확인했다.
- [ ] 로그와 오류 응답에 민감정보가 없다.
- [ ] 실행한 명령, 실패, 미실행 검사를 구분했다.
- [ ] 배포 중단 기준과 코드·데이터 rollback 경로가 있다.
- [ ] 사람이 diff와 남은 위험을 이해하고 승인했다.

AI가 작성했다는 사실만으로 코드를 더 위험하거나 안전하다고 판정할 수는 없습니다. 배포 여부는 diff, 실패를 검출하는 테스트, 서버 권한 경계, 운영 복구 계획으로 판단합니다.

VIEW—

© Chacolate. 일부를 인용할 때는 글 제목과 원문 링크를 함께 표시해 주세요. 무단 전재·재배포는 허용하지 않습니다.