현재 dabom-processor-usage의 usage 이벤트 처리 흐름은 아래와 같다.
- Kafka에서
usage-events를 수신한다. - payload를 검증한다.
familyId-customerId관계를 검증한다.- Redis warmup을 수행한다.
- Lua가
duplicate,status,shouldNotify를 계산한다. - Java가 Lua 결과를 해석한다.
- DB 정산을 직접 수행한다.
- notification 대상이면 Outbox에
PUBLISH_PENDING을 저장한다. - notification을 즉시 비동기 발행한다.
- 성공하면
SENT, 실패하면PUBLISH_PENDING을 유지한다. - notification 비대상이더라도, 같은
eventId에 pending row가 남아 있으면 즉시 발행을 다시 시도한다.
초입에서 아래를 검증한다.
- payload null 여부
familyId > 0customerId > 0bytesUsed > 0
이 검증은 가장 앞에서 잘못된 메시지를 차단하기 위한 단계다.
효과:
- Redis 상태 오염 방지
- DB 정산 오염 방지
- 불필요한 후속 처리 방지
familyId-customerId 관계를 Redis membership cache와 DB fallback으로 검증한다.
순서:
- Redis
family:{familyId}:members확인 - cache hit면 통과
- miss 또는 불일치면 DB fallback
- DB에도 없으면 invalid input
- Redis와 DB 조회가 실패하면 retryable 예외로 전파
이 단계는 usage 이벤트가 실제 가족 구성원 관계를 만족하는지 확인하는 단계다.
효과:
- 잘못된 family-customer 조합 차단
- Redis, Lua, DB, notification까지 잘못된 입력이 내려가지 않음
Lua 실행 전에 필요한 Redis 상태를 채운다.
대상:
- 가족 info
- 가족 remaining
- 개인 월 사용량
- policy constraints
warmup은 Redis에 값이 없을 때 DB를 읽어 필요한 키를 채우는 역할을 한다.
효과:
- Lua가 필요한 기준값을 항상 Redis에서 읽을 수 있음
- 월초 첫 이벤트나 캐시 miss 상황에서도 동일한 처리 경로 유지
Lua는 아래를 원자적으로 수행한다.
- duplicate 여부 확인
- 사용량 반영
- 차단 여부 판단
- 경고 상태 판단
- 알림 dedup 판단
- 결과 캐시 저장
대표 결과:
duplicatestatusshouldNotifytotalUsedremainingmonthlyUseduserRatiomonthlyLimit
왜 Lua를 쓰는가:
- Redis read/write와 상태 계산을 한 번에 수행할 수 있기 때문이다.
- 사용량 증가와 상태 판단을 분리하면 race condition이 생길 수 있다.
- Lua는 Redis 내부에서 원자적으로 실행되므로 같은 이벤트가 몰려도 일관된 결과를 만든다.
Java는 Lua status를 중앙 매퍼에서 해석한다.
지원하는 status:
NORMALWARNING_50WARNING_30WARNING_10MANUALAPP_BLOCKTIME_BLOCKMONTHLY_LIMIT_EXCEEDEDFAMILY_QUOTA_EXCEEDED
이 단계에서 아래가 정해진다.
- DB persist result
- notification 대상 여부
- notification payload 의미
즉 Lua는 상태 문자열을 반환하고, Java는 그 문자열을 후속 처리 규칙으로 바꾸는 역할을 맡는다.
DB 정산은 usage 서비스가 직접 수행한다.
usage_recordinsert 시도- 새 insert 성공 시에만
customer_quotafamily_quota반영
usage_record는 만들지 않음customer_quota차단 상태 반영
allowed와 blocked를 나누는 이유는 아래와 같다.
- allowed 이벤트는 실제 사용량 증가를 영속화해야 한다.
- blocked 이벤트는 사용이 허용되지 않았으므로 사용량 row를 만들지 않고 차단 상태만 반영한다.
같은 이벤트가 다시 들어와도 usage_record가 멱등 가드 역할을 한다.
notification은 아래 조건을 모두 만족할 때만 대상이다.
- status 기준으로 알림 대상
- Lua 결과 기준
shouldNotify = true
notification 대상이면:
- Outbox에
PUBLISH_PENDING저장 NotificationPayload를 직렬화해payload_json에 저장- usage 서비스가 즉시 비동기로 발행 시도
- 성공하면
SENT - 실패하면
PUBLISH_PENDING유지
notification 비대상이면 새 Outbox row는 만들지 않는다.
이 구조의 의미는 아래와 같다.
- 즉시성은 usage 서비스가 담당한다.
- 발행 실패 후 복구 기준점은 Outbox가 담당한다.
- notification 대상이 아닌 이벤트는 불필요한 DB row를 남기지 않는다.
sequenceDiagram
participant K as Kafka usage-events
participant U as Usage Service
participant M as Membership Cache
participant R as Redis/Lua
participant D as DB
participant O as Outbox
participant N as Kafka notification-events
K->>U: usage-events 1건
U->>U: payload 검증
U->>M: family-customer 검증
U->>R: warmup + Lua 실행
R-->>U: duplicate, status, shouldNotify
U->>D: DB 직접 정산
alt shouldNotify = true
U->>O: PUBLISH_PENDING 저장
U->>N: 비동기 발행 시도
alt ack success
U->>O: SENT 반영
else ack fail
U->>O: PUBLISH_PENDING 유지
end
else shouldNotify = false
U->>U: Outbox row 생성 없음
end
이 흐름에서 중요한 점은 아래 세 가지다.
- 실시간 판단과 영속 정산이 분리되어 있다.
- Redis/Lua는 빠른 판단
- DB는 최종 정산
- 멱등성이 구조 안에 들어가 있다.
- Redis dedup
usage_record기반 DB 멱등 정산
- notification은 즉시성과 복구성을 동시에 가진다.
- 즉시 비동기 발행
- Outbox 기반 후속 복구