배움 __IL/addtionalBackEnd

SMS 발송 한도 관리(Redis VS MySQL)

Mo_bi!e 2025. 8. 31. 14:18

I. 들어가며

회사 서비스에서 SMS발송 시 한국 외 고객(국제 발송 : 전체 유저의 45%)에게는 YY서비스(CPaaS)을 사용해 SMS를 발송한다. 하지만 YY서비스은 일부 미국 통신사로는 발송이 실패하는 문제가 있었다. 이 부분을 보완하기 위해 국내 XX텔레콤(SMS Provider)을 통한 SMS fallback을 붙였다. 그런데 문제는 비용 국내 XX텔레콤(SMS Provider)의 국내SMS 와 국제 SMS간의 비용차이는 10배이다.

 

fallback은 SMS발송 품질을 위해 꼭 필요하지만, abuse가 발생하면 손해가 발생할 수있다. 그래서 하루 발송 한도 관리(Quota)가 반드시 필요했고, 한도 관리를 위한 방법으로 고려한 MySQL과 Redis를 고민하며 해결했다.

 

II. 문제정의

1. MySQL 적근방식

당초에는 MySQL에 “일일 카운터”를 이용하는 방식을 고려했다

  • 요구조건 명세
    • 즉 1일 1 Row insert → 발송 시마다 update로 카운팅
    • 하루 제한 도달 시 slack 알림

요구조건은 단순해 보이지만 문제점이 있다.

 

문제점

  1. 경합 : 다중 요청 시 트랜잭션/행 잠금 발생
    • 락, 트랜잭션 경합 등으로 *스파이크 발생이 가능하다.
    • (*스파이크 : 시스템의 응답 시간(latency)이나 자원 사용률(CPU, DB connection, lock wait 등)이 짧은 시간 동안 비정상적으로 치솟는 현상)
  2. 성능 저하 : 실패 → 재시도 경로에서 DB 왕복으로 지연
  3. Redis 대비 복잡 : 개발 및 운영과정에서 복잡

 

2. Redis 접근방식

MySQL의 트랜잭션 기반 카운팅은 경합과 운영 복잡성을 야기한다. 반면 Redis(Valkey)는 레이트 리밋(rate limit), 카운팅(counter)과 같은 캐시형 워크로드에 최적화되어 있다.

 

  1. 고성능 (Throughput & Latency)
    • 모든 연산이 메모리에서 수행되므로 디스크 기반 RDBMS보다 응답 지연(latency)이 짧고, 처리량(throughput)이 높다.
    • 초당 수천~수만 건의 증가 연산을 부담 없이 처리 가능하다.
  2. 동시성 처리 (Concurrency Handling)
    • INCR, SETNX 같은 원자적(atomic) 연산을 기본 제공한다.
    • 별도의 락(lock)이나 트랜잭션 제어 없이도 경합 없는 동시성 제어가 가능하다.
    • 스파이크 트래픽 상황에서도 안정적으로 카운트를 집계할 수 있다.
  3. 운영 단순성 (Operational Simplicity)
    • TTL(Time-To-Live)을 활용해 자정(EST) 기준 자동 리셋이 가능하다.
    • Redis처럼 TTL 자동 만료가 없으므로, 날짜별 row 관리가 필요. (별도 테이블도 create 필요)
  4. 워크로드 적합성 (Workload Suitability)
    • Redis는 세션 관리, 레이트 리밋, 카운팅과 같은 캐시형/일시적 데이터 관리 시나리오에 최적화되어 있다.
    • SMS 발송 한도 관리처럼 “정확한 영속성보다는 빠르고 일시적인 제약 관리”가 중요한 경우 특히 적합하다.

 

III. 설계 및 구현

1. 키 설계 (EST 기준 : 미국 동부)

  • 일일 카운트: sms:quota:smsProvider:{yyyyMMdd}
    → 오늘 하루 smsProvider 발송 횟수 저장, 자정(EST) 자동 만료
  • 멱등성: sms:idemp:smsProvider:{messageId}
    → 동일 메시지가 중복 발송되지 않도록 1분 TTL로 제어
  • Slack 알림: sms:quota:notify:{yyyyMMdd}
    → 한도 초과 알림은 하루 한 번만 보내도록 제약

 

2. 핵심 연산 방식

  • 하루 한도 카운팅
    • INCR로 원자적 증가
    • 카운트가 처음 생성된 경우에만 EXPIREAT을 설정하여 다음날 자정(EST)에 만료(TTL 설정) → 자동 리셋
  • 멱등성 보장
    • SETNX(setIfAbsent)로 동일한 messageId가 이미 처리된 경우 재발송 차단
    • TTL은 짧게(예: 1분) 두어 재시도 허용 가능
  • Slack 알림 1회/일
    • 초과 시 SETNX 성공한 최초 요청에서만 알림 발송
    • 키는 자정에 만료되어 다음날 다시 알림 가능

 

3. 원자성 확보 (Atomic Operations)

  • Redis의 INCR, SETNX, EXPIRE는 서버 단일 명령으로 실행되는 원자 연산이다.
  • 따라서 별도의 락(lock)이나 트랜잭션을 두지 않아도 동시성 안전성이 보장된다.
  • Spring Data Redis에서는 RedisAtomicLong이나 setIfAbsent API로 이를 바로 활용할 수 있다.
    • Lua 스크립트 이용이 필요치 않다.

 

4. 처리 순서 (사전 차단 방식 기준)

  1. 멱등성 체크
    • 키: sms:idemp:smsProvider:{messageId}
    • 연산: SETNX(+TTL 1분)
    • 실패(false)면 이미 처리 중/완료 → 즉시 SKIP(이중 발송 방지)
  2. 해외 발송 여부 확인
    • 해외가 아니면 기존 경로로 종료
    • 해외면 현재 사용량 읽기(캐시 목적)
      • 연산: 읽기(선택) → GET sms:quota:smsProvider:{yyyyMMdd}
      • 한도 초과로 예상되면 즉시 거부(“quota exceeded”)
      • 주의: 여기서는 증가(INCR)하지 않음 — 실제 국내 sms provider 발송이 아닐 수 있으므로
  3. smsProvider 로 실제 발송 성공 시 → 카운트 증가
    • 키: sms:quota:smsProvider:{yyyyMMdd}
    • 연산: INCR (원자), 처음 생성이면 EXPIREAT(다음날 00:00 EST) 설정
    • 의미: 실제 국내 sms provider 으로 국제 발송이 이루어졌을 때만 집계
  4. 초과 감지 시 Slack 알림(일 1회)
    • 방금 증가 결과가 limit 초과면 알림 트리거
    • 키: sms:quota:notify:{yyyyMMdd}
    • 연산: SETNX 성공 시에만 Slack 발송, 그리고 EXPIREAT(다음날 00:00 EST)
    • 하루 1회 보장

 

정리하면

 

  • Redis는 INCR + SETNX + EXPIREAT 세 가지 기본 연산만으로 일일 한도, 멱등성, 알림 제어를 단순하게 구현 가능하다.
  • 모든 연산이 원자적이므로 동시성 문제를 신경쓸 필요가 없고 MySQL과 비교하면 단순하다.

 

IV. 아키텍처

1. 문제점

  • 중복 + 휴먼 에러
    • SMS 발송 경로별로 sms 호출 메소드가 다수 존재
    • 모든 곳에서 Redis 한도 체크, 멱등성 검사, Slack 알림 로직을 직접 적용해야 함
    • 누락되거나 다르게 구현될 가능성 → 유지보수 시 휴먼 에러 위험
  • 흩어진 책임
    • 발송, 카운트, 멱등성, 알림 로직이 여러 모듈에 흩어져 존재
    • 정책 변경(예: 200건 → 500건, Slack 알림 조건 변경) 시 중복된 코드 모두 수정 필요

 

2. 문제 해결 (파사드 패턴 채택)

  1. 단일 진입점 제공
    • SmsGateway라는 파사드(or Gateway) 계층 추가
    • 발송 전후의 모든 제약(Quota, 멱등성, 알림)을 한 곳에서 관리
    • 다른 모듈은 오직 SmsGateway.sendSmsWithLimitCheck(...)만 호출 → 코드 단순화
  2. 복잡한 서브시스템 통합
    • Redis 한도 체크 (sms:quota:...)
    • 멱등성 보장 (sms:idemp:...)
    • Slack 알림 (sms:quota:notify:...)
      → 세부 로직을 SmsGateway 내부로 감춤 → 외부에 단순 인터페이스 제공
  3. 변경 용이성
    • 한도(200건 → 500건), 알림 조건, 멱등성 TTL 등 정책 변경 시 SmsGateway 내부 수정만으로 반영 가능
    • 분산 환경에서도 Redis 원자 연산으로 동시성 보장 → 서버 수와 무관하게 안전

 

V. 참고 : MySQL VS Redis 비교

카운팅/지연 트랜잭션/락 경합 → 스파이크 메모리 INCR → 저지연·고처리량
동시성 행 잠금, 경합 처리 필요 원자 연산(INCR/SETNX)로 단순
일일 리셋 날짜 row 관리 필요 TTL/EXPIREAT로 자정 자동 리셋
운영 복잡성 배치/정리/인덱스 고민 키 만료로 단순, 코드 변화 최소

 

VI. 나가며

 

이번 적용에서 아쉬운 점은 공용 인프라 모듈 부재였습니다.

  • Redis 미사용 모듈에도 새로 연결/설정을 매번 추가가 필요하고
  • SmsGateway/Quota 로직도 모듈별로 중복 구현했습니다.

이 경험으로 정책, 연결, 구성을 한곳에서 관리하는 공용 인프라 모듈의 필요성을 확실히 깨달았다.

 

또한 기존 틀에 붙여 쓰다 보니 SOLID 원칙(특히 SRP·OCP)을 온전히 지키지 못한 부분도 있었다.
→ 차후에는 infra 레벨에서 추상화 계층을 명확히 두고, 서비스 모듈은 단일 책임만 지도록 개선할 계획입니다.