재고가 없는 상품을 판매중지시키는 배치를 SQL 로 옮기는 작업을 하면서 기존 코드를 봤다. stock = 0 으로 정확히 0과 같은지를 비교하는 조건 하나가 거기 들어 있었다.
Table of contents
Open Table of contents
검증 — 세어서 확인한 실재
그 조건을 그대로 옮기려던 참에 0 이하도 있지 않느냐는 물음이 왔다. 두 가지 방법이 있었는데 컬럼이 INT 이니 음수가 들어갈 수 있다고 추측으로 답하는 것과 운영 데이터베이스에서 직접 세어 보는 것이다. 뒤쪽을 골랐다. 이유는 들어갈 수 있다와 들어 있다는 다른 말이라서다. 대신 운영 DB 에 집계 쿼리를 내는 승인을 받느라 반나절이 드는 것을 감수했다.
products 에 승인되고 정상 상태인데 stock < 0 인 것이 열일곱 건 있었고 MIN(stock) 이 음의 한 자리였다. product_options 는 수백 건이나 됐고 가장 작은 값이 네 자리 음수였다. MySQL 매뉴얼대로 UNSIGNED 를 붙이면 음수가 아예 못 들어가는데 이 컬럼들은 부호 있는 INT 였다.
원인 — 영원히 안 잡히는 상품
그 열일곱 건은 승인되고 정상 상태이므로 지금 판매 중인 상품들이었다. 재고가 음수인 상품이 그대로 마켓에 걸려 있는 상태였다.
기존 조건은 정확히 0인지만 보므로 음수인 값은 아예 걸리지 않는다. 배치가 매일 돌아도 그 상품들은 영원히 중지되지 않으므로 조건을 stock <= 0 으로 바꿔야 했다.
조치 — 같은 조건을 쓰는 모든 곳
이것은 이 배치만의 문제가 아니라 재고를 기준으로 상품을 거르는 모든 곳에 해당하는 얘기다. 판매중지 배치와 품절 표시와 재고 통계와 검증 쿼리가 전부 같은 조건을 쓰고 있었다.
grep -rn "stock = 0\|stock == 0" 으로 같음 비교를 쓰는 곳을 전부 찾아 이하 비교로 바꿔야 하는 작업이 된다. 한 곳만 고치고 끝내면 나머지에서 같은 누락이 계속 생기기 때문이다.
주의 — 합계가 왜곡하는 실체
옵션이 있는 상품은 재고가 SUM(product_options.stock) 인데 여기서는 음수가 더 위험했다. 한 옵션의 큰 음수가 다른 옵션의 양수를 상쇄하면 팔 수 있는 옵션이 있는데도 상품 전체가 재고 없음으로 잡힌다.
반대 경우도 가능한데 큰 양수가 작은 음수를 덮으면 못 파는 옵션이 있는데도 합계로는 재고가 있어 보인다. 합계가 실체를 양방향으로 왜곡하므로 합계만 보고 판정하면 안 된다.
배경 — 안 판 원인과 조회 방식
음수 재고가 애초에 왜 생기는지는 이번에 보지 않았는데 차감이 중복으로 돌거나 동시 주문에서 lockForUpdate() 없이 빼는 것으로 짐작만 했다. 이번 작업 범위가 아니라 재고 차감 경로의 별건 조사 대상으로 ISSUES.md 에 남긴다고 적었다.
이 확인은 운영 데이터베이스를 대상으로 하므로 행을 안 읽고 집계만 각 테이블에 한 번씩 냈다. 이 조사의 시작이 0 이하도 있지 않느냐는 물음이었는데 안 물었으면 기존 조건을 그대로 옮겼을 것이고 기존 코드를 옮기는 작업이 그 조건을 검증할 기회이기도 하다.
정리
- 음수 재고가 실재하므로 추측하지 말고 세어 본다
- 0과 같은지로 판정하면 그 상품들을 전부 놓친다
- 조건은 0 이하여야 한다
- 재고로 거르는 모든 배치와 통계와 검증에 같이 적용한다
- 합계는 음수가 양수를 상쇄해 실체를 왜곡한다
- 반대로 양수가 음수를 덮어 있어 보이게도 한다
- 원인을 안 팠으면 별건 조사 대상으로 분리해 적는다
- 옮기는 작업이 기존 조건을 검증할 기회다