가송장 패널티에 이의를 제기해야 하는 건이 있어서 그 전에 재출고가 정말 안 됐는지를 다시 확인했다. 이 건은 앞서 세 번 확인했고 세 번 다 입점사가 재발송 송장을 등록하지 않았다는 결론이었다.
Table of contents
Open Table of contents
문제 — 세 번 같은 결론을 낸 조사
세 번의 근거는 모두 order_histories 였고 거기에 shipment_created 항목이 두 개 찍혀 있었다. 그런데 shipments 에는 행이 하나뿐이라 생성 로그는 있는데 실제로는 안 만들어졌다고 읽었다.
그 독해가 거꾸로였다. 레코드가 하나라는 사실이 실패의 증거가 아니라 다른 방식으로 처리됐다는 증거였다.
원인 — 이력의 생성이라는 문구
shipments 를 제대로 열어 보니 created_at 은 원본 출고 시점이고 updated_at 은 재출고 시점이었으며 tracking_no 는 재발송 번호였다. 새 행을 만든 것이 아니라 updateOrCreate() 가 기존 행에 항목을 더하고 송장을 덮어쓴 것이었다.
이력 로그의 문구는 코드가 남긴 문자열이므로 그 코드 경로가 생성과 갱신을 함께 처리하면 로그로는 둘이 구분되지 않는다. Laravel 9 공식 문서대로 updateOrCreate() 는 조건에 맞는 모델이 있으면 갱신하고 없으면 생성하는데 로그 문구는 그 뒤에 한 줄로 찍히고 있었다. 문구를 근거로 삼는 순간 정반대 결론이 나올 수 있었다.
판단 기준 — 덮어쓰기를 판별하는 세 지문
이 패턴을 알아보는 지문을 셋으로 정리했다. 해당 order_id 의 shipments 가 한 건뿐이고 created_at 은 그대로인데 updated_at 만 이동했으며 tracking_no 가 원본이 아니라 재발송 번호인 경우다.
셋이 모두 맞으면 덮어쓰기로 처리된 것이다. 소명에는 두 가지 방법이 있었는데 이력 화면 캡처를 붙이는 것과 shipments 행의 세 컬럼 값을 붙이는 것이다. 뒤쪽을 골랐다. 이유는 앞쪽이 바로 세 번 틀린 근거라서다. 대신 마켓 담당자가 컬럼 이름을 읽어야 하므로 각 값이 무엇을 뜻하는지 한 줄씩 설명을 붙이는 수고를 감수했다. 이 판정이 외부에 나가는 소명의 방향을 정하는 자리였으므로 틀린 채로 나갔으면 우리가 잘못 소명한 것이 됐다.
앞의 결론이 전제가 될 때
같은 결론을 세 번 냈는데도 안 잡힌 이유는 첫 조사의 결론이 다음 조사의 전제가 됐기 때문이다. 두 번째와 세 번째는 정말 그런지를 다시 본 것이 아니라 그 결론을 뒷받침할 근거를 더 찾았다.
같은 방향으로 세 번 본 것이지 세 번 검증한 것이 아니었다. 이번에 뒤집힌 것은 안 된 것이 맞느냐가 아니라 테이블에는 무엇이 있느냐로 물음을 바꿨기 때문이다.
주의 — 시간 컬럼을 덮어쓸 때의 파급
이 덮어쓰기가 다른 문제도 만들고 있었는데 created_at 이 원본 출고 시점으로 남는다는 점이었다. 가송장 탐지가 created_at 을 기준으로 돌면 재발송한 건이 오래전에 출고됐는데 아직 배송이 안 된 것으로 보인다.
그러면 재발송 직후부터 계속 오탐하게 되고 이 건의 패널티 자체가 그렇게 생겼을 가능성이 있다. 한 곳의 덮어쓰기가 다른 곳의 탐지를 망가뜨리므로 시간 컬럼을 덮어쓸 때는 그것을 읽는 곳을 함께 봐야 한다.
정리
- 이력의 생성이라는 문구가 실제 생성을 뜻하지 않을 수 있다
- 한 코드 경로가 생성과 갱신을 함께 처리하면 로그로 구분되지 않는다
- 판별 지문은 건수와
created_at과updated_at의 차이와tracking_no셋이다 - 이력만 보고 판정하면 정반대 결론에 이를 수 있다
- 같은 결론을 여러 번 내는 것이 여러 번 검증한 것은 아니다
- 앞의 결론이 다음 조사의 전제가 되면 방향이 고정된다
- 뒤집으려면 물음 자체를 바꾼다
- 시간 컬럼을 덮어쓰면 그것을 읽는 탐지가 오탐한다