유지보수를 맡고 있는 레거시 PLM 시스템에서 문득 의심이 생겼습니다. “하위 파트를 변경해서 릴리즈하면, 그 파트를 포함하는 상위 BOM의 리비전도 같이 올라야 하는 것 아닌가? 지금 안 오르는 것 같은데.” 감으로 코드를 수정하기엔 리비전 로직은 PLM의 심장입니다. 그래서 수정 전에 감사(audit)부터 했습니다. 이 글은 그 기록입니다.
의심을 검증 가능한 질문으로 바꾸기
“리비전이 이상한 것 같다”는 검증할 수 없습니다. 질문을 이렇게 쪼갰습니다.
- 리비전 번호는 어디서, 어떤 규칙으로 채번되는가
- “최신본” 플래그(IS_LAST)는 어느 시점에 어떻게 전환되는가
- 변경 릴리즈 시 상위 BOM으로의 롤업(전파)이 존재하는가
- 각 단계의 트랜잭션 경계는 어디인가
방법: 독립 분석 스레드 3개로 교차검증
리비전 로직은 결재 완료 후처리, 파트 생성, BOM 릴리즈 등 여러 경로에 흩어져 있습니다. 한 번의 분석으로 “없다”를 확신하기 어려워서, AI 에이전트 분석 스레드를 서로 다른 각도로 3개 돌렸습니다 — 결재 승인 흐름 추적, 파트 리비전 생성 추적, 양산 BOM 정합성 추적. 각 스레드는 서로의 결과를 모르는 상태에서 코드 근거와 함께 결론을 내고, 마지막에 사람이 핵심 주장만 직접 교차검증했습니다.
이 방식이 유효했던 이유: 셋 중 하나라도 “롤업이 있다”고 하면 나머지가 놓친 경로가 있다는 뜻이 됩니다. 결과는 — 3개 스레드 모두 독립적으로 “상위 롤업 없음”에 도달했습니다. 게다가 스레드 간 표현이 갈린 지점(어떤 메서드가 실제 호출되는가)이 하나 있었는데, 직접 확인해 보니 한쪽이 참조한 메서드는 호출처가 없는 데드코드였습니다. 교차검증이 없었다면 죽은 코드를 근거로 삼을 뻔했습니다.
확정된 구조적 결함: 상위 리비전 롤업의 부재
리비전이 실제로 오르는 경로는 결재 완료 후처리 단 하나였고, 그 갱신 쿼리는 전부 WHERE OBJID = ? — 변경 대상으로 직접 연결된 그 행 하나만 갱신합니다. 하위 변경을 상위로 전파하는 역전개(where-used) 루프는 코드 어디에도 없었습니다. 하위 파트만 릴리즈하면 상위 BOM은 옛 리비전인 채 “최신”으로 표시됩니다.
흥미로운 건, 데이터 모델에는 부모 참조가 있고 재귀 조회 인프라(WITH RECURSIVE)도 이미 있다는 점입니다. 전부 화면 트리 표시용 하향 전개로만 쓰이고 있을 뿐. “재료는 다 있는데 배선이 안 된” 상태라, 역전개 쿼리 하나와 부모 갱신 루프만 추가하면 구현 가능한 결함입니다.
덤으로 나온 무결성 결함들
감사를 하면 목표물 말고도 걸려 나옵니다. 대표적인 것들:
- 예외를 삼키는 catch — 리비전 행 생성이 실패(롤백)해도 rethrow하지 않아 사용자에겐 정상 팝업이 뜨고, 이후 저장의 MERGE 문이 필수 값이 전부 NULL인 반쪽짜리 행을 만들어낼 수 있는 경로가 있었습니다.
- INSERT에 리비전 컬럼 자체가 누락 — 행 생성과 리비전 입력이 별도 요청·별도 트랜잭션이라, 중간에 이탈하면 리비전 NULL의 고아 행이 남습니다.
- 유니크 제약 없는 최신본 플래그 — IS_LAST 전환이 “기존 것 0으로, 새것 1로”의 비원자적 UPDATE 2방인데 락도 제약도 없어, 동시 확정 시 최신본이 0개 또는 2개가 될 수 있습니다. 게다가 기존 것을 내리는 조건에 상태 필터와 NULL 비교 함정이 있어 단독 실행에서도 2개가 될 경로가 존재합니다.
- LIMIT 없는 상관 서브쿼리 — 최신본이 2개가 되는 순간, 다음 채번 서브쿼리가 다중 행 에러로 터지거나 잘못된 번호를 뽑습니다. 결함이 결함을 연쇄시키는 구조.
수정은 순서가 있다
바로 롤업부터 구현하고 싶어지지만, 순서를 반대로 잡았습니다. 무결성(예외 rethrow, 최신본 유니크 보장, 채번 쿼리 방어)을 먼저 잡고, 롤업은 그 위에 올려야 합니다. 최신본 플래그가 2개가 될 수 있는 기반 위에 전파 로직을 얹으면, 잘못된 최신본이 상위로 전파되는 더 나쁜 시스템이 되니까요. 이번 단계는 감사와 문서화까지로 끊고, 수정은 협의 후 착수하기로 했습니다.
교훈
- “없다”는 것을 증명할 때는 독립 분석을 교차시켜라. 한 번의 탐색이 못 찾은 것과 존재하지 않는 것은 다르다. 서로 모르는 분석 3개가 같은 결론에 도달하면 그때 확정이다.
- 최신본 플래그는 제약이 없으면 언젠가 2개가 된다. IS_LAST류 컬럼에는 부분 유니크 인덱스든 락이든, DB 수준의 보장이 필요하다.
- 예외를 삼키는 catch는 데이터로 복수한다. rollback만 하고 rethrow하지 않는 catch는 “실패했는데 성공한 척”하는 UI를 만들고, 그 다음 사용자 행동이 반쪽 데이터를 만든다.
- 수정 순서는 의존성 역순. 전파 로직은 무결성 위에서만 안전하다.
덧붙여
이 감사는 AI 코딩 에이전트(Claude Code)의 멀티 에이전트 분석으로 진행했습니다. 독립 스레드 3개의 코드 추적과 근거 수집은 에이전트가, 질문 설계(“무엇을 확인해야 의심이 확정되는가”)와 스레드 간 불일치의 최종 판정, 수정 순서의 결정은 사람이 했습니다. 레거시를 상대할 때 AI가 가장 값진 순간은 코드를 짤 때가 아니라 “없다”를 확정할 때라는 걸 배운 작업이었습니다.