
AI 리뷰는 한 번으로 충분할까 ― 동일한 조건으로 다시 실행했을 때 | Claude × Roblox 개인 개발
요약
개인 개발 과정에서 Claude와 Gemini CLI를 활용하여 코드 리뷰 및 설계 사양서 작성 워크플로우를 진행하고 있습니다. 모델명 지정의 중요성을 다루며, 실제로 표시되는 모델명과 실제 세션 로그에 기록된 모델명이 다를 수 있음을 발견했습니다. 이는 AI 도구 사용 시 UI 표시에만 의존해서는 안 되며, 반드시 상세한 로그 확인이 필요함을 강조합니다.
핵심 포인트
- AI 개발 워크플로우: Claude/Gemini로 설계 및 구현 진행
- 모델명 표시와 실제 세션 로그가 다를 수 있음 (오류 지적)
- UI에 보이는 모델명만 믿지 말고, 반드시 상세 로그를 확인해야 함
- 잘못된 폐기된 모델명을 지정해도 에러 없이 동작할 수 있어 주의 필요
1. 서론
Roblox 『Wobble Survival: Don't Fall!』를 개인 개발하고 있습니다. 워크플로우는 「Claude.ai로 설계·사양서 작성 → Claude Code로 구현」입니다.
지난번에는 Gemini CLI와 Claude Code에게 서로의 리뷰 지적 사항을 비평하게 했던 이야기를 썼습니다.
이번에는 그 후속 이야기입니다. 당초 계획했던 것은 모델을 명시적으로 지정하여 동일한 리뷰를 다시 실행하는 것이었습니다. 지난번 글에서 "숙제로 남아 있습니다"라고 썼던 재시험입니다.
결론부터 말하자면, 재시험은 엄격한 비교가 되지 않았습니다. 리뷰를 받고 코드를 수정했기 때문에, 전제 조건이 변해 있었기 때문입니다.
그래서 조건을 맞춘 다른 실험으로 전환했더니, 그쪽에서 예상치 못한 결과가 나왔습니다. 릴리스를 중단해야 할 수준의 버그입니다.
2. 그 전에 ― 모델명에 관한 이야기
재시험 이야기를 하기 전에 정정할 내용이 있습니다.
제6탄에서 "Gemini CLI가 Auto 선택으로 되어 있었고, 실제로 동작하고 있었던 것은 gemini-3.5-flash였다"라고 썼습니다. 이것은 오류였습니다.
나중에 세션 로그를 정밀 조사한 결과, 리뷰 본체를 생성하고 있었던 것은 gemini-3.1-pro-preview-customtools였습니다. gemini-3.5-flash가 응답했던 것은 "사용 중인 모델명을 알려줘"라는 질문에 대한 인사 1건뿐이었으며, 리뷰에는 관여하지 않았습니다.
모델명을 물었을 때의 응답이 Flash 계열이었던 것을, 리뷰 본체도 그럴 것이라고 잘못 읽었습니다. 로그를 열어놓고도 읽는 방식이 허술했습니다.
그리고 그 질문에 대한 답변 자체는 다음과 같았습니다.
안녕하세요! 저는 Google의 Gemini 모델입니다. 오늘은 어떤 도움을 드릴까요?
식별자를 대답하지 않았습니다.
애초에 Gemini CLI의 Auto 선택은 태스크에 따라 모델을 다시 선택합니다. 실제로 이 세션의 로그에는 두 개의 모델명이 기록되어 있었습니다. 가벼운 응답과 코드 리뷰에 대해 서로 다른 모델이 할당되었던 것으로 보입니다.
즉, "이 세션의 모델은 무엇인가"라는 질문 자체는 큰 의미가 없습니다. 응답마다 다를 가능성이 있습니다. 그래서 로그를 확인할 수밖에 없는데, 거기서 잘못 읽은 것입니다.
명시적으로 지정하면 안전할 것이라고도 할 수 없었다
제6탄의 결론은 "모델은 명시적으로 지정하고, 사용 모델을 기록으로 남긴다"였습니다. 이번에는 그 방식대로 -m 옵션으로 지정하여 실행했습니다.
Gemini CLI는 실행하면 입력창 아래에 모델명을 상시 표시합니다. 지정한 것이 그대로 나타나 있었습니다.
workspace (/directory) branch sandbox /model
H:\...\wobble-survival main no sandbox gemini-3-pro-preview
/model 명령어로 확인해도 마찬가지입니다.
● 2. Manual (gemini-3-pro-preview)
표시되고 있으니, 이것으로 동작하고 있다고 생각했습니다.
그런데 나중에 로그를 보니 기록된 모델명이 달랐습니다.
| 위치 | 문자열 |
|---|---|
| 실행 시 표시 | gemini-3-pro-preview |
/model 표시 | gemini-3-pro-preview |
| 세션 로그 | gemini-3.1-pro-preview |
조사해 보니, gemini-3-pro-preview는 2026년 3월 9일에 API에서 폐지되었습니다. 5개월 전에 사라진 이름을 지정하고 있었던 셈입니다.
하지만 에러는 발생하지 않았습니다. 실행되었고, 리뷰가 돌아왔습니다. 그리고 UI는 두 곳 모두 지정한 문자열을 그대로 계속 표시합니다.
화면을 보고 있는 한, 교체되었다는 사실을 알아차릴 수 없습니다.
그 로그가 사라졌다
여기서부터가 본론입니다.
위의 gemini-3.1-pro-preview라는 기록을 확인한 며칠 후, 동일한 로그 파일이 사라져 있었습니다.
Windows의 검색 인덱스에는 흔적이 남아 있어 파일이 존재했다는 것은 알 수 있습니다. 하지만 실체는 없으며, 휴지통에도 없습니다.
Gemini CLI의 세션 로그에는 보유 정책(retention policy)이 있으며, 기본값은 30일입니다. 하지만 오래된 것부터 삭제되는 방식이기 때문에, 이로는 설명이 되지 않습니다. 실제로 그보다 2주 더 오래된 7월 16일의 로그는 남아 있습니다. 사라진 것은 7월 30일의 로그였습니다.
원인은 특정하지 못했습니다. 시계열상으로는 CLI의 자동 업데이트가 포함되어 있었지만, 확인할 수 있는 수단이 없습니다.
말할 수 있는 것은 이것뿐입니다. 확인했을 때는 분명히 그렇게 기록되어 있었다. 그리고 지금은 남아 있지 않다.
3. 엄격한 재시험은 되지 않았다
본론인 재시험입니다.
하고자 했던 것은 "모델을 바꾸면 지적의 질이 변하는가"의 확인이었습니다. 동일한 체크리스트, 동일한 파일, 동일한 프롬프트로 실행합니다.
결과는 5건의 지적. 1회차와 비교하려던 찰나, 단순한 비교는 불가능하다는 것을 깨달았습니다. 리뷰를 받고 코드 쪽을 수정했기 때문입니다.
변한 점은 두 가지입니다.
1. 설계 의도에 대한 코멘트를 추가했다
제7탄에서 썼듯이, 간판의 거리 판정에 "왜 이렇게 구현했는지"를 덧붙여 두었습니다. 1회차 리뷰 시점에는 존재하지 않았던 기술입니다.
2. 798행을 삭제했다
제6탄에서 썼던 데드 코드(dead code) 삭제입니다. 1회차에서 데드 코드로 지적된 부분은 이제 존재하지 않습니다.
모델 차이를 엄격하게 측정하고 싶었다면, 동일한 커밋(commit)의 파일에 대해 실행했어야 했습니다. 리뷰 $\rightarrow$ 수정 $\rightarrow$ 리뷰라는 흐름으로 개발하다 보면, 자신도 모르는 사이에 전제 조건이 움직이게 됩니다.
다만, 변화의 효과는 보인다
물론, 비교의 축을 바꾸면 읽어낼 수 있는 것도 있습니다.
간판에 대한 오지적은 이번에 나오지 않았습니다.
1회차에서는 "RenderStepped의 거리 판정은 중복이며, MaxDistance로 대체할 수 있다"라는 지적이 확신도 "높음"으로 나왔었습니다. 제7탄에서 썼던, 의도적인 구현을 "중복"이라고 판정된 건입니다. 그 부분에 코멘트를 덧붙였습니다.
그리고 이번에는 같은 부분에 대한 지적이 없습니다. 덧붙인 코멘트가 효과가 있었을 가능성이 있습니다.
다만 모델도 다르기 때문에, "코멘트 덕분"이라고 단정할 수는 없습니다. 말할 수 있는 것은, 의도를 덧붙인 부분에 대해 적어도 동일한 오지적이 반복되지 않았다는 것입니다.
데드 코드 삭제 쪽은 영향 파악이 쉽습니다. 이번 지적은 매 프레임 처리의 GC(Garbage Collection) 부하 나 아바타 의존 값의 하드코딩 등, 실행 시 동작(runtime behavior)에 가까운 것이 중심이었습니다. 데드 코드가 사라진 이상, 다음에 검출되는 것이 다른 종류가 되는 것은 당연합니다. 이 부분은 "모델이 바뀌어서 성질이 변했다"라고 말할 수 없습니다.
4. 행 번호는 모델에 따라 믿을 수 없다
비교로서는 엄격하지 않게 되었지만, 단일 관측으로서 얻은 것은 있습니다.
제6탄에서 "행 번호는 신뢰할 수 없다"라고 쓰고, 체크리스트에 인용 문자열(quoted string)을 필수로 하는 개정을 넣었습니다. 그 효과가 나타나고 있습니다.
| 항목 | 결과 |
|---|---|
| 인용 문자열 | 5건 모두 완전 일치. 존재하지 않는 기술은 제로 |
| 행 번호 | 5건 모두 부정확 |
어긋난 폭은 -68 / -160 / -283 / -290 이었습니다. 전 건이 같은 방향으로 어긋나 있었으며, 파일 뒷부분일수록 커지는 단조 드리프트(monotonic drift)였습니다.
중요한 것은, 1회차와 어긋나는 방식이 다르다는 점입니다. 1회차는 "일률적으로 57행 어긋남"이 2건, "563행 어긋남"이 1건인 구성이었습니다. 이번에는 단조 드리프트 1계통입니다.
즉, 어긋나는 방식은 실행할 때마다 달라집니다. 일정한 오프셋(offset)이라면 보정해서 사용할 방법도 있겠지만, 그것도 불가능합니다.
다만, 이것은 모델 차이가 있다
만약을 위해 적어두자면, 이것은 Gemini CLI 측의 이야기입니다. 같은 파일을 보여준 Claude Code의 행 번호는 정확했습니다. 제6탄에서 검증했듯이, 코멘트 블록의 종료 행은 모두 실측치와 일치했으며, 시작 행의 차이도 범위 지정에 의한 것이라 실질적인 해는 없었습니다.
동일한 조건에서 한쪽은 되고 한쪽은 되지 않았습니다. 원인은 특정하지 못했습니다. 알 수 있는 것은, 행 번호의 정밀도는 모델(혹은 도구)에 따라 차이가 있다는 것입니다.
그리고 인용 문자열은 100% 일치했습니다. grep을 돌리면 단번에 실제 위치가 나옵니다.
어느 모델을 사용하든, 행 번호를 믿지 않고 인용 문자열로 대조하는 운용을 해둔다면 이 차이는 문제가 되지 않습니다. 지난번의 개정은 정답이었습니다.
5. 조건을 맞춰서 다시 한번
엄격한 비교가 불가능하다는 것을 알게 되었으므로, 설계를 변경했습니다.
동일한 모델, 동일한 파일, 동일한 체크리스트로 2회차를 실행한다. 바꾸는 것은 "2회차라는 점"뿐입니다. 이렇게 하면 전제 조건이 변하지 않습니다.
질문도 달라집니다. 모델 간의 우열이 아니라, AI 리뷰는 한 번으로 충분한가입니다.
대상은 이미 리뷰 2회와 상호 비평을 거쳐, 798행을 깎아내고 9건을 Issue로 격리한 파일입니다. 모든 것을 다 파헤쳤다고 생각했습니다.
실험 설계에서 주의한 점은 3가지입니다.
새로운 세션에서 실행한다. 직전 세션은 GitHub Issue를 읽은 상태입니다. 그대로 계속하면 정답을 본 상태에서의 테스트가 됩니다.
Issue 정보를 일절 전달하지 않는다. 지시서에도 쓰지 않습니다. gh issue list 실행도 금지했습니다.
지시서의 순서를 고정한다. 실험의 취지를 말미에 배치하고, "리뷰 출력을 마칠 때까지 읽지 마시오"라고 명시했습니다. 목적을 먼저 알게 되면 "무언가를 찾는 테스트"라는 것을 눈치채게 되어, 탐색 방식이 바뀔 가능성이 있기 때문입니다.
이에 더해, 대조 작업을 하기 전에 "각 Issue가 정적 분석 (Static Analysis)으로 검출 가능한가"의 분류를 확정하는 절차를 넣었습니다. 나중에 "이것은 검출 불가능했으므로 분모에 넣지 않겠다"라며 조정하는 것을 방지하기 위해서입니다.
6. 2회차에서 나타난 릴리스 블로커 (Release Blocker)
결과적으로 20건의 지적이 돌아왔습니다. 기지(Known)의 9건과 대조하면 다음과 같습니다.
| 지표 | 결과 |
|---|---|
| 기지 9건의 재검출 | 4건 |
| ... |
신규 항목이 더 많습니다. 그리고 그중에 이것이 있었습니다.
GameConfig.DEBUG가 true인 상태 그대로.
서버 측의 DebugAddCoins 핸들러는 검증 없이 addCoins(player, 1000)를 실행하기 때문에, 이대로 출시하면 모든 플레이어가 M키를 연타하여 코인을 무한으로 입수할 수 있습니다.
디버그용으로 만든 코인 지급 기능이, 플래그를 true인 채로 공개되는 상태였습니다. 게다가 서버 측에 검증이 없으므로 클라이언트를 개조할 필요조차 없습니다.
순정 Roblox 클라이언트로 M키를 누르기만 하면 됩니다.
코인은 게임 내 경제의 중심이며 유료 통화와도 연결됩니다. 이것은 출시를 중단시켜야 할 버그였습니다.
그리고 중요한 점은, 아무도 이것을 발견하지 못했다는 것입니다.
| 리뷰 | 검출 |
|---|---|
| 1회차 (Gemini CLI) | ❌ |
| ... | 2회차 재실행 (Claude Code) |
4번의 리뷰를 빠져나갔습니다. 게다가 동일한 모델의 1회차에서도 놓치고 있었습니다.
수정 내용
대응은 2단계로 진행했습니다.
먼저 서버 측에 가드 (Guard)를 설치합니다. 핸들러의 서두에서 Studio 내부인지 판정하고, 그렇지 않다면 경고를 출력한 뒤 즉시 종료하는 형태입니다.
이것이 본질적인 방어입니다. 클라이언트 측의 플래그는 개조할 수 있고, RemoteEvent는 누구나 직접 발화할 수 있습니다. 플래그를 되돌리는 것을 잊더라도, 운영 환경이라면 차단되는 구조로 만들었습니다.
그 위에, 플래그 자체를 분리했습니다.
| 플래그 | 제어 대상 | 기본값 |
|---|---|---|
GameConfig.DEBUG | 게시판의 더미 표시 등 무해한 개발 지원 | true |
GameConfig.DEBUG_GRANT_COINS | 코인 지급 | false |
원래의 문제는 위험한 기능과 무해한 기능이 동일한 스위치로 관리되고 있었다는 점이었습니다. DEBUG = false로 통일하는 안도 있었지만, 그렇게 하면 개발 시에 수동으로 true로 되돌리는 운용이 됩니다. 이번과 같은 사고를 재생산하게 됩니다.
위험한 것만 격리하여, 상시 OFF를 기본값으로 설정했습니다.
7. 숫자가 보여주는 것
다시 숫자를 보겠습니다.
- 기지 9건 중,
재검출은 4건 - 대신
신규 15건, 그중 실질적 피해 4건
놓치는 부분이 한 방향이 아니라 양방향으로 일어나고 있습니다. 1회차가 찾은 것을 2회차가 놓치고, 2회차가 찾은 것을 1회차가 놓치고 있습니다.
1회의 실행은 커버리지 (Coverage)의 상한을 보장하지 않습니다.
그리고 누락되는 방식에는 경향이 있었습니다. 미검출된 항목의 상당수는 "코드의 내부 정합성은 맞지만, 외부 지식을 대조해 보지 않으면 걸리지 않는" 유형입니다. 권장되지 않는 API (Deprecated API)의 검출, Roblox의 사양 지식에 의존하는 것들. 이번 실행은 "파일 내 대칭성 결여"를 찾아내는 방향으로 치우쳐 있었고, 그 축이 통째로 누락되어 있었습니다.
무작위로 누락되는 것이 아니라, 축 자체가 통째로 빠지는 것입니다. 그렇다면 동일한 프롬프트로 두 번 실행하는 것보다, "권장되지 않는 API (Deprecated API)를 조사하기", "Instance의 라이프사이클을 조사하기"와 같이 관점을 분할하여 실행하는 것이 더 효율적일지도 모릅니다. 이는 가설이며, 아직 시도해 보지는 않았습니다.
8. 어디에서 멈출 것인가
실행할 때마다 새로운 것이 나온다면 끝이 없습니다. 실제로 두 번째 실행에서 신규 15건이 발견되었습니다. 세 번째를 실행하면 또 다른 것이 나올 것입니다.
멈추는 시점은 **실질적인 피해 (Real harm)**를 기준으로 결정했습니다.
| 구분 | 내용 | 판단 |
|---|---|---|
| 릴리스 블로커 (Release Blocker) | 진행 불가 · 데이터 손상 | 즉시 |
| ... | ||
| 이 기준으로 분류하여, 7건을 수정하고 push했습니다. 나머지는 이슈 (Issue)에 쌓아두었습니다. |
선을 그을 때 한 가지 정한 것이 있습니다. "지금은 일어나지 않으니까 대처하지 않는다"라는 판단을 방어 (Defense)에 대해서는 사용하지 않는다는 것입니다.
이 게임은 향후 맵을 추가할 예정이며, Studio에서의 배치는 계속 변할 것입니다. "현재 배치에서는 일어나지 않는다"는 "앞으로도 일어나지 않는다"를 의미하지 않습니다.
실제로 입구 표지판을 생성하는 코드는 대상 어트랙션을 직접 작성(Hard-coding)하여 열거하고 있었습니다. 맵을 추가해도, 표지판만 만들어지지 않습니다. 게다가 미끄러짐 처리나 승선은 작동하기 때문에 경고도 뜨지 않아 알아차리기 어렵습니다. "지금은 3개뿐이니까"라며 방치했던 부분입니다.
YAGNI를 어디에 적용할 것인가
"You Aren't Gonna Need It" —— 지금 필요하지 않은 것을 만들지 말라는 원칙이 있습니다. 저도 이 원칙에는 찬성하지만, 이번 일을 계기로 적용 범위를 다시 생각하게 되었습니다.
YAGNI가 지키고 있는 것은 "기능"입니다. 사용할지 모를 기능에 개발 리소스를 쏟아붓지 말라는 이야기이지, 사고에 대한 방어까지 대상으로 삼으면 단순히 취약한 코드를 정당화하는 꼴이 됩니다.
입구 표지판의 동적화나 대기 처리 시 타임아웃 부여도 사용자에게 제공하는 기능이 아닙니다. 배치 실수에 대한 방어입니다. 이 부분을 "지금은 3개뿐이니까"라며 넘긴 것이 이번 판단 미스였습니다.
또 하나, 비용의 전제도 바뀌었습니다. YAGNI가 논의되어 온 맥락에서 구현 비용은 인간의 시간이었습니다. "사용할지 모르는 것에 며칠씩 쓰지 마라"는 당연합니다. 하지만 AI가 구현한다면, 방어적인 몇 줄의 코드는 몇 분이면 충분합니다. 저울의 한쪽 면이 거의 제로가 되었는데, 원칙만 당시의 가중치 그대로 남아 있는 것입니다.
그리고 까다로운 점은, AI 스스로 YAGNI를 들고 나온다는 것입니다. 자신이 몇 분이면 쓸 수 있는 코드에 대해, 인간의 시간을 전제로 한 원칙을 내세우며 "아직 필요하지 않으므로 구현하지 않겠습니다"라고 말합니다. 그 상황에서는 항상 옳게 들리기 때문에 반론하기 어렵습니다.
그래서 기준을 정했습니다.
| 유형 | YAGNI 적용 |
|---|---|
| 기능의 추가 · 확장 | 적용해도 좋다. 나중으로 미룰 수 있음 |
| 배치 실수 · 조작 실수에 대한 방어 | 적용하지 않는다. 저렴하다면 넣는다 |
| 가독성 · 유지보수성 개선 | 적용해도 좋다. 나중으로 미룰 수 있음 |
그리고 보류하기로 결정했다면, 이유를 코멘트로 남기기로 했습니다. YAGNI는 "쓰지 않는 것"은 결정하지만, "쓰지 않은 이유를 남기지 않는 것"까지 요구하지는 않습니다. 기록이 없으면 다음 리뷰에서 똑같은 지적이 다시 나옵니다.
실제로 실행 시점에 어트랙션을 삭제하는 경로가 존재하지 않는 코드에 대해, 두 개의 모델이 독립적으로 동일한 오지적을 했습니다. 코드만 보면 누구나 같은 결론에 도달한다는 뜻입니다. 그곳에는 "삭제하는 경로가 존재하지 않으므로 불필요"라고 덧붙였습니다.
9. 요약 / 다음 예고
동일한 코드를 다시 한번 리뷰시킨 결과입니다.
1회로는 부족하다. 기지(Known) 9건 중 재검출은 4건. 대신 신규 15건, 그중 실질적 피해 4건이 나왔다. 놓치는 것은 양방향으로 일어난다 -
4번의 리뷰를 뚫고 지나간 릴리스 블로커가 있었다. 디버그용 코인 지급 기능이 플래그를 true
그대로 출하되는 상태였습니다.
모델 간 차이를 측정하려면 코드를 고정해야 합니다. 리뷰 → 수정 → 리뷰를 진행하면 비교의 전제가 흔들립니다.
행 번호의 정밀도에는 모델 간 차이가 있습니다. 한쪽은 어긋나고, 다른 쪽은 정확했습니다. 어긋나는 방식조차 실행마다 달라집니다. 인용 문자열로 대조하면 이 차이는 문제가 되지 않습니다.
모델명 확인은 생각보다 어렵습니다. 물어봐도 답하지 않고, UI 표시와 실제가 다르고, 로그는 사라질 때가 있습니다.
멈출 시점은 실질적인 피해(실해)로 결정합니다. 모든 것을 하려고 하면 끝나지 않습니다.
YAGNI(You Ain't Gonna Need It)는 기능에 적용하고 방어에는 적용하지 않습니다. AI가 구현한다면 비용의 전제도 바뀝니다. 보류할 거라면 이유를 남깁니다.
‘여러 모델로 리뷰하면 정확도가 높아질 것’이라고 생각하며 시작했지만, 실제로 측정해 보니 조금 다른 것이었습니다. 정확도가 높아지기 때문이 아니라, 한 번의 실행이 커버리지의 상한을 주지 못하기 때문에 여러 번, 여러 모델로 돌립니다. 수집된 지적 사항을 그대로 적용하는 것이 아니라, 사양과 대조하여 확인하고 거기서부터 수정할 부분을 찾아냅니다. 이 확인 과정은 인간의 일입니다.
AI 리뷰 이야기는 여기서 잠시 멈춥니다. 세 번에 걸쳐 썼지만, 결국 AI에게 형식적인 검토를 맡기고 의도 판정은 스스로 책임진다는 제6차 가이드라인이 그대로 끝까지 유효했습니다.
시간이 지나 모델 성능이 올라가면 다시 시도해 볼 생각입니다. 이번에도 같은 모델의 두 번째 실행에서 미지의 블로커가 나오고 있으니, 성능이 바뀌면 또 다른 축이 포착될 것입니다.
다음 편에서는 추(진자)를 어떻게 흔들 것인가에 대한 이야기를 쓸 예정입니다. 이 게임은 추로 흔들리는 이동 수단 위에 계속 서 있는 게임인데, 세 종류의 어트랙션 각각에 다른 흔들림 방식을 적용했습니다. 여기에 안착하기까지 두 번 실패했습니다. 첫 번째는 움직임이 너무 기계적이었고, 두 번째는 몇 초 만에 폭주했습니다.
Discussion

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