- 가상 회원 100만명 존재
- 청구서 100만 건 + 청구 항목 500만 건 존재
[정산 시스템]
-
정해진 날짜에 배치로 청구 요금 정산 후 데이터베이스에 적재
- 정산 데이터 중복 생성 x
-
청구 요금은 사용자에게 메시지 플랫폼으로 발송
- 고객별 고객이 정한 날짜에 발송
- 고객이 금지한 시간에 발송 x
[메시지 플랫폼]
- 이메일로 청구 내역 발송, 이메일 발송 실패 시 SMS로 발송
- 실제 발송 x -> 이메일의 경우 1초 delay 후 완료 처리
- 1% 확률로 발송 실패 처리
- 이메일/SMS 템플릿 관리
- 예약 발송 기능 제공
- 시스템 금지 시간대 존재
1. 정산 배치(매월) - 2. 발송 배치(매일 혹은 특정 시간마다) - 3. 메시지 플랫폼(알림 발송 / 카프카)
- 배치 시스템은 Job 단위로 구분하며, chunk 단위로 작업 진행
- 정산 Job, 발송 Job 총 두 개의 Job으로 구분
- 각각의 Job은 1개의 Step으로 구성
🎯 목적: 고객들의 당월 요금 청구서 생성
- 고객의 이용 내역을 토대로 요금 청구서 생성
- BILL(청구서), BILL_ITEM(청구 항목) 테이블에 INSERT
📌 전제: 고객, 상품, 당월의 고객의 상품 이용 정보 데이터가 DB에 존재
👉 트리거: 스케줄러에 등록되어, 매달 특정 일시에 실행
Job: 회원별 사용 내역 정산 후 요금 청구서 및 요금 항목 생성
Step 1: 청구서(BILL) & 청구 항목(BILL_ITEM) 테이블에 데이터 생성
Reader: 회원을 기준으로 회원의 사용 요금 정보 조회
Processor: 청구 금액 계산 : 총 사용 금액 및 할인, 최종 납부 금액 계산
Writer:
- 청구서(BILL) 생성
- BILL_ITEM보다 먼저 생성되어야 함 (BILL_ID가 BILL_ITEM에 필요)
- 청구 항목(BILL_ITEM) 생성
- 청구 항목은 상품 등과의 FK 연결이 아닌, JSON 타입 스냅샷을 컬럼으로 가짐
- Step은 read - process - write 순서로 진행하므로 회원 단위로 조회해서 요금을 계산한 후 DB에 적재하는 과정이 하나로 묶여야 한다고 판단
- 배치의 중복 실행 방지 및 재시도 효율성에 대한 방법
🎯 목적: 당월 요금 청구서 발송 이벤트 발행
- 생성된 요금 청구서 발송 이벤트를 Kafka로 발행
- 발송 완료 처리되지 않은 요금 청구서 대상
📌 전제: 요금 청구서 및 청구 항목 데이터가 DB에 존재
👉 트리거: 매일 특정 시간 or 관리자 버튼 클릭 or 등록된 예약 발송 일시
Job: 회원별 요금 청구서 알림 발송 요청 생성
Step 1: Kafka에 알림 발송 요청 메시지 생성
Reader: 해당 날짜에 메시지를 받고 싶은 회원 및 해당 회원의 청구서와 청구 항목 조회
Processor: Reader에서 조회한 데이터를 회원 및 청구서에 맞는 DTO로 생성
Writer:
- Kafka에 해당 청구서에 대한 알림 요청 발행
- Processor에서 생성한 DTO의 내용을 Email 토픽의 이벤트로 전달
- 고객이 알림을 받고 싶은 날짜를 선택하기 때문에 매일 알림 대상을 선별해야 함
- 알림을 받을 고객을 선정한 후, 금지 시간대를 피하여 알림 발송할 필요가 있음
방안
1. Batch에서 조회 후, 메시지에 포함
- 메시지의 크기가 커짐
2. Kafka Consumer에서 회원 조회 후 알림 전송
- Consumer에서 DB 조회를 진행해야 함
✅ 결정: Kafka Consumer에서 조회 후 알림 전송(Batch에서 회원, 청구서, 청구 항목을 담은 이벤트 발송)
- consumer는 '메시지 발송'이라는 책임만 있으면 된다고 생각했고, DB 조회를 하게 된다면 단건 조회가 많이 발생할 것이라는 우려가 존재했음
- 하지만, Batch에서 조회하고 청구서의 모든 데이터를 이벤트에 담기에는 이벤트 크기가 너무 커질 것이라고 판단
방안
1. 발송 요청 배치에서 HTML 생성
- ‘배치는 알림 요청 생성, kafka는 알림 발송’이라는 명확한 책임 분리
- 네트워크로 보내지는 메시지의 크기가 큼
2. Kafka Consumer에서 HTML 생성
- 네트워크로 보내지는 메시지의 크기가 작음
- Kafka Consumer에서 HTML 생성 작업을 거쳐야 함
✅ 결정: Kafka Consumer에서 HTML 생성
- 생성된 HTML은 크기가 크고, 이벤트 크기는 작게 유지해야 하기 때문
- HTML 렌더링 작업은 그렇게 길지 않음
- CPU 작업이지만, 1초 딜레이라는 외부 API에 비해 비용이 낮음
- 또한, Email Consumer의 작업이기도 함
- SMS는 HTML 렌더링을 하지 않기 때문에 SMS Consumer와는 다른 작업이라는 의미가 있음
발송 요청 batch → Kafka 이벤트의 크기에 대한 고민
방안
1. 이벤트에 bill_id, member_id 등 최소한의 정보만 포함
2. 이벤트에 청구서 HTML을 만들기 위한 모든 것을 포함
- 이벤트의 크기는 작게 유지
✅ 결정: 이벤트에 bill_id, member_id 등 최소한의 정보만 포함
- 이벤트의 크기를 작게 유지하기 위함
🎯 목적: 고객에게 (이메일로) 요금 청구서 전송
- 이메일: 회원별 요금 청구서를 (HTML 혹은 우리가 정의한 방식으로) 전송
- 1% 확률로 발송 실패
- SMS: 이메일 전송 최종 실패 시 회원별 요금 청구서(의 요약본 혹은 우리가 정의한 방식으로)를 전송
- 이메일, SMS 외 다른 형태의 알림으로 전송 가능성 존재 고려
📌 전제: 요금 청구서 데이터가 DB에 존재
👉 트리거: 메시지 발송 이벤트 발행 시 실시간으로 처리
[작업 흐름]
Email 토픽의 파티션에 존재하는 이벤트를 Consume
작업 1: Consume한 이벤트에 존재하는 청구 관련 내용 조회
작업 2: 템플릿 조회 후, 청구 관련 내용 매핑하여 HTML로 변환
작업 3: 변환한 HTML을 사용자의 Email로 전송 요청
실패 1
- 정해진 횟수(3회 ~ 5회)만큼 Email 재시도
- 지수 백오프를 사용하여 재시도 간격 조절
실패 2
정해진 재시도 횟수만큼 시도했으나 실패한 경우
- '필요 내용만 추려서' SMS (토픽)의 이벤트로 전달 (SMS 토픽 처리 정책에 따라 달라짐)
실패 3
SMS 발송 역시 정해진 재시도 횟수만큼 시도했으나 실패한 경우
- DLQ(토픽)의 이벤트로 전달
- 발송 실패 메시지로 분류, 별도 처리 필요
- consumer에서 db를 조회하여 알림에 들어갈 청구서 정보를 조회해야 함
- email consumer에서 조회 / 실패 시 sms consumer에서 또 조회
✅ 결정: consumer에서 db를 조회
- 이벤트에 모든 내용을 담을 수 없고, 결국 청구서를 생성하기 위해서 consumer에서 db 조회를 해야 한다고 판단
- 하지만, 메일 전송을 위해 매번 조회, 랜더링 과정을 거친다는 점이 고민됨
- 최초에는 email / 실패 시 sms 전송이 필요
- 추가로 늘어날 가능성이 있음
✅ 결정: 플랫폼 별 토픽 생성
- 단일 토픽 + 인터페이스로 구현하게 된다면 Email -> SMS같이 이어지는 경우 처리가 복잡해 질 수 있음
- 운영 부담 제거: EC2 인스턴스 패치/스케일/장애 대응 없이 컨테이너(Task)만 실행.
- 비용 효율(실행 시간 과금): 정산 배치는 특정 시간에만 실행되므로, 24시간 켜진 EC2 대신 필요할 때만 과금되는 구조가 유리.
- 격리성 확보: 배치/컨슈머가 순간적으로 CPU·메모리를 많이 써도 API 같은 상시 서비스 영향 최소화(블라스트 레디어스 축소).
- 장애 격리: Kafka/Consumer/Batch 중 하나가 문제여도 API·DB까지 연쇄 장애로 번지는 리스크 감소.
- 보안(네트워크 분리)
- Public Subnet: 외부 트래픽을 받는 API/Nginx
- Private Subnet: DB/Kafka/Consumer 등 외부 노출 금지 대상
- 리소스 최적화: 컴포넌트별 병목이 다르므로(예: Kafka는 I/O·메모리, Batch는 CPU) 리소스/스케일 정책을 독립적으로 튜닝 가능.
- 보안: 백엔드(Spring API)의 실제 IP/포트를 외부에 직접 노출하지 않고 앞단에서 차단/통제.
- 확장성: 향후 API를 스케일아웃하면 Nginx가 단일 진입점으로 라우팅/분산 역할 수행(필요 시 레이트리밋/헤더 정책/TLS 종료도 가능).
- 트래픽의 피크가 ‘정산 시점’에 집중: 배치 종료 직후 이벤트가 폭증하므로, 컨슈머는 탄력적 확장이 필요.
- Backpressure/Lag 해소: 메시지가 쌓일 때 컨슈머 수를 늘려 지연 시간 최소화.
- 비용 최적화: 평소에는 최소 개수로 운영, 피크에만 확장해 유휴 비용 감소.

