이 글에서 다룰 이야기
AI한테 같은 작업을 두 번 시켜보면 재미있는 일이 생겨요. 똑같은 요청인데 코드가 매번 달라요. 어제는 통과하던 방식이 오늘은 미묘하게 다른 모양으로 나오고, 심지어 같은 대화 안에서도 방금 한 약속과 다른 코드를 내놓기도 해요.
이걸 비결정론적(non-deterministic) 이라고 불러요. 같은 입력을 넣어도 출력이 매번 같다는 보장이 없다는 뜻이에요. 반대로 같은 입력이면 항상 같은 출력이 나오는 걸 결정론적(deterministic) 이라고 해요.
문제는 검수예요. 출력이 매번 다른 걸 사람 눈으로만 검사하면, 사람도 매번 다른 걸 놓쳐요. 그래서 저는 반대 전략을 써요.
비결정론적인 생산자(AI)는 그대로 두고, 검사하는 쪽을 전부 결정론적인 장치로 바꿔요. 그리고 검사가 잘 되도록 작업을 잘게 쪼개요.
이 글에서는 제가 쓰는 frontend-oracle-design 워크플로우가 이 전략을 어떻게 구현하는지, 단계별로 하나씩 풀어볼게요. 철학적인 배경은 구현보다 정답 기준을 먼저 잠가요에서 다뤘고, 이 글은 실제 절차가 어떻게 굴러가는지에 집중해요.
먼저: 요리사는 매번 다른데, 온도계는 항상 같아요
비유 하나로 시작할게요.
솜씨 좋은 요리사를 고용했는데, 이 요리사는 같은 주문을 받아도 매번 조금씩 다르게 만들어요. 어떤 날은 훌륭하고, 어떤 날은 간이 이상해요. 요리 과정을 옆에서 지켜보는 걸로는 통제가 안 돼요. 과정이 매번 다르니까요.
그럼 어떻게 하냐면, 주방에 계기를 설치해요.
- 고기 중심 온도는 온도계가 재요 — 매번 같은 기준으로
- 굽는 시간은 타이머가 재요 — 매번 같은 기준으로
- 재료 양은 저울이 재요 — 매번 같은 기준으로
요리사가 어떤 순서로 뭘 하든, 접시가 나가기 전에 계기들을 전부 통과해야 해요. 계기는 기분이 없어요. 같은 접시를 두 번 재면 두 번 다 같은 값이 나와요. 이게 결정론적 검증이에요.
코드로 돌아오면 계기들은 이렇게 생겼어요.
| 계기 | 코드에서는 | 뭘 잡아주나요 |
|---|---|---|
| 온도계 | 타입 체크 (tsc) | 타입이 안 맞는 코드, 없는 필드 접근 |
| 저울 | 린트 (eslint) | 금지된 패턴, 위험한 관용구 |
| 타이머 | 테스트 (vitest, Playwright) | 요구사항과 다른 동작 |
| 검인 도장 | 구조 검사 스크립트 | 정답 표의 형식 위반, 순서 위반 |
| 봉인 스티커 | 해시(SHA-256) 잠금 | 합의한 기준이 몰래 바뀌는 것 |
여기서 중요한 포인트가 하나 있어요. 계기 하나로는 부족해요. 온도계는 간이 짠 걸 못 잡고, 저울은 덜 익은 걸 못 잡잖아요. 각 검증기는 잡는 결함의 종류가 달라서, 여러 겹으로 겹쳐야 구멍이 메워져요. 안전공학에서는 이걸 스위스 치즈 모델이라고 불러요 — 치즈 한 장에는 구멍이 있지만, 여러 장을 겹치면 구멍이 관통할 확률이 뚝 떨어진다는 얘기예요.
워크플로우 전체 지도
절차를 먼저 한눈에 볼게요. 크게 설계(Design) 와 전달(Delivery) 두 구간이에요.
[설계 구간]
1. 조사 — 정답을 정할 출처를 고정해요
2. 질문 — 애매한 경계를 미리 물어봐요 (BVA)
3. 정답 표 — Oracle Card로 요구사항을 행 단위로 적어요
4. 잠금 — 스크립트로 검사하고 해시로 봉인해요
[전달 구간]
5. 빨간불 — 테스트를 먼저 쓰고 "의도한 이유로" 실패시켜요 (VALID_RED)
6. 초록불 — 최소 구현으로 통과시켜요 (GREEN)
7. 훈련 — 일부러 버그를 심어 테스트가 잡는지 확인해요 (변이 테스트)
8. 재심 — 독립 리뷰어가 카드만 들고 다시 검사해요
그리고 전 구간을 관통하는 규칙이 둘 있어요. 관찰마다 원인을 하나씩 분류하는 라우팅과, 재시도 횟수의 상한(예산) 이에요. 이제 하나씩 볼게요.
1단계 — 조사: 재판 전에 증거부터 채택해요
첫 단계는 코드를 읽는 게 아니라 "정답을 정할 권한이 어디에 있는지" 를 고정하는 거예요.
재판을 생각해보면요, 본격적인 공방 전에 어떤 증거를 채택할지부터 정하잖아요. 채택 안 된 자료는 아무리 그럴듯해도 판결에 못 써요. 여기서도 똑같이 해요.
- 채택되는 것: 사용자의 명시적인 답변, 승인된 기획서·명세·디자인의 정확한 위치
- 채택 안 되는 것: 지금 돌아가는 제품 코드, 기존 테스트, "요즘은 다 이렇게 하던데요" 같은 베스트 프랙티스
지금 돌아가는 코드는 "지금 무슨 일이 일어나는가"를 알려줄 뿐, "무엇이 맞는가"를 알려주지 않아요. AI는 저장소를 다 읽을 수 있어서 현재 동작을 요구사항으로 착각하는 사고를 아주 자연스럽게 내거든요. 버그가 사양으로 승격되는 순간이죠.
출처끼리 서로 모순되거나 필수 자료에 접근이 안 되면? 그럴듯한 쪽을 고르지 않고 NEEDS_DECISION(결정 필요) 상태로 멈춰서 사람한테 물어봐요. 이 "멈춤"이 워크플로우 전체에서 계속 나올 거예요.
2단계 — 질문: 보험 특약처럼 경계를 미리 적어요
출처가 정해졌으면, 이제 애매한 경계를 찾아서 미리 물어봐요.
보험 계약이랑 비슷해요. "다치면 보장해드려요"라는 문장은 듣기 좋지만, 실제 분쟁은 전부 경계에서 나요. 출근길은 되나요? 자전거는요? 그래서 좋은 계약서는 특약으로 경계 사례를 미리 문장화해요.
요구사항도 똑같아요. "목록을 보여주세요"는 쉬운데, 사고는 경계에서 나요.
- 목록이 0개면요? 1개면요? 1만 개면요?
- 요청이 실패하면요? 5초씩 걸리면요?
- 같은 버튼을 연타하면요?
이렇게 값의 경계를 중심으로 케이스를 뽑는 기법을 경계값 분석(BVA, Boundary Value Analysis) 이라고 해요. 버그가 값 범위의 한가운데보다 가장자리에서 압도적으로 자주 나온다는 오래된 관찰에 기반한 기법이에요.
이 단계의 질문에도 상한이 있어요 — 최대 2라운드. 무한히 물어보면 진행이 안 되니까, 2라운드 안에 결과를 바꾸는 질문만 골라서 물어요.
3단계 — 정답 표: 채점 기준표를 먼저 만들어요
답이 모였으면 Oracle Card라는 표로 정리해요. 시험으로 치면 답안지가 아니라 채점 기준표(루브릭) 를 먼저 만드는 거예요.
| # | 이런 상황에서 (Given) | 이렇게 하면 (When) | 이래야 하고 (Then) | 절대 이러면 안 돼요 (Never) | 출처 |
| --- | --------------------- | ------------------ | ------------------ | --------------------------- | ------------ |
| O1 | 목록이 비어 있음 | 페이지 진입 | 안내 문구 노출 | 스피너가 계속 도는 상태 | 기획서 3.2절 |
| O2 | 요청이 5초 초과 | 자동 취소 | 재시도 버튼 노출 | 무한 대기 | 사용자 답변 |
| O3 | 같은 항목 연속 클릭 | 두 번째부터 무시 | 요청 1회만 발생 | 중복 요청 | 사용자 답변 |
각 행이 나중에 테스트 하나가 돼요. 그리고 출처 칸이 비면 그 행은 진행 못 해요. 출처 없는 행을 "보통 이렇게 하니까"로 채우는 순간, 검증이 추측으로 바뀌거든요.
표를 다 쓰면 스스로 한 번 공격해봐요. "이 표만 만족하면서도 여전히 틀린 구현이 가능한가?"를 따져보는 적대적 자가 리뷰예요. 가능하면 행이 모자란 거니까 더 채워요.
4단계 — 잠금: 계약서에 공증을 받아요
여기서부터 결정론적 장치가 본격적으로 등장해요. 사람이 쓴 표를 기계가 검사하고 봉인해요.
- 구조 검사 — 스크립트(
oracle-verify)가 표의 형식을 검사해요. 출처 없는 행, 검증 불가능한 문장 같은 걸 기계적으로 걸러요. 사람이 "대충 괜찮네"로 넘기는 걸 기계는 안 넘겨요. - 해시 봉인 — 표 파일의 SHA-256 해시를 기록해요. 해시는 내용이 한 글자만 바뀌어도 완전히 달라지는 지문 같은 거라서, 이후 매 단계 시작 전에 지문을 대조하면 표가 몰래 바뀌었는지 즉시 알 수 있어요.
종이 계약서에 공증받고 간인 찍는 것과 같아요. 간인(도장을 페이지에 걸쳐 찍는 것)이 있으면 나중에 페이지를 슬쩍 갈아끼우는 게 불가능해지잖아요.
지문이 안 맞으면 어떻게 하냐면 — 다시 잠가서 통과시키지 않아요. 그동안 모은 증거를 전부 폐기하고 1단계로 돌아가요. 검증을 통과시키려고 검증 기준을 바꾸면 그건 이미 검증이 아니니까요.
왜 이렇게까지 하냐면, 비결정론적인 작업자는 긴 작업 중에 기준 자체를 미묘하게 다시 해석하는 일이 잦거든요. 사람도 그래요. 해시는 그 재해석을 물리적으로 차단해요.
5단계 — 빨간불: 화재경보기부터 테스트해요
이제야 테스트를 써요. 순서가 중요해요 — 제품 코드보다 테스트가 먼저예요.
표의 각 행을 테스트로 옮기고, 실행해서 의도한 이유로 실패하는지 먼저 확인해요. 이 상태를 VALID_RED라고 불러요.
새 화재경보기를 달았다고 생각해보세요. 달자마자 하는 일이 뭐냐면 테스트 버튼을 눌러보는 거잖아요. 불이 났을 때 울리는지 확인도 안 한 경보기를 믿고 잘 수는 없으니까요. VALID_RED가 바로 그 테스트 버튼이에요. 아직 구현이 없으니 테스트는 당연히 실패해야 하고, 실패하는 이유가 "기능이 없어서" 인지 확인해요. 오타나 설정 문제로 빨간 거면 경보기 자체가 고장 난 거죠.
이 단계에서 결정론을 지키는 세부 규칙이 몇 개 있어요.
- 실행 기록은 장부(ledger)에 남아요. 테스트 실행은 전용 스크립트를 거치고, 결과가 추가 전용(append-only) 장부에 적혀요. 보고할 때는 "통과했어요"라는 말 대신 장부의 실행 번호를 인용해요. 장부에 없는 실행은 통과로 인정 안 해요. 말은 비결정론적이지만 장부는 결정론적이거든요.
- 상태 전이도 스크립트가 심판이에요. "테스트 없이 구현부터 했는지", "실패하는 테스트를 몰래 skip으로 돌렸는지" 같은 순서 위반을 전이 스크립트가 검사하고, 위반이면 전이 자체를 거부해요. 축구 심판이 기록지를 들고 있는 것과 같아요 — 선수가 "아까 골 넣었는데요"라고 우겨도 기록지에 없으면 골이 아니에요.
- 네트워크는 MSW로 막아요. 진짜 서버는 응답이 매번 다를 수 있는 비결정론 덩어리라서, 요청을 중간에서 가로채 정해진 응답을 주는 도구(MSW)로 바꿔요. 검증 대상이 아닌 곳의 비결정론을 제거하는 거예요.
6단계 — 초록불: 최소한만 구현해요
빨간불을 확인했으면 구현해요. 목표는 잠긴 표를 통과하는 최소한의 코드예요. 표에 없는 기능을 얹거나 "나중을 위한" 추상화를 만들지 않아요.
통과 못 하면 고쳐서 다시 시도하는데, 여기에도 예산이 있어요 — 구현 개선 최대 3라운드. 그리고 이 횟수를 머릿속으로 세지 않고 스크립트가 세요. "이번이 몇 번째더라?"는 비결정론적 작업자한테 맡기면 항상 후하게 세지거든요.
예산을 다 썼는데도 빨간불이면, 마지막 실패 내용과 함께 정직하게 실패로 보고하고 멈춰요. "될 때까지 해볼게요"보다 이게 빠르고, 무엇보다 실패가 데이터로 남아요.
7단계 — 훈련: 일부러 불을 내봐요
위험도가 높은 작업(결제, 권한 같은)에는 한 겹을 더 얹어요. 변이 테스트(mutation testing) 예요.
구현 코드에 일부러 작은 버그를 심어요. <를 <=로 바꾼다든가, 조건 하나를 뒤집는다든가요. 그리고 테스트를 돌려서 빨간불이 켜지는지 봐요. 켜지면 테스트가 그 종류의 버그를 잡을 수 있다는 증거고(변이를 "죽였다"고 표현해요), 안 켜지면 테스트에 구멍이 있다는 뜻이에요. 확인 후엔 버그를 원복하고 다시 초록불을 확인해요.
소방 대피 훈련이랑 같아요. 진짜 불이 나기 전에 가짜 상황을 만들어서 경보와 대피 절차가 작동하는지 확인하는 거죠. "경보기 달았으니 됐어요"와 "실제로 울리는 걸 확인했어요"는 완전히 다른 수준의 확신이에요.
8단계 — 재심: 다른 심판에게 카드만 줘요
마지막으로 독립 리뷰어를 붙여요. 포인트는 "독립"이에요.
자기가 짠 코드를 자기가 검토하면 같은 착각을 반복해요. 자기 글 오타를 자기가 못 찾는 것과 같아요. 그래서 메인 작업과 분리된 세션의 리뷰어에게 정답 표와 결과물만 줘요. "이거 하느라 고생한 사연"은 안 줘요. 사연을 들으면 리뷰어도 같이 설득당하거든요.
리뷰어가 지적을 하면 그걸 그냥 고치는 게 아니라, 다음 단계의 라우팅 표로 분류부터 해요. 그리고 유효한 지적을 반영한 뒤 테스트 전체를 다시 돌려서 초록불을 재확인해요. 이 재검증까지 끝난 상태(REVIEW_VERIFIED)가 이 워크플로우의 "완료"예요. 테스트 통과가 완료가 아니에요 — 완료를 리뷰 뒤로 옮겨두면, 테스트를 느슨하게 만들어서 이득 볼 일이 사라져요.
전 구간 공통 — 응급실 분류표와 알람 시계
위 단계들 사이사이에서 뭔가 이상한 게 발견되면, 반사적으로 코드를 고치지 않고 분류부터 해요. 응급실의 트리아지(중증도 분류)랑 같아요. 들어오는 순서대로 치료하는 게 아니라, 증상을 보고 정해진 경로로 보내는 거예요.
| 분류 | 무슨 뜻이냐면 | 가는 경로 |
|---|---|---|
POLICY_GAP | 정답 자체가 안 정해짐 | 멈추고 사람에게 질문 |
EVIDENCE_GAP | 정답은 있는데 확인을 안 함 | 잠긴 범위 안에서 테스트만 추가 |
HARNESS_DEFECT | 테스트 도구 문제 | 도구만 보정 (예산 2회) |
PRODUCT_DEFECT | 진짜 버그 | 빨간불 확인 후 구현 수정 (예산 3회) |
ENVIRONMENT_DEFECT | 환경 문제 | 코드 안 건드리고 실패 보고 |
NON_ORACLE_OPINION | 근거 없는 취향 | 기록만, 완료를 막지 않음 |
이 분류가 없으면 "테스트가 빨개졌으니 일단 제품 코드를 고친다"는 반사 행동이 나와요. 빨간 이유가 사실 도구 문제였는데 멀쩡한 코드를 고치는 사고, 생각보다 흔해요.
그리고 각 경로의 예산(질문 2라운드, 도구 보정 2회, 구현 3라운드)은 서로 빌려 쓸 수 없고, 스크립트가 세요. 알람 시계랑 같아요. "5분만 더"를 내 의지에 맡기면 반드시 늦잠 자니까, 기계한테 맡기는 거예요.
모듈화 — 검문소가 작동하려면 짐이 작아야 해요
여기까지가 검문소 얘기였다면, 이제 왜 작업을 잘게 쪼개는지 얘기할 차례예요. 결론부터 말하면, 검문소는 짐이 작을 때만 제대로 작동해요.
통관 검사를 생각해보세요
컨테이너 하나에 전자제품, 식품, 의류가 전부 섞여 들어오면 검사관이 뭘 어떻게 검사하겠어요. 열어보다가 지쳐서 "대충 괜찮아 보이네요"가 나와요. 반대로 품목별로 상자를 나눠서 보내면, 상자마다 해당 품목의 검사 기준을 정확히 적용할 수 있어요.
코드도 똑같아요. 500줄짜리 변경(diff)은 리뷰어(사람이든 기계든)를 지치게 하고, 테스트가 실패해도 어느 부분 때문인지 특정이 안 돼요. 50줄짜리 변경 열 개는 각각이 명확하게 판정돼요. 앞에서 만든 Oracle Card가 바로 이 "상자"예요. 카드 하나 = 작은 계약 하나 = 검수 가능한 단위 하나.
모든 모듈을 처음부터 만들지 않아요
여기서 진짜 중요한 원칙이 나와요. 잘게 쪼갠다고 해서 모든 조각을 매번 새로 만들고 매번 처음부터 검수하는 게 아니에요.
레고를 생각해보세요. 우주선을 만들 때 블록을 사출 성형부터 시작하는 사람은 없잖아요. 이미 규격이 검증된 블록을 가져다 조립하고, 새로 설계하는 건 이번 작품에만 필요한 특수 부품 몇 개뿐이에요. 검수도 그 새 부품에만 집중하면 돼요. 기존 블록은 규격(공개 API)만 맞는지 확인하면 끝이고요.
워크플로우에서도 똑같이 해요.
- 기존 컴포넌트·유틸·패턴을 먼저 찾아요. 저장소에 이미 있는 걸 다시 만드는 게 AI 코드의 가장 흔한 실패라서, 새로 쓰기 전에 검색부터 해요. 이미 있는 코드는 이미 검수를 통과한 블록이에요.
- 검증 파이프라인도 재활용해요. 타입 체크, 린트, 테스트 러너, CI — 이미 깔려 있는 검문소를 그대로 써요. 작업마다 새 검증 도구를 발명하지 않아요. 새로 만드는 건 이번 작업의 정답 표와 그 표의 테스트뿐이에요.
- 지금 해결할 문제에만 집중해요. "나중에 쓸지도 모르는" 추상화나 설정은 안 만들어요. 그건 검수할 계약(카드)이 없는 코드라서, 검문소를 그냥 통과해버리는 짐이거든요.
쪼개진 조각은 소유 경계와 같이 살아요
쪼갠 조각들을 어디에 두는지도 규칙이 있어요. 테스트와 가짜 서버 응답(MSW 핸들러)은 그 기능을 소유한 폴더 옆에 둬요. 루트에 e2e/, mocks/ 폴더를 만들어 전부 모으면 편해 보이지만, 나중에 기능을 지울 때 테스트가 같이 안 지워져서 누구 것인지 모르는 죽은 검사가 쌓여요. 기능과 테스트가 같이 태어나고 같이 죽게 두는 게 모듈화의 완성이에요.
정리 — 전략은 한 문장이에요
출력이 매번 다른 생산자를 통제하려면, 생산자를 고치려 하지 말고 판정하는 쪽을 전부 기계로 바꾸고, 판정 가능한 크기로 일을 쪼개요.
단계별로 다시 줄이면 이래요.
| 단계 | 하는 일 | 결정론적 장치 |
|---|---|---|
| 1. 조사 | 정답 출처 고정 | 출처 없는 정답 금지 규칙 |
| 2. 질문 | 경계값 미리 문장화 | BVA, 질문 예산 2라운드 |
| 3. 정답 표 | 행 단위 채점 기준표 | Given/When/Then/Never + 출처 |
| 4. 잠금 | 표를 기계 검사·봉인 | 구조 검사 스크립트, SHA-256 |
| 5. 빨간불 | 테스트 먼저, 실패 확인 | VALID_RED, 실행 장부 |
| 6. 초록불 | 최소 구현 | 전이 스크립트, 예산 3라운드 |
| 7. 훈련 | 가짜 버그로 테스트 검증 | 변이 테스트 |
| 8. 재심 | 독립 리뷰 + 재검증 | 카드만 주는 분리 세션 |
그리고 모듈화는 이 표 전체가 작동하기 위한 전제 조건이에요. 검수 단위가 작아야 각 장치의 판정이 선명해지고, 이미 검증된 블록을 재활용해야 새로 검수할 면적이 줄어들거든요.
AI가 더 좋아지면 이런 절차가 필요 없어질까요? 저는 반대로 생각해요. 생산 속도가 빨라질수록 틀린 것도 빨리, 많이 만들어져요. 그럴수록 기분이 없는 계기들이 더 필요해져요.