68파일 PR 리뷰 — 도메인 슬라이스와 스팟검증

26년 08월 17일

68파일 PR을 어떻게 리뷰할 것인가

법원 경매 데이터를 수집해서 권리분석까지 돌리는 개인 프로젝트에 next-gen 브랜치 작업이 쌓였다.

master로 보내는 PR을 만들었는데 28커밋, 68파일, +4,053/−472였다.

변경 테마만 다섯 개다.

법원 서류 원문 수집 체인, VWorld 토지정보 보강 잡, 권리 파서 고도화, 크롤러 성능 조정, 스키마·타입 정리.

이 덩치를 한 명이 파일 순서대로 읽으면 앞에서 읽은 내용이 뒤에서 희석된다.

리뷰 품질은 읽는 순서보다 자르는 방식이 결정한다고 생각해서 리뷰 방식부터 설계했다.

도메인 4슬라이스

diff를 파일 수가 아니라 변경 성격으로 잘랐다.

A는 크롤러 파이프라인, B는 외부 API 클라이언트, C는 권리·지분·임차인 파서, D는 db와 타입이다.

각 슬라이스를 독립 리뷰어 태스크로 만들어 병렬 팬아웃했다.

경계를 도메인으로 잡으니 각 리뷰어가 자기 슬라이스의 맥락을 온전히 가졌다.

파서 로직과 저장 로직이 같은 파일에 섞여 있어도 성격으로 나눈 쪽이 파일 개수로 나눈 쪽보다 리뷰어가 가져올 맥락이 명확했다.

대기 시간에도 검증을

리뷰어 넷이 도는 10분 남짓 동안 오케스트레이터는 놀지 않았다.

CI 상태를 확인하고 이미 눈에 들어온 발견 사항 두어 건을 직접 소스에서 선검증했다.

리뷰어 산출물의 교차검증 데이터를 미리 확보해두는 셈이다.

결과는 슬라이스별로 회수해 하나의 파일로 병합했다.

총 49건, blocker 1 / high 20 / medium 22 / low 3이었고 전원 REQUEST CHANGES 의견이었다.

리뷰어 주장은 가설이다

여기서부터가 이날의 핵심이다.

49건을 그대로 믿고 종합 리포트를 쓰지 않았다.

최고 영향도 6건은 오케스트레이터가 직접 소스를 열어 재확정한 뒤에야 리포트에 넣었다.

리뷰어 주장은 가설이고 검증은 별개의 계층이다.

방향은 맞고 디테일은 틀린 사례

실제 사례가 나왔다.

한 리뷰어가 스윕 상태 필터가 저장값과 미스매치인데 저장값이 한글이라 못 맞는다고 주장했다.

실제 저장값을 열어보니 영문 SOLD, CANCELED였다.

디테일은 틀렸다.

근데 미스매치 자체는 유효했다.

쿼리는 NOT IN (‘SOLD’, ‘CANCELLED’)인데 저장값은 L이 하나인 CANCELED였다.

취하된 물건이 제외되지 않고 불필요한 API 호출과 서류 저장이 반복되는 실제 결함이었다.

방향과 디테일을 분리해서 검증하지 않았다면, 틀린 근거 때문에 유효한 결함까지 같이 버렸을 것이다.

확정된 6결함

직접 검증으로 확정한 여섯 건 중에는 법적 수치를 다루는 프로젝트 특성상 무거운 것들이 있었다.

물건 명세서를 가져올 때 dspslGdsSeq를 1로 하드코딩해서 한 사건에 물건이 여러 개면 전부 1번 물건의 명세서가 저장되는 것.

지분 “1,000/2,000”이 쉼표 뒤만 매칭돼서 0/2로 파싱되는 것.

정규식 룩비하인드가 쉼표는 통과시켰고 지분율이라는 법적 수치가 0으로 왜곡됐다.

“미등기”라는 단어 안의 “등기”를 마커로 인식해서 전입일이 등기일로 승격되는 것도 있었다.

MemorySink 경로에는 CollectedProperty에 documents 필드 자체가 없어서 수집된 법원 서류가 에러 없이 사라졌다.

마지막은 프로덕션 DDL 부재다.

migration 파일이 브랜치에 없어서 배포 시점에 스키마가 코드가 기대하는 테이블을 못 읽고 빌드가 깨진다.

Roadmap에 배포 금지로 적어둔 것과 일치하는 재확정이었다.

스키마 드리프트가 코드 버그로 위장했다

같은 세션에서 만난 트러블도 정리해둔다.

워크트리에서 db 패키지 빌드가 두 종류 TS 에러로 실패했다.

nullable 컬럼 타입 불일치와, 생성된 스키마에 propertyDocuments export가 없다는 에러였다.

코드 버그처럼 보였는데 원인은 달랐다.

generated 스키마가 gitignore된 introspection 산출물이라 브랜치를 바꿔도 갱신되지 않았던 것.

코드는 신규 컬럼과 신규 테이블을 기대하는데 생성물이 옛날 버전이라 타입이 어긋난 거였다.

git status에도 안 보이는 영역이라 소스를 고치려 들면 멀쩡한 코드를 잘못 고치는 상황이다.

로컬 supabase에서 읽기 전용 introspection으로 재생성하니 해결됐다.

turbo가 실패까지 캐시한다

여기서 함정이 하나 더 있었다.

재생성 후에 재실행했는데 동일한 빌드 에러가 그대로 다시 나왔다.

출력 하단의 Cached 라인을 보고 알았다.

turbo는 실패한 태스크 결과도 캐시하고 gitignore된 디렉터리가 캐시 입력에 없으면 재생성 전후 해시가 같아서 실패가 재생된다.

—force로 캐시를 우회하니 실제 결과가 나왔다.

테스트는 207 pass / 1 skip / 0 fail이었다.

이 함정을 모르면 재생성했는데도 똑같이 실패한다고 오판해서 롤백하거나 멀쩡한 코드를 고친다.

빌드 실패가 캐시 재생인지 실제 실패인지는 출력의 Cached 라인으로 구분해야 한다.

워크트리는 창문일 뿐

비슷한 맥락에서 워크트리 DB 구성도 확인했다.

워크트리를 세 개 떠두고 있지만 drizzle 설정의 폴백이 전부 같은 로컬 postgres를 가리킨다.

즉 어떤 워크트리에서 DDL을 밀어도 같은 로컬 DB가 바뀐다.

DB는 공유되지만 generated 스키마만 워크트리별 사본이라 한쪽에서 DDL을 바꾸면 다른 워크트리의 사본이 스테일이 된다.

이게 앞의 위장 TS 에러의 구조적 원인이었다.

브랜치별로 다른 DB 상태가 필요하면 스택 분리가 선행되어야 한다.

마무리

이날 배운 핵심은 두 가지다.

하나, 대형 diff 리뷰는 파일 수가 아니라 도메인으로 자르고 종합 전에 최고 영향도 발견은 직접 소스로 재확정한다.

둘, 리뷰어 주장은 가설이다.

방향이 맞아도 디테일이 틀릴 수 있고 디테일이 틀려도 방향은 살아있을 수 있다.

검증 계층을 하나 더 두는 비용보다, 유효한 결함을 틀린 근거와 함께 버리는 손실이 크다고 생각한다.