Skip to content

phonil/Only4_Backend

 
 

Repository files navigation

프로젝트 요구사항

주제 : 대용량 통신 요금 명세서 정산 및 알림 발송 시스템

세부 요구

  • 가상 회원 100만명 존재
  • 청구서 100만 건 + 청구 항목 500만 건 존재

[정산 시스템]

  • 정해진 날짜에 배치로 청구 요금 정산 후 데이터베이스에 적재

    • 정산 데이터 중복 생성 x
  • 청구 요금은 사용자에게 메시지 플랫폼으로 발송

    • 고객별 고객이 정한 날짜에 발송
    • 고객이 금지한 시간에 발송 x

[메시지 플랫폼]

  • 이메일로 청구 내역 발송, 이메일 발송 실패 시 SMS로 발송
  • 실제 발송 x -> 이메일의 경우 1초 delay 후 완료 처리
    • 1% 확률로 발송 실패 처리
  • 이메일/SMS 템플릿 관리
  • 예약 발송 기능 제공
  • 시스템 금지 시간대 존재

작업 구성

1. 정산 배치(매월) - 2. 발송 배치(매일 혹은 특정 시간마다) - 3. 메시지 플랫폼(알림 발송 / 카프카)

- 배치 시스템은 Job 단위로 구분하며, chunk 단위로 작업 진행
- 정산 Job, 발송 Job 총 두 개의 Job으로 구분
- 각각의 Job은 1개의 Step으로 구성

1. 정산 Batch Job

🎯 목적: 고객들의 당월 요금 청구서 생성
- 고객의 이용 내역을 토대로 요금 청구서 생성
- 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에 적재하는 과정이 하나로 묶여야 한다고 판단

고민 요소

  • 배치의 중복 실행 방지 및 재시도 효율성에 대한 방법

2. 메시지 발송 요청 Batch Job

image
🎯 목적: 당월 요금 청구서 발송 이벤트 발행
- 생성된 요금 청구서 발송 이벤트를 Kafka로 발행
    - 발송 완료 처리되지 않은 요금 청구서 대상
    
📌 전제: 요금 청구서 및 청구 항목 데이터가 DB에 존재
👉 트리거: 매일 특정 시간 or 관리자 버튼 클릭 or 등록된 예약 발송 일시

Job: 회원별 요금 청구서 알림 발송 요청 생성
Step 1: Kafka에 알림 발송 요청 메시지 생성

Reader: 해당 날짜에 메시지를 받고 싶은 회원 및 해당 회원의 청구서와 청구 항목 조회
Processor: Reader에서 조회한 데이터를 회원 및 청구서에 맞는 DTO로 생성
Writer:
- Kafka에 해당 청구서에 대한 알림 요청 발행
	- Processor에서 생성한 DTO의 내용을 Email 토픽의 이벤트로 전달

현재 방식 선택 이유

  • 고객이 알림을 받고 싶은 날짜를 선택하기 때문에 매일 알림 대상을 선별해야 함
  • 알림을 받을 고객을 선정한 후, 금지 시간대를 피하여 알림 발송할 필요가 있음

고민 1. 회원 및 청구서 데이터 DB 조회 시점

방안
1. Batch에서 조회 후, 메시지에 포함
	- 메시지의 크기가 커짐
2. Kafka Consumer에서 회원 조회 후 알림 전송
	- Consumer에서 DB 조회를 진행해야 함

✅ 결정: Kafka Consumer에서 조회 후 알림 전송(Batch에서 회원, 청구서, 청구 항목을 담은 이벤트 발송)

  • consumer는 '메시지 발송'이라는 책임만 있으면 된다고 생각했고, DB 조회를 하게 된다면 단건 조회가 많이 발생할 것이라는 우려가 존재했음
  • 하지만, Batch에서 조회하고 청구서의 모든 데이터를 이벤트에 담기에는 이벤트 크기가 너무 커질 것이라고 판단

고민 2. HTML 생성의 책임

방안
1. 발송 요청 배치에서 HTML 생성
    - ‘배치는 알림 요청 생성, kafka는 알림 발송’이라는 명확한 책임 분리
    - 네트워크로 보내지는 메시지의 크기가 큼
2. Kafka Consumer에서 HTML 생성
    - 네트워크로 보내지는 메시지의 크기가 작음
    - Kafka Consumer에서 HTML 생성 작업을 거쳐야 함

✅ 결정: Kafka Consumer에서 HTML 생성

  • 생성된 HTML은 크기가 크고, 이벤트 크기는 작게 유지해야 하기 때문
  • HTML 렌더링 작업은 그렇게 길지 않음
    • CPU 작업이지만, 1초 딜레이라는 외부 API에 비해 비용이 낮음
  • 또한, Email Consumer의 작업이기도 함
    • SMS는 HTML 렌더링을 하지 않기 때문에 SMS Consumer와는 다른 작업이라는 의미가 있음

고민 3. 이벤트의 크기

발송 요청 batch → Kafka 이벤트의 크기에 대한 고민

방안
1. 이벤트에 bill_id, member_id 등 최소한의 정보만 포함
2. 이벤트에 청구서 HTML을 만들기 위한 모든 것을 포함
  - 이벤트의 크기는 작게 유지

✅ 결정: 이벤트에 bill_id, member_id 등 최소한의 정보만 포함

  • 이벤트의 크기를 작게 유지하기 위함

3. 메시지 전송 시스템 (kafka)

알림 발송 흐름 image

시퀀스 다이어그램 image

🎯 목적: 고객에게 (이메일로) 요금 청구서 전송
- 이메일: 회원별 요금 청구서를 (HTML 혹은 우리가 정의한 방식으로) 전송
    - 1% 확률로 발송 실패
- SMS: 이메일 전송 최종 실패 시 회원별 요금 청구서(의 요약본 혹은 우리가 정의한 방식으로)를 전송
- 이메일, SMS 외 다른 형태의 알림으로 전송 가능성 존재 고려

📌 전제: 요금 청구서 데이터가 DB에 존재
👉 트리거: 메시지 발송 이벤트 발행 시 실시간으로 처리

[작업 흐름]
Email 토픽의 파티션에 존재하는 이벤트를 Consume
작업 1: Consume한 이벤트에 존재하는 청구 관련 내용 조회
작업 2: 템플릿 조회 후, 청구 관련 내용 매핑하여 HTML로 변환
작업 3: 변환한 HTML을 사용자의 Email로 전송 요청

실패 1
- 정해진 횟수(3회 ~ 5회)만큼 Email 재시도
    - 지수 백오프를 사용하여 재시도 간격 조절

실패 2
정해진 재시도 횟수만큼 시도했으나 실패한 경우
    - '필요 내용만 추려서' SMS (토픽)의 이벤트로 전달 (SMS 토픽 처리 정책에 따라 달라짐)

실패 3
SMS 발송 역시 정해진 재시도 횟수만큼 시도했으나 실패한 경우
- DLQ(토픽)의 이벤트로 전달
- 발송 실패 메시지로 분류, 별도 처리 필요

고민 1. 알림 내용 구성을 위한 DB 조회

  • consumer에서 db를 조회하여 알림에 들어갈 청구서 정보를 조회해야 함
  • email consumer에서 조회 / 실패 시 sms consumer에서 또 조회

✅ 결정: consumer에서 db를 조회

  • 이벤트에 모든 내용을 담을 수 없고, 결국 청구서를 생성하기 위해서 consumer에서 db 조회를 해야 한다고 판단
  • 하지만, 메일 전송을 위해 매번 조회, 랜더링 과정을 거친다는 점이 고민됨

고민 2. 다른 플랫폼의 메시지 전송

image - 최초에는 email / 실패 시 sms 전송이 필요 - 추가로 늘어날 가능성이 있음

✅ 결정: 플랫폼 별 토픽 생성

  • 단일 토픽 + 인터페이스로 구현하게 된다면 Email -> SMS같이 이어지는 경우 처리가 복잡해 질 수 있음


Infra Architecture

image

1) Batch & Kafka Consumer를 ECS Fargate(서버리스) 로 운영한 이유

  • 운영 부담 제거: EC2 인스턴스 패치/스케일/장애 대응 없이 컨테이너(Task)만 실행.
  • 비용 효율(실행 시간 과금): 정산 배치는 특정 시간에만 실행되므로, 24시간 켜진 EC2 대신 필요할 때만 과금되는 구조가 유리.
  • 격리성 확보: 배치/컨슈머가 순간적으로 CPU·메모리를 많이 써도 API 같은 상시 서비스 영향 최소화(블라스트 레디어스 축소).

2) 역할별로 컴포넌트를 분리한 이유(“5개로 분리”의 핵심 논리)

  • 장애 격리: Kafka/Consumer/Batch 중 하나가 문제여도 API·DB까지 연쇄 장애로 번지는 리스크 감소.
  • 보안(네트워크 분리)
  • Public Subnet: 외부 트래픽을 받는 API/Nginx
  • Private Subnet: DB/Kafka/Consumer 등 외부 노출 금지 대상
  • 리소스 최적화: 컴포넌트별 병목이 다르므로(예: Kafka는 I/O·메모리, Batch는 CPU) 리소스/스케일 정책을 독립적으로 튜닝 가능.

3) Nginx(Reverse Proxy)를 둔 이유

  • 보안: 백엔드(Spring API)의 실제 IP/포트를 외부에 직접 노출하지 않고 앞단에서 차단/통제.
  • 확장성: 향후 API를 스케일아웃하면 Nginx가 단일 진입점으로 라우팅/분산 역할 수행(필요 시 레이트리밋/헤더 정책/TLS 종료도 가능).

4) Kafka Consumer에만 오토스케일링을 준 이유

  • 트래픽의 피크가 ‘정산 시점’에 집중: 배치 종료 직후 이벤트가 폭증하므로, 컨슈머는 탄력적 확장이 필요.
  • Backpressure/Lag 해소: 메시지가 쌓일 때 컨슈머 수를 늘려 지연 시간 최소화.
  • 비용 최적화: 평소에는 최소 개수로 운영, 피크에만 확장해 유휴 비용 감소.

About

통신사 요금 정산 및 알림 발송 요청 프로그램

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • Java 99.1%
  • Other 0.9%