버그를 잡는 일은 탐정 일과 닮았다.
문제는 범인을 찾는 것만으로 끝나지 않는다는 점이다. 탐정이 아무리 멋지게 추리해도, 법정에 가면 결국 이렇게 물어본다.
“증거는 어디 있죠?”
개발에서도 똑같다.
“패치했습니다.”
좋다.
“테스트도 통과했습니다.”
더 좋다.
그런데 조금 더 까다로운 질문이 남는다.
“실제 화면에서도 재현됐나요?”
“패치 전 실패와 패치 후 성공이 같은 조건인가요?”
“재현하려고 임시로 바꾼 값은 원래대로 돌렸나요?”
“보고서에 링크한 로그 파일은 진짜 존재하나요?”
“토큰이나 비밀값이 증거 파일에 남아 있지는 않나요?”
이번 ReproKit 개선은 이 질문들에 답하기 위한 작업이었다.
한 줄로 말하면, ReproKit이 단순히 “증거를 모으는 도구”에서 “증거 봉투를 검사하는 도구”로 한 단계 진화했다.
원래 ReproKit은 꽤 잘하고 있었다
ReproKit은 QueryPie 이슈를 재현하고 검증하기 위한 작은 프레임워크다.
구조는 세 계층으로 나뉜다.
Repro Core
Evidence Capture
Result Package
역할은 간단하다.
Repro Core 이슈를 재현하고 검증한다
Evidence Capture 실제 UI나 터미널 증거를 수집한다
Result Package 보고서와 증거 파일을 묶는다
여기까지도 충분히 유용했다.
예전에는 티켓마다 재현 스크립트 이름도 다르고, 어떤 명령이 데이터를 바꾸는지, 어떤 파일이 최종 증거인지 매번 추적해야 했다. ReproKit은 이 흐름을 표준화했다.
그런데 이번 Query Audit 중복 row 이슈를 처리하면서 빈틈이 하나 보였다.
보고서에는 실제 UI 스크린샷과 터미널 로그가 있었다. 하지만 verifier가 처음에 잡은 artifact는 스크린샷 2개뿐이었다.
터미널 transcript와 JSON 증거는 보고서에 적혀 있었지만, 검증기가 충분히 똑똑하게 주워 담지 못했다.
사람이 보면 알 수 있다.
하지만 ReproKit의 목표는 “사람이 열심히 눈으로 확인해야 하는 일”을 줄이는 것이다.
그래서 질문을 바꿨다.
“보고서가 PASS라고 말하는가?”가 아니라,
“PASS라고 말할 자격이 있는 증거 봉투인가?”
증거 봉투에는 네 가지 라벨이 필요했다
이번 개선에서 가장 중요한 결정은 전용 모듈을 만들지 않는 것이었다.
처음에는 유혹이 있었다.
scanner_timeout_repro.py
query_audit_fixture.py
duplicate_row_detector.py
이름만 보면 멋있다. 하지만 너무 특정적이다. QCP 하나를 위해 전용 모듈을 너무 많이 만들면 ReproKit은 금방 도구 창고가 된다. 도구 창고는 처음엔 든든하지만, 시간이 지나면 어디에 뭘 뒀는지 모르게 된다.
그래서 이번에는 더 일반적인 구조를 골랐다.
reproductionControls
fixtures
evidencePairs
cleanupChecks
이 네 가지는 특정 이슈에 묶이지 않는다.
어떤 이슈든 재현하려면 임시 장치가 있을 수 있고, 테스트 데이터가 있을 수 있고, 패치 전후 증거가 있고, 마지막에는 정리가 필요하다.
reproductionControls: 실험실 조명을 켰다면 기록하자
버그 재현을 하다 보면 가끔 실험 조건을 안정화해야 한다.
예를 들어 timeout branch를 실제 UI에서 캡처하고 싶은데, 매번 진짜로 5초 이상 걸리는 쿼리를 기다리면 재현이 흔들린다.
그래서 로컬에서만 timeout 기준을 아주 작게 낮출 수 있다.
이건 제품 패치가 아니다.
증거 사진을 잘 찍기 위해 조명을 켠 것이다.
문제는 이 조명을 켰다는 사실과, 촬영 후 껐다는 사실을 남겨야 한다는 점이다.
그래서 reproductionControls를 만들었다.
예를 들면 이런 정보가 들어간다.
{
"name": "force-timeout-branch",
"type": "temporary-source-mutation",
"scope": "local UI reproduction only",
"includedInProductPatch": false,
"restored": true
}
이제 보고서를 보는 사람은 바로 알 수 있다.
“아, 이 변경은 제품 패치가 아니라 재현을 안정화하기 위한 임시 장치였고, 끝나고 원복됐구나.”
이 작은 구분이 중요하다.
재현 장치와 제품 패치가 섞이면 검증이 흐려진다.
fixtures: 화면에 나온 데이터는 어디서 왔나
실제 UI 증빙에는 데이터가 필요하다.
이번 Query Audit 케이스에서는 로컬 DB에 식별 가능한 로그를 넣었다.
QCP-4731 runtime evidence query #01
QCP-4731 runtime evidence query #02
...
이런 fixture는 아주 유용하다. 실제 QueryPie UI에서 “아, 이 row가 이번 재현용 데이터구나”를 바로 알 수 있기 때문이다.
하지만 fixture도 기록되어야 한다.
몇 건을 넣었는지, 어떤 prefix를 썼는지, 실제 UI에서 보이는지, cleanup이 필요한지.
그래서 fixtures를 추가했다.
{
"name": "query-audit-runtime-evidence-rows",
"type": "local-db-seed",
"rowCount": 16,
"visibleInActualUi": true
}
이건 Query Audit에만 쓰이는 구조가 아니다.
Data Policy, User Group, DB Connection, Slack DM, Proxy target DB 같은 다른 재현에도 그대로 쓸 수 있다.
evidencePairs: before와 after를 한 쌍으로 묶자
패치 검증에서 가장 중요한 이야기는 늘 이것이다.
전에는 실패했다
후에는 고쳐졌다
그런데 실제 결과 폴더를 보면 before screenshot, after log, JSON, transcript가 흩어져 있을 수 있다. 파일은 있는데 이야기가 끊긴다.
그래서 evidencePairs가 필요하다.
{
"name": "query-audit-duplicate-row-cursor-window",
"before": {
"verdict": "REPRODUCED",
"artifacts": [
"screenshots/actual-querypie-ui/00-before-patch-actual-querypie-ui-duplicates.png",
"transcripts/before-cursor-regression-test.log"
]
},
"after": {
"verdict": "FIXED",
"artifacts": [
"transcripts/after-cursor-regression-test.log"
]
}
}
이렇게 묶으면 결과 패키지가 단순 파일 모음이 아니라 작은 사건 기록이 된다.
before에는 무슨 일이 있었고, after에는 무엇이 달라졌는지 바로 보인다.
cleanupChecks: 탐정은 현장을 정리하고 떠난다
재현 작업은 종종 흔적을 남긴다.
임시 token 파일, local config override, pre-patch 코드, mock service, fixture 데이터.
이런 흔적이 남으면 다음 작업을 망칠 수 있다.
그래서 cleanupChecks를 넣었다.
{
"temporaryThresholdRestored": true,
"prePatchCursorRemoved": true,
"liveE2ETokenFileRemoved": true,
"secretScanHits": 0
}
이제 “정리했음”을 말로만 하는 게 아니라 manifest에 남긴다.
특히 secret scan 결과가 같이 들어가는 점이 좋다.
증거는 설득력이 있어야 하지만, 비밀을 품고 있으면 안 된다.
verifier도 더 까다로워졌다
구조만 추가하면 절반이다. 나머지 절반은 검증이다.
그래서 verify-result-package.py에 옵션을 추가했다.
--require-actual-ui-screenshot
--require-terminal-transcript
--require-restored-controls
이제 verifier는 marker 문구만 보지 않는다.
실제로 아래를 확인한다.
screenshots/actual-querypie-ui/* 파일이 있는가?
transcripts/*.log 또는 transcripts/*.txt가 있는가?
reproductionControls가 있고 모두 restored=true인가?
secret leak 후보는 없는가?
이번 개선 후 같은 QCP-4731 패키지를 돌려보니 차이가 분명했다.
이전 artifactCount: 2
개선 후 artifactCount: 6
이전에는 스크린샷 2개만 잡혔다. 이제는 실제 UI 스크린샷, before/after transcript, JSON verdict, summary까지 함께 잡힌다.
검증 결과는 이렇게 더 풍부해졌다.
actualUiScreenshots: 2
terminalTranscripts: 2
reproductionControls.count: 2
reproductionControls.ok: true
evidencePairCount: 1
fixtureCount: 1
secretLeakCandidates: []
PASS라는 한 단어 뒤에 근거가 생겼다.
일부러 실패도 시켜봤다
검증기는 성공할 때보다 실패할 때 진짜 가치가 보인다.
그래서 임시 패키지를 만들고 일부러 두 가지를 빼봤다.
terminal transcript 없음
reproductionControls restored=false
결과는 기대대로 실패했다.
status=FAIL
- terminal transcript artifact not found
- reproductionControls are not fully restored
이게 중요하다.
좋은 검증기는 “좋아요”만 말하는 도구가 아니다. 빠진 증거가 있을 때 정확히 “이게 빠졌어요”라고 말해야 한다.
ReproKit은 더 커진 게 아니라 더 깐깐해졌다
이번 개선은 거대한 프레임워크를 만든 작업이 아니다.
새로운 전용 runner를 잔뜩 추가하지 않았다. 특정 QCP에 묶인 seeder나 detector도 만들지 않았다.
대신 결과 패키지가 스스로 설명할 수 있게 했다.
어떻게 재현했는지
무엇을 임시로 바꿨는지
어떤 fixture를 썼는지
before/after 증거가 어떻게 연결되는지
정리는 됐는지
이 다섯 가지를 가볍게 담게 했다.
비유하자면, ReproKit에 거대한 새 엔진을 얹은 게 아니다. 증거 봉투 겉면에 체크리스트와 바코드를 붙인 것이다.
그런데 이 차이가 꽤 크다.
이제 리뷰어는 폴더를 뒤지지 않아도 된다. verifier가 먼저 말해준다.
“이 증거 봉투는 열어볼 만합니다.”
또는,
“잠깐만요. 터미널 로그가 없고, 임시 변경도 원복되지 않았습니다.”
이번 개선의 교훈
테스트 자동화는 코드를 실행하는 데서 끝나지 않는다.
좋은 테스트 자동화는 결과를 믿을 수 있게 만든다.
좋은 증빙 자동화는 스크린샷을 찍는 데서 끝나지 않는다.
그 스크린샷이 실제 화면인지, 어떤 조건에서 찍혔는지, 관련 로그가 있는지, 비밀값은 없는지, 임시 변경은 원복됐는지까지 확인한다.
이번 ReproKit 개선은 그런 방향의 작은 한 걸음이다.
이제 ReproKit은 이렇게 말할 수 있다.
재현했습니다.
증거도 있습니다.
그리고 그 증거가 믿을 만한지도 확인했습니다.
탐정에게도, 개발자에게도, 이 정도면 꽤 든든한 증거 봉투다.