본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 02. 14:08

AI 코드 리뷰: 유능한 조수인가, 잘못된 확신을 주는 기계인가?

요약

AI 코드 리뷰어의 효용성과 위험성을 분석합니다. 단순 스타일 교정 및 안티패턴 탐지에는 탁월하지만, 복잡한 로직이나 보안 취약점에서는 잘못된 확신을 줄 수 있어 올바른 워크플로 설계가 필수적입니다.

핵심 포인트

  • 명명 규칙, 스타일, 에러 핸들링 등 표면적 리뷰에 매우 효율적임
  • 잘 알려진 안티패턴에 대한 기계적 패턴 매칭 능력이 뛰어남
  • 복잡한 권한 부여 등 심층적 로직 오류에는 취약할 수 있음
  • AI의 리뷰 결과를 맹신하지 않는 워크플로 설계가 중요함

당신은 풀 리퀘스트 (Pull Request)를 엽니다. 30초 후, AI 리뷰어가 댓글을 남깁니다. "제가 보기엔 괜찮습니다. 발견된 이슈가 없습니다."

당신은 미세한 화학적 보상을 느낍니다. 승인. 속도. 머지 (Merge)까지 단 한 번의 클릭만이 남았습니다. 인간 리뷰어를 기다리는 인지적 비용은 단 30초로 압축되었고, 당신이 두 시간 동안 씨름하며 작성한 디프 (Diff)는 현존하는 그 어떤 엔지니어보다 더 많은 코드를 읽은 시스템으로부터 축복을 받았습니다.

그러다 일주일 후, 동일한 코드가 프로덕션 (Production) 환경에 미묘한 권한 부여 (Authorization) 버그를 배포하고, 당신은 인시던트 (Incident) 채널을 응시하며 그 "제가 보기엔 괜찮습니다"라는 말이 어떻게 실제 리뷰를 통과했는지 의문을 갖게 됩니다.

이것은 모든 AI 코드 리뷰어를 그림자처럼 따라다니는 질문입니다. 이것은 유능한 조수인가 — 점심시간 전에 당신에게 12개의 댓글을 남기곤 했던 시니어 엔지니어의 더 빠르고, 차분하며, 인내심 있는 버전인가? 아니면 잘못된 확신을 주는 기계인가 — 자신이 완전히 이해하지 못하는 코드에 대해 안심시키는 말을 내뱉으며, 당신이 어쨌든 머지하도록 설득하는 도구인가?

솔직한 답변은 _둘 다_이며, 당신이 어떤 것을 얻게 될지는 그것을 둘러싼 워크플로 (Workflow)를 어떻게 설정하느냐에 거의 전적으로 달려 있습니다. 실제로 중요한 네 가지 관점 — 이 리뷰어들이 무엇을 잘 잡아내는지, 정확성 측면에서 어디서 실패하는지, 보안 측면에서 어디서 실패하는지, 어디서 내용을 지어내는지, 그리고 단점을 떠안지 않으면서 장점만 취할 수 있는 리뷰 프로세스를 어떻게 설계할 것인지에 대해 자세히 살펴보겠습니다.

AI 코드 리뷰가 실제로 잘하는 것

실패 사례들을 나열하기 전에, 이 도구들이 잘하는 것에 대해 공정하게 평가할 가치가 있습니다. 왜냐하면 대부분의 팀은 올바른 이유로 이 도구들을 도입하고, 그 이유가 무엇이었는지는 잊어버리기 때문입니다.

AI 리뷰어는 코드 리뷰의 표면 계층 (Surface layer)에서 매우 뛰어납니다. 유능한 엔지니어들이 작성하기 귀찮아서 입 밖으로 내뱉는 것은 그만두었지만, 여전히 누군가가 잡아주기를 바라는 종류의 댓글들 말입니다:

  • 동일한 diff 내에서의 일관성 없는 명명 (Inconsistent naming). 한 함수에서는 userId라고 부르고, 다음 함수에서는 user_id라고 불렀습니다.
  • 스타일 드리프트 (Style drift). 여기서는 trailing comma(후행 쉼표)가 있고, 저기서는 빠져 있으며, 파일의 나머지 부분은 async/await를 사용하는데 여기서는 callback을 사용합니다.
  • 명백한 nullability(null 가능성) 허점. undefined를 반환할 수 있는 try 블록의 두 줄 뒤에서 response.data.user를 구조 분해 할당 (destructuring) 했습니다.
  • 누락된 에러 핸들링 (Error handling). 새로운 엔드포인트가 데이터베이스 에러는 잡아내지만, 두 줄 아래에서 인증(auth) 에러는 삼켜버립니다.
  • 데드 코드 (Dead code), 사용되지 않는 변수, 더 이상 아무것도 가리키지 않는 import 문.
  • 함수 시그니처(signature)는 변경되었으나 JSDoc은 변경되지 않아, 설명하는 함수와 모순되는 문서화 (Documentation).

이러한 코멘트들은 화려하지는 않지만, 예전에는 시니어 엔지니어들의 실제 리뷰 대역폭 (bandwidth)을 소모하곤 했습니다. 지치지 않고, 짜증 내지 않으며, 코멘트 스레드에 _"지난 네 번의 리뷰에서 언급했듯이..."_라고 쓰지 않는 도구에게 이러한 업무를 넘기는 것은 명백한 승리입니다.

AI 리뷰어가 잘하는 두 번째 요소는 **잘 알려진 안티패턴 (antipatterns)에 대한 기계적 패턴 매칭 (mechanical pattern matching)**입니다. 15년 동안 OWASP Top 10에 포함되어 온 바로 그 패턴들 말입니다. 10년 동안 모든 시니어 인터뷰 질문에 등장했던 바로 그 패턴들입니다. 수천 개의 학습 데이터셋 블로그 포스트에 나타나는 바로 그 패턴들입니다.

만약 당신이 req.query.user와 문자열을 연결하여 SQL 쿼리를 작성한다면, AI 리뷰어는 이를 찾아낼 것입니다. 상태를 변경하는 엔드포인트에서 CSRF 보호를 건너뛴다면, 이를 찾아낼 것입니다. 생(raw) 비밀번호를 로그에 남긴다면, 이를 찾아낼 것입니다. JWT secret을 커밋한다면, 이를 찾아낼 것입니다.

이 두 범주의 공통점은 _코드가 문제를 드러낸다_는 것입니다. 함수가 수상해 보입니다. diff가 증거입니다. 리뷰어는 당신의 시스템이 무엇을 하는지 알 필요가 없습니다. 그저 당신이 건네준 코드 라인들을 읽기만 하면 됩니다.

이것이 AI가 진정으로 변화시키고 있는 코드 리뷰의 절반입니다. 만약 당신의 팀이 AI 리뷰를 _정확히 이 정도_로만 취급하고 그 이상을 기대하지 않는다면, 매우 적은 리스크로 실질적인 생산성 향상을 얻을 수 있을 것입니다.

문제는 당신이 AI가 나머지 절반의 역할까지 수행하기를 기대하는 순간 시작됩니다.

나머지 절반: 정확성(Correctness), 의도(Intent), 그리고 버그가 실제로 존재하는 곳

실제 버그는 Diff(차이점)에서 눈에 보이는 코드 안에 존재하는 경우가 거의 없습니다. 버그는 Diff에 있는 코드와 시스템의 나머지 부분 사이의 관계 속에 존재합니다.

당신의 팀이 최근에 배포한 실제 버그들을 떠올려 보십시오. 그 과정을 따라가 보십시오. 다음 중 몇 개나 발견되었습니까?

  • 리뷰어가 새로운 엔드포인트(Endpoint)가 다른 엔드포인트의 로직을 중복하고 있으며, 이제 두 버전이 서로 일치하지 않는다는 점을 알아차렸을 때
  • 리뷰어가 account.status 필드가 2022년 이전에 마이그레이션된 테넌트(Tenant)에서_만_ pending_review가 될 수 있다는 점을 기억해냈고, 새 코드가 그 케이스를 처리하지 못했을 때
  • 리뷰어가 새로운 백그라운드 작업(Background job)이 5분마다 실행되지만, 해당 작업이 쿼리하는 테이블이 지난 분기에 날짜별로 파티셔닝(Partitioned)되어, 쿼리가 전체 아카이브 파티션을 스캔하기 직전이라는 점을 깨달았을 때
  • 리뷰어가 새로운 로직이 고립된 상태에서는 정확하지만, 호출자(Caller)가 이미 세 단계 상위 프레임에서 동일한 확인을 수행하여 이제 카운트가 하나 차이 나게 되었다는 점을 알아차렸을 때

이러한 버그 중 그 어느 것도 Diff에서는 보이지 않습니다. Diff는 괜찮아 보입니다. Diff는 로컬적으로는 괜찮습니다. 버그는 Diff와 그 외의 모든 것 사이의 대화 속에 존재합니다. 즉, 코드베이스의 나머지 2만 줄, 2년 전에 실행된 마이그레이션, 시스템이 프로덕션(Production)에서 실행되는 운영상의 현실 등이 그것입니다.

이 지점이 바로 AI 코드 리뷰가 가장 강력한 한계(Ceiling)에 부딪히는 곳이며, 왜 그런지에 대해 정확히 짚고 넘어갈 가치가 있습니다. 모델이 나빠서가 아닙니다. 버그를 잡는 데 필요한 정보가 프롬프트(Prompt)에 들어있지 않기 때문입니다. 리뷰어는 Diff를 읽고 있습니다. 주변 파일을 읽고 있을 수도 있습니다. 심지어 코드베이스에서 검색된 일부 청크(Chunks)를 읽고 있을 수도 있습니다. 하지만 다음은 읽고 있지 않습니다:

  • 2년 전, 모든 인증 확인(auth checks)은 컨트롤러(controller)가 아닌 미들웨어 계층(middleware layer)에서 이루어져야 한다고 팀이 결정했던 Slack 스레드.
  • 이 서비스가 3개의 리전(region)에서 실행된다고 명시된 런북(runbook), 그리고 새로운 코드가 단일 시간대(timezone)를 가정함으로써 그중 2개의 리전을 망가뜨리는 상황.
  • "소프트 삭제(soft delete)"란 데이터가 사라지는 것이 아니라, 목록에서는 숨겨지되 여전히 과금 대상이라는 제품 관리자(product manager)의 구두 결정.
  • 데이터베이스 커넥션 풀(database connection pool) 크기가 10개로 설정되어 있으므로, 이 코드베이스 내의 모든 for await에는 동시성 제한(concurrency limit)이 필요하다는 불문율.

당신의 팀에서 2년 동안 일한 시니어 리뷰어(senior reviewer)는 이러한 사항들을 잡아냅니다. 왜냐하면 시스템이 그들의 머릿속에 살아있기 때문입니다. AI는 그러한 결정들의 표면적인 흔적이 코드나 컨텍스트 윈도우(context window)에 포함되었을 때만 이를 잡아냅니다. 대부분의 경우, 그중 어느 것도 포함되지 않습니다.

여기서의 실패 모드(failure mode)는 틀린 피드백이 아닙니다. 그것은 확신에 찬 부재(confident absent) 피드백입니다. AI 리뷰어는 버그를 보지 못하기 때문에 버그를 표시하지 않습니다. 더 나쁜 것은, AI가 _"발견된 문제 없음(no issues found)"_이라고 말한다는 점입니다. 이는 부분적인 커버리지(coverage)에 대한 인정이 아니라, 긍정적인 리뷰처럼 들립니다. 코드베이스를 잘 모르는 사람이라면 최소한 망설이기라도 할 것입니다. 모델은 망설이지 않습니다. 그것이 바로 잘못된 확신(false confidence)입니다.

해결책은 모델이 더 많은 것을 하기를 기대하는 것이 아닙니다. 모델의 침묵을 증거로 취급하는 것을 멈추는 것입니다.

보안 리뷰는 이 문제의 가장 날카로운 단면입니다

보안은 "디프(diff)에서 의심스러워 보이는 것"과 "실제로 악용 가능한 것" 사이의 간극이 가장 넓고 빠르게 벌어지는 영역입니다.

세 가지 디프(diff)를 고려해 봅시다:

diff-1.py

@app.route("/users/<user_id>")
def get_user(user_id):
    query = f"SELECT * FROM users WHERE id = {user_id}"
...

이것은 SQL 인젝션(SQL injection)입니다. 현존하는 모든 AI 리뷰어는 이를 잡아낼 것입니다. 이는 모든 시대의 모든 모델의 학습 데이터(training data)에 들어있습니다. 또한, 어떤 리뷰 프로세스라도 갖춘 팀이라면 이런 코드가 배포될 가능성도 낮습니다.

diff-2.py

@app.route("/users/<user_id>")
@authenticated
def get_user(user_id):
...

깔끔해 보입니다. @authenticated 데코레이터(decorator)가 있고, 쿼리는 매개변수화(parameterized)되어 있으며, 반환 값은 구조화되어 있습니다. AI 리뷰어는 이를 읽고 _"좋아 보입니다"_라고 말할 것이며, 겉보기에는 실제로 그렇습니다.

하지만 이것은 부적절한 직접 객체 참조 (IDOR, Insecure Direct Object Reference) 취약점일 수 있습니다. _인증된 사용자(authenticated user)_가 _특정 user_id_를 가져올 권한이 있는지 확인하는 절차가 없습니다. 로그인한 사람이라면 누구나 다른 사람의 프로필을 읽을 수 있습니다.

모델은 여러분의 권한 모델(authorization model)에 대한 _의도(intent)_를 알지 못하면 이를 잡아낼 수 없습니다. 여러분의 시스템이 인증된 사용자가 다른 모든 사용자를 읽도록 허용하나요? 소셜 앱이라면 그럴 수도 있습니다. 혹은 그렇지 않을 수도 있습니다. 헬스케어 앱이라면 각 사용자는 자기 자신과 부양가족의 정보만 읽을 수 있어야 합니다. 코드 차이(diff)는 어느 쪽인지 말해주지 않습니다. 리뷰어도 어느 쪽인지 모릅니다. 그래서 리뷰어는 침묵이라는 편안한 답변을 기본값으로 선택합니다.

diff-3.py

@app.route("/teams/<team_id>/invite", methods=["POST"])
@authenticated
def invite_member(team_id):
...

구조는 비슷하지만 찾아내기는 더 어렵습니다. 모델이 team_id에 대해 동작할 _사용자(user)_의 권한을 확인한다 하더라도, request.json["email"]이 검증되지 않았다는 점, 응답 형태(response shape)를 기반으로 어떤 이메일이 이미 멤버인지 열거(enumerate)하는 데 이 엔드포인트가 사용될 수 있다는 점, 그리고 팀의 기존 패턴이 게이트웨이(gateway)에 속도 제한(rate limit)을 두는 방식이라 이 새로운 엔드포인트는 다른 경로를 통해 등록되어 속도 제한이 누락되었다는 점 등을 놓칠 수 있습니다. 눈에 보이는 이 8줄의 코드에는 그 어떤 것도 나타나 있지 않습니다.

이러한 패턴은 모든 보안 카테고리에서 반복됩니다. 권한 부여(Authorization) 버그는 _도메인 규칙(rules of your domain)_에 달려 있습니다. 경쟁 상태(Race conditions)는 _런타임이 실제 운영 환경에서 사용하는 동시성 모델(concurrency model)_에 달려 있습니다. 암호학적 실수(Cryptographic mistakes)는 여러분이 수용한 위협 모델(threat model)에 달려 있습니다. 비밀 정보 처리(Secrets handling)는 이 코드가 반환된 후 데이터가 흐르는 시스템에 달려 있습니다. 이 중 어느 것도 diff에는 들어있지 않습니다. 그 모든 것은 여러분의 팀원들의 머릿속에 있습니다.

AI 리뷰어는 보안에 대한 _1차 검토 (first pass)_로서 진정으로 유용합니다. 학습 데이터와 일치하는 패턴, 즉 OWASP 형태의 문제나 명백하고 어리석은 실수들을 잡아낼 것입니다. 위험한 점은 이 1차 검토를 _보안 리뷰 (security review)_로 취급하는 것입니다. 그것은 보안 리뷰가 아닙니다. 진정한 보안 리뷰에는 여러분의 애플리케이션이 무엇을 위한 것인지, 어떤 데이터를 보유하고 있는지, 누가 무엇을 할 수 있는지, 그리고 공격자가 실제로 무엇을 원하는지를 아는 사람이 필요합니다.

경고
AI 리뷰어가 "보안 문제가 발견되지 않았습니다"라고 말할 때, 이를 _"내 학습 데이터 세트의 패턴 중 diff와 일치하는 것이 없습니다"_라고 읽어야 합니다. 이 두 문장은 같은 의미가 아닙니다.

리뷰에서의 환각 (Hallucination) 문제

코드 생성에서의 환각 (Hallucination)은 이미 잘 알려져 있고 활발히 논의되는 실패 모드 (failure mode)입니다. 모델은 함수 이름, 패키지 버전, 설정 플래그, API 엔드포인트를 지어냅니다. 여러분은 아마도 모델이 존재하지 않는 모듈을 자신 있게 임포트 (import)하는 것을 본 적이 있을 것입니다.

덜 논의되는 부분은 _코드 리뷰 (code review)_에서 환각이 어떤 모습으로 나타나는가 하는 점입니다. 그 형태는 다르며, 어쩌면 더 위험할 수도 있습니다. 왜냐하면 모델이 여러분이 실행할 코드를 작성하는 것이 아니라, 여러분이 행동에 옮길 _단언 (assertions)_을 하고 있기 때문입니다.

리뷰 모델은 세 가지 뚜렷한 방식으로 환각을 일으킬 수 있으며, 각각의 방식은 서로 다른 방식으로 피해를 줍니다.

유령 참조 (Phantom references). 리뷰어가 다음과 같이 코멘트를 남깁니다: "이것은 우리가 이미 이 케이스를 처리하고 있는 src/auth/middleware.py의 패턴과 유사합니다." 여러분이 src/auth/middleware.py를 열어봅니다. 그런 패턴은 없습니다. 심지어 해당 경로에 파일이 없을 수도 있습니다. 모델은 자신의 학습 분포 (training distribution)가 보상하는 방식에 따라 그럴듯한 참조를 지어낸 것입니다.

확인만 한다면 이를 찾아낼 수 있지만, 위험한 버전은 참조가 거의 실제와 같을 때입니다. 파일이 존재하고, 함수 이름은 비슷하지만 약간 틀리며, 패턴은 유사하지만 동일하지는 않은 경우입니다. 여러분은 코드를 훑어보고 _"그래, 일관성 있어 보이네"_라고 판단한 뒤 머지 (merge)합니다. 리뷰어가 여러분에게 완전히 사실이 아닌 내용을 사실인 것처럼 설득해 버린 것입니다.

유령 보증 (Phantom guarantees). 리뷰어가 다음과 같이 코멘트합니다: "여기서는 프레임워크가 입력 검증 (input validation)을 대신 처리해주므로, 명시적으로 추가할 필요가 없습니다." 때로는 이것이 사실일 수도 있습니다. 때로는 프레임워크에 _어느 정도_의 검증 기능은 있지만, 이 엔드포인트(endpoint)에 중요한 종류의 검증은 아닐 수도 있습니다. 때로는 프레임워크의 v3 버전에는 이 기능이 있었으나 현재 사용 중인 v5 버전에서는 삭제되었을 수도 있습니다. 때로는 프레임워크에 그런 기능이 아예 없는데도 모델이 다른 프레임워크의 문서와 혼동하고 있는 것일 수도 있습니다.

이제 당신은 검증하지 않은 시스템에 대한 단언 (assertion)에 의존하게 되었습니다. 디프 (diff)는 변경되지 않았습니다. 취약점은 이제 실시간으로 작동하게 되었으며, 이를 배포하도록 정당화한 논거는 리뷰 스레드에서 자신감 있게 들렸던 문장 하나뿐입니다.

유령 승인 (Phantom approvals). 리뷰어가 디프 (diff)를 읽다가 일부 구간을 이해하지 못하자 안전한 선택을 합니다. 즉, 해당 구간에 대해서는 아무런 언급을 하지 않고, 이해한 부분에 대해서만 긍정적인 내용을 말하는 것입니다. 전체적인 요약은 _"괜찮아 보입니다"_로 결론 납니다. 당신은 그 요약을 읽습니다. 그리고 디프 (diff)에서 가장 복잡한 부분이 실제로는 전혀 다뤄지지 않았다는 사실을 알아차리지 못합니다.

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0