테마
한빛유통 내부 감사가 지난 분기의 데이터 불일치 세 가지를 넘겼다. 결제 완료 주문 9건이 결제 대기(PENDING)로 되돌아갔고, 고객 17명의 적립금 잔액이 적립과 사용 이력의 합보다 모두 41,300원 적었고, 같은 주문에 환불이 두 번 나간 일이 4건 있었다. 세 곳 모두 에러 로그는 깨끗했다.
박팀장은 세 곳을 한 번에 정리하자고 했다. 개발팀은 "세 곳 다 처음 읽을 때 SELECT … FOR UPDATE로 잠그면 끝납니다"라고 제안했다.
T_ORD 주문: 100,000행 · 896블록 · PK_T_ORD (ORD_SEQ) · 이 문제에서는 배송 메모 DLV_MEMO를 더했다 · 결제 콜백과 배송 상태 배치가 ORD_ST, PAY_DT를 하루 약 46,000번 고친다(한 번에 수 ms)M_CUS 고객: 2,000행 · 30블록 · PK_M_CUS (CUS_ID) · 이 문제에서는 적립금 잔액 PNT_BAL을 더했다T_ORD_RFND 환불(이 문제를 위해 더한 테이블): RFND_SEQ(시퀀스 SQ_RFND) · ORD_SEQ · RFND_AMT · RFND_DT · 인덱스 IX_T_ORD_RFND_ORD (ORD_SEQ) · 한 주문의 환불 합계는 PAY_AMT를 넘을 수 없다-- ① 「배송 메모 수정」 화면 — 열 때 읽고, 상담원이 통화하며 2 ~ 5분 뒤 저장
SELECT ORD_SEQ, CUS_ID, ORD_ST, DLV_MEMO FROM T_ORD WHERE ORD_SEQ = :ord_seq;
UPDATE T_ORD SET DLV_MEMO = :memo, ORD_ST = :ord_st WHERE ORD_SEQ = :ord_seq;
COMMIT;
-- ② 적립금 적립 · 사용 — 주문 확정 배치와 결제 화면이 같은 프로시저를 부른다 (적립은 +, 사용은 -)
SELECT PNT_BAL INTO v_bal FROM M_CUS WHERE CUS_ID = :cus_id;
v_bal := v_bal + :delta;
IF v_bal < 0 THEN RAISE_APPLICATION_ERROR(-20021, '적립금이 모자랍니다'); END IF;
UPDATE M_CUS SET PNT_BAL = v_bal WHERE CUS_ID = :cus_id;
COMMIT;
-- ③ 「환불」 버튼 — 상담원이 금액을 넣고 누른다
SELECT PAY_AMT INTO v_pay FROM T_ORD WHERE ORD_SEQ = :ord_seq;
SELECT NVL(SUM(RFND_AMT), 0) INTO v_done FROM T_ORD_RFND WHERE ORD_SEQ = :ord_seq;
IF v_done + :amt > v_pay THEN RAISE_APPLICATION_ERROR(-20031, '환불 한도를 넘습니다'); END IF;
INSERT INTO T_ORD_RFND (RFND_SEQ, ORD_SEQ, RFND_AMT, RFND_DT)
VALUES (SQ_RFND.NEXTVAL, :ord_seq, :amt, SYSDATE);
COMMIT;
[① 시간표 — 주문 98,402]
10:11:02 상담원 화면 열기 → ORD_ST PENDING · DLV_MEMO '경비실'
10:11:40 결제 콜백 UPDATE ORD_ST = 'COMPLETE' · COMMIT
10:14:15 상담원 저장 → DLV_MEMO '문 앞' · ORD_ST = 'PENDING' (화면 값) · COMMIT ← 결제 완료가 되돌아갔다
[② 시간표 — 고객 CUS_001288 · 잔액 5,000]
19:20:31.402 주문 확정 배치 (적립 +1,200) SELECT → 5,000 · UPDATE 6,200 · COMMIT 19:20:31.409
19:20:31.404 결제 화면 (사용 -3,000) SELECT → 5,000 · 2,000 계산 · UPDATE → 기다림
19:20:31.409 깨어나 계산해 둔 2,000 을 쓴다 · COMMIT
→ 잔액 2,000 (맞는 값 3,200 · 적립 1,200 이 사라졌다)
[③ 시간표 — 주문 97,551 · PAY_AMT 32,000 · 상담원이 「환불」을 두 번 눌렀다]
15:02:10.20 요청 1 SUM → 0 · 0 + 32,000 ≤ 32,000 · INSERT · COMMIT 15:02:10.23
15:02:10.21 요청 2 SUM → 0 (요청 1 은 COMMIT 전) · INSERT · COMMIT 15:02:10.25
→ 환불 64,000 · 두 요청 모두 기다림 0 (서로 다른 새 행을 넣었다)
[처리의 길이와 빈도 — 지난 분기 하루 평균]
한 번의 길이 하루 횟수 같은 행이 겹치는 일
① 메모 수정 2 ~ 5분 (사람) 약 1,400 하루 0 ~ 2번
② 적립 · 사용 약 5ms 약 31,000 저녁 피크에 같은 고객이 분당 몇 번
③ 환불 약 15ms 약 260 버튼 두 번 · 주 1번쯤
T_ORD 를 고치는 결제 콜백 · 배송 배치 하루 약 46,000번 · 한 번에 수 ms