정산 대상이 몇 건 빠져 있었다. 만드는 쪽과 처리하는 쪽이 다른 시스템이었다.
Table of contents
Open Table of contents
증상 — 빠져 있던 정산 대상
두 시스템이 이렇게 나눠 맡고 있다.
주문 시스템 정산 대상을 만든다 (settle_target 테이블에 넣는다)
정산 시스템 그 테이블을 읽어 처리한다
같은 DB의 같은 settle_target 을 쓰지만 코드베이스는 서로 다르다.
빠진 건을 찾아보니 settle_target 에 아예 없었다. 처리하는 쪽이 놓친 것이 아니라 만드는 쪽이 안 넣은 것이었다.
들어온 것만 처리하는 쪽에서는 안 들어온 것을 알 방법이 없다. settle_target 을 읽는 SELECT 는 정상으로 돌았고 처리 로그에도 오류가 하나도 없었다.
원인 — MIXED가 조건에 안 걸렸다
넣을지 말지를 정하는 자리는 이랬다.
if ($order->state === 'DONE' && in_array($order->payMethod, ['CARD','BANK','VBANK'], true)) {
$this->settleTarget->insert($order);
}
전액 포인트 결제를 빼려고 허용할 수단을 나열해 두고 있었다.
빠진 주문의 결제수단은 이랬다.
결제수단 MIXED (포인트 3,000 + 카드 12,000)
MIXED 가 목록에 없으니 in_array 가 거짓이 되어 그대로 지나간다.
포인트와 카드를 같이 쓰는 결제가 나중에 생겼는데 이 목록은 그 전에 쓰인 것이었다. 실제로는 카드로 12,000원이 나갔으니 정산 대상이 맞는 주문이다.
in_array 의 목록은 수단이 늘 때마다 손대야 한다. 고치는 것을 잊으면 그 payMethod 로 결제한 주문이 통째로 빠진다.
사이에서 사라지는 것을 세게 했다
두 시스템 사이에는 아무 확인이 없었다.
-- 정산 대상이어야 하는데 안 들어간 것
SELECT o.no FROM orders o
LEFT JOIN settle_target s ON o.no = s.order_no
WHERE o.state = 'DONE' AND o.done_date BETWEEN ? AND ?
AND s.order_no IS NULL;
매일 이 조회를 돌리고 0이 아니면 알리게 했다.
2017-12-11 누락 4건
이 조회의 조건은 만드는 쪽 코드를 보지 않고 따로 썼다. 같은 조건을 옮겨 적으면 그 조건이 틀렸을 때 확인도 같이 틀린다.
만드는 쪽이 in_array 로 판단했으니 확인은 state 와 조인만으로 쓴 셈이다. LEFT JOIN 과 IS NULL 로만 세는 쪽이 만드는 코드와 겹치지 않아 어긋남이 드러난다.
조치 — 수단이 아니라 금액으로
정산 대상인지를 판단하는 자리를 하나로 모았다.
final class SettleRule {
public static function isTarget(Order $o): bool {
if ($o->state !== 'DONE') return false;
if ($o->totalPaidAmount <= 0) return false; // 전액 포인트면 0이다
return true;
}
}
payMethod 대신 totalPaidAmount 를 본다.
빼려던 것은 실제로 돈이 안 오간 주문이었고 그것은 totalPaidAmount 가 0이라는 결과로 드러난다. payMethod 가 무엇이든 이 조건은 그대로다.
MIXED 가 생겼을 때 조건이 깨진 것은 허용할 수단을 나열해 뒀기 때문이다. 결과로 판단하면 새 수단이 열 개 생겨도 이 줄을 안 고친다.
검증 — 넣는 것과 확인하는 것을 나눴다
판단은 한 곳에서 하되 확인은 다른 방식으로 하게 했다.
넣기 SettleRule::isTarget() 로 판단해서 넣는다
확인 금액 합계로 맞춰 본다
건수가 아니라 금액으로 맞췄다.
SELECT SUM(paid_amount) FROM orders WHERE done_date BETWEEN ? AND ? AND state='DONE';
SELECT SUM(amount) FROM settle_target WHERE done_date BETWEEN ? AND ?;
두 합계가 같아야 하고 다르면 어딘가 어긋난 것이다.
COUNT(*) 는 같은데 SUM 이 다른 날이 나왔다. 부분 취소된 주문의 amount 가 갱신되지 않고 있었다.
세는 방식을 다르게 하니 다른 문제가 드러났다. 건수만 봤으면 이 건은 영영 안 보였을 것이다.
대응 — 늦게 들어오는 것
주문 확정과 정산 대상 생성 사이에 시간이 있었다.
12/10 23:58 주문 확정
12/11 00:03 정산 대상 생성
5분 사이에 날짜가 넘어가서 done_date 가 10일인데 11일 정산으로 갔다.
$this->settleTarget->insert([
'order_no' => $o->no,
'done_date' => $o->doneDate, // 주문 확정 시각
'created_at' => date('Y-m-d H:i:s'), // 만든 시각
]);
두 시각을 다 남기고 정산은 done_date 기준으로 한다.
늦게 들어온 것을 세는 조회도 뒀다.
SELECT COUNT(*) FROM settle_target
WHERE DATE(created_at) > DATE(done_date);
이미 마감한 날짜에 뒤늦게 들어온 건은 따로 처리해야 한다.
마지막으로 두 시스템 사이의 약속을 문서로 뒀다.
settle_target 테이블 규약
넣는 쪽 주문 시스템
읽는 쪽 정산 시스템
넣는 조건 SettleRule::isTarget() 참조
넣는 시점 주문 확정 직후
필수 컬럼 order_no, amount, done_date, created_at
읽는 쪽은 done_date 기준으로 처리한다.
마감 후 들어온 건은 late_flag 로 표시되며 다음 마감에 포함한다.
컬럼 변경은 양쪽에 알린 뒤 한다.
두 팀이 같이 보는 자리에 두어 한쪽만 아는 것이 없게 했다. 이 약속이 없으면 한쪽이 컬럼을 고칠 때 다른 쪽이 모른다.
정리
- 만드는 쪽과 처리하는 쪽이 다르면 사이에서 사라지는 것이 생긴다
- 양쪽 어디에서도 오류로 안 잡히고 로그도 깨끗하다
- 허용할 수단을 나열하는 조건은 수단이 늘 때마다 깨진다
payMethod대신totalPaidAmount로 판단하면 수단이 늘어도 그대로다- 사이를 매일 맞춰 보는 조회를 두되 그 조건은 만드는 쪽과 별개로 쓴다
- 같은 조건을 옮겨 적으면 그 조건이 틀렸을 때 확인도 같이 틀린다
- 판단은 한 곳에서 하되 확인은 다른 방식으로 한다
- 건수로 못 잡는 것이 금액 합계로 잡힌다
- 확정 시각과 만든 시각을 다 남기고 확정 시각 기준으로 처리한다
- 마감 후 들어온 것을 표시하고 어떻게 할지 정한다
- 두 시스템 사이의 약속을 문서로 둔다