새 수수료 계산을 플래그로 감싸서 넣었다. 문제가 생기면 끄면 된다고 생각했는데 껐더니 값이 원래대로 안 돌아왔다.
Table of contents
Open Table of contents
증상 — 껐는데 값이 그대로였다
플래그가 걸린 자리를 찾았다.
if (Feature::on('fee_v2')) {
$fee = $this->feeV2->calc($order);
} else {
$fee = $this->feeV1->calc($order);
}
$order->fee = $fee;
$this->orders->save($order);
Feature::on 으로 계산은 갈리는데 save 는 갈리지 않는다.
플래그를 켠 동안 새 방식으로 계산한 값이 이미 orders.fee 에 들어가 있다. 끄면 앞으로 계산하는 것만 옛 방식이 되고 이미 저장된 것은 그대로 남는다.
원인 — 쓰는 쪽만 감쌌다
읽는 자리를 찾아봤다.
$fee = $order->fee; // 저장된 값을 읽는다
$order->fee 를 읽는 자리에는 Feature::on 이 없어서 켠 동안 쌓인 값을 계속 읽는다.
쓰는 쪽만 막고 읽는 쪽을 안 막으면 끈 상태가 켜기 전 상태와 같아지지 않는다. feeV1 로 계산이 돌아갔는데 화면에는 여전히 feeV2 가 남긴 값이 나오고 있었다.
fee_v2 플래그가 감싸는 것이 계산인지 결과인지를 구분하지 않은 것이 원인이었다. 계산만 감싸면 그 결과가 남는 자리는 감싸지지 않는다.
비교 — 되돌아가는 값과 아닌 값
플래그로 감쌀 때 값의 성격이 둘로 갈린다.
[되돌아가는 것] 요청마다 계산해서 응답에만 쓰는 값
[안 되돌아가는 것] 저장되는 값, 외부에 보낸 것, 다른 처리의 입력이 된 것
앞은 끄면 다음 요청부터 원래대로이고 뒤는 이미 남은 것이 그대로 있다.
새 수수료는 뒤쪽이었다. orders.fee 에 남고 정산의 입력이 되는 값이라 플래그로 감싼다고 끌 수 있는 것이 아니었다.
감싸기 전에 이 구분을 먼저 해야 한다. 뒤쪽이면 Feature::on 만으로는 부족하고 자리를 따로 두는 설계가 함께 필요했다.
조치 — 저장 자리 분리
저장 자리를 갈랐다.
ALTER TABLE orders ADD COLUMN fee_v2 decimal(18,8) DEFAULT NULL;
$order->fee = $this->feeV1->calc($order);
if (Feature::on('fee_v2')) {
$order->fee_v2 = $this->feeV2->calc($order);
}
fee 는 항상 옛 방식으로 채우고 새 값은 fee_v2 에 담는다.
읽는 쪽에서 고르게 했다.
$fee = Feature::on('fee_v2') ? ($order->fee_v2 ?? $order->fee) : $order->fee;
끄면 fee 를 읽으므로 켠 동안 쌓인 주문도 옛 값으로 돌아온다.
컬럼 하나만 쓰면 어느 시점에 어느 방식으로 계산됐는지도 알 수 없다. 자리를 나눈 덕에 끄는 것도 되고 나중에 대조하는 것도 가능해졌다.
검증 — 켜기 전에 두 값을 비교
자리가 둘이 되니 예상 못 한 이득이 있었다.
읽는 쪽은 아직 fee 를 쓰게 두고 feeV2 가 fee_v2 만 채우게 며칠 돌렸다.
SELECT COUNT(*) AS n,
SUM(ABS(fee - fee_v2) > 0.00000001) AS diff
FROM orders WHERE reg_date >= ? AND fee_v2 IS NOT NULL;
n=12840 diff=37
n=12840 중 diff=37 이 나왔고 켜기 전에 그 37건을 열어 봤다.
31건은 새 방식이 맞았고 6건은 새 방식의 반올림이 틀렸다. 바로 켰으면 그 6건을 운영에서 사고로 만났을 것이다.
켜고 나서 발견하는 것과 켜기 전에 아는 것은 값이 전혀 다르다. 두 값을 함께 두는 구조가 그 확인을 가능하게 했다.
제약 — 플래그를 지울 조건
끈 상태가 실제로 옛 상태인지도 밟아 봤다.
1. 끈 상태로 주문 → 값 A
2. 켠 상태로 주문 → 값 B
3. 다시 끄고 1번 주문을 조회 → 값 A 여야 한다
4. 다시 끄고 2번 주문을 조회 → 값 A 여야 한다
4번이 핵심인데 켠 동안 만들어진 주문도 fee 로 보여야 끈 것이다.
끌 수 있다는 것과 껐을 때 원래대로 돌아온다는 것은 확인하기 전까지 다른 문제였다.
fee 와 fee_v2 를 계속 두면 그것대로 짐이라 정리할 시점도 같이 정했다.
새 값이 4주간 차이 0으로 유지되면 읽는 쪽을 새 값으로 고정하고,
그다음 릴리스에서 옛 컬럼과 플래그를 지운다.
조건과 시점을 안 적으면 플래그가 영구히 남는다. 남은 플래그는 다음 사람이 지워도 되는지 판단할 근거를 잃는다.
정리
- 플래그로 감싸도 쓰는 쪽만 갈리면 끈 상태가 원래 상태가 아니다
- 읽는 쪽에도 갈림이 있어야 끈 것이 된다
- 계산을 감싸는 것과 결과를 감싸는 것은 다르다
- 응답에만 쓰는 값과 저장되는 값을 먼저 가른다
- 저장되고 외부로 나가고 다른 처리의 입력이 된 값은 안 돌아온다
- 저장 자리를 나누고 옛 값을 항상 채운다
- 자리가 둘이면 켜기 전에 두 값을 비교할 수 있다
- 12,840건 중 37건이 달랐고 그중 6건이 새 방식의 오류였다
- 켜고 발견하는 것과 켜기 전에 아는 것은 값이 다르다
- 끈 상태를 실제로 밟아 확인한다
- 플래그를 지울 조건과 시점을 같이 적는다