[QRworld] 문자 인증도 다 돈이야. Caffeine Cache와 IP Rate Limit

suhwani·2025년 6월 28일
0
post-thumbnail

이번 포스팅에서는 회원가입 시 문자 인증을 도입하고, 관련된 부가 기능을 도입한 내용에 대해 작성할 예정이다. 문자 인증은 외부 API(coolsms), 인증 번호 기억을 위한 로컬 캐시(Caffeine Cache), 요청 횟수 제한을 위한 Rate Limit 도입에 대한 이야기이다.

1-1. 문자 인증 및 메세지 전송 기능 도입

  • 외부 API 선택: coolsms

    NHN cloud, Naver cloud, Kakao 등 여러 플랫폼이 문자(카톡) 전송 클라우드 서비스를 지원하고 있다. 하지만 사업자로 등록한 경우에만 사용할 수 있게 되어있어 개인 프로젝트를 진행 중인 나에게는 맞지 않았다. 따라서 가격이 저렴하면서 개인이 쓰기에 제약이 없는 coolsms를 사용하기로 했다.
    coolsms는 단문 문자 20원, 장문 문자 50원으로 책정되어 있다.

  • 회원가입, 문자 인증 시퀀스

    인증 번호는 유효 기간이 필요하다. "10분 이내에 등록해야 한다"라는 조건이 필요하다. 이는 휘발성 데이터로 DB가 아닌 캐시를 사용하기로 했다. 인증 번호 뿐만 아니라 회원가입 하려는 전화번호가 인증되었는지 정보도 같이 캐시에 저장하여, 회원가입 시 인증된 전화번호인지 확인해야한다.

  • 캐시 사용 이유 및 캐시 유지 기간

    고려한 캐시는 3가지가 있다. 그 중 많은 기능을 지원하는 Caffeine Cache를 사용하기로 했다. Caffeine Cache는 특히 셋 중 성능이 가장 우수하다. 또한 커뮤니티와 문서화도 잘 되어 있어 적합하다고 판단했다.

    • 로컬 캐시를 선택한 이유
      • 서버가 여러 개로 늘어나면 캐시 일관성, 정합성을 보장할 수 없다. 로컬 캐시는 자신의 서버의 데이터만 보장하고, 다른 서버로 들어온 데이터까지 관여할 수 없다. 그렇기에 서버가 여러 개인 상황에서는 캐싱 서버(Redis)를 따로 두어 여러 서버에서 공유 자원을 쓰도록 한다.
      • 하지만 장애 발생 지점이 될 수 있다. 캐싱 서버의 메모리와 트래픽을 조절해줘야 하고 관리해야할 지점이 늘어나게 된다. 물론 비용도 발생할 수 있다. 가장 큰 이유는 현재 프로젝트에서 Redis를 도입해야할 만큼 부족한 점을 못 느꼈기에 트래픽이 증가하여 서버 여러 대가 필요한 순간이 온다면 캐싱 서버를 도입하기로 했다.

1-2. 코드 변경

위에서 설명했듯이 인증 번호 담은 캐시, 인증된 전화번호 담은 캐시 2개가 필요하다.
나는 인증 번호 유효기간(TTL)을 10분, 인증된 전화번호 유효기간(TTL)을 60분으로 설정하였다.

  • TTL(Time To Live): 넣은 순간부터 설정된 시간이 지나면 항목을 무조건 만료
  • TTL(Time To Idle): 마지막 접근 시점부터 설정된 시간 동안 접근이 없으면 만료
인증번호 담은 캐시: verficationCodeCache
[ <전화번호1: 인증번호>, <전화번호2: 인증번호> ....]

인증된 전화번호를 담은 캐시: verifiedPhoneNumberCache
[ <전화번호1: true>, <전화번호2: true> ]

  • 같은 전화번호로 2번의 인증 문자를 요청한다면??
    1. 처음 인증 문자 요청
      • 캐시에 <전화번호1: 인증번호1> 저장 후 문자 발송
    2. 같은 전화번호로 인증 문자 반복 요청
      • 새롭게 생성된 인증번호2를 사용하여 <전화번호1: 인증번호2> 변경
      • 기존 인증번호1 삭제되어 동일한 전화번호, 인증번호1을 사용하여 인증 시 실패

1-3. 동작 확인

핸드폰 화면을 캡쳐하여 화질이 좋지 못하지만, 동작 테스트 완료..!!

2-1. Rate Limit, 불필요한 요청은 비용 발생으로 이어진다.

위와 같이 요청당 서버에서 비용을 지불해야하는 API가 있다면, 사용자의 불필요하거나 남용되는 요청을 제한할 필요가 있다. 이 때 사용자마다 요청 횟수를 제한하는 기능을 Rate Limit이라고 한다.

  • Rate Limit
    • 소수의 클라이언트가 과도한 호출로 서버, DB, 외부 API 자원을 독점하지 못하게 제어하는 것
    • 비용 절감 및 디도스 공격, 비정상 트래픽(봇)에 대한 방어를 수행
    • 알고리즘: Fiexed Window Counter, Sliding Window Log, Sliding Window Counter, Token Bucket

2-2. 구현 방법

  1. 웹서버(Nginx) 사용: Spring 앞에 웹서버를 두고 IP를 차단
  2. 라이브러리 기반: 토큰 버킷 알고리즘을 사용하여, 서버 내에서 Rate Limit을 구현을 도와주는 라이브러리 사용
  3. Redis 사용: IP를 캐싱하여 토큰 버킷 알고리즘을 구현

Bucket4j + Caffeine 캐시 사용

  • 왜 라이브러리를 쓰는 방법을 선택했나?
    • 웹서버를 쓴다면, IP Rate Limit 정책이 변경되거나 특정 API를 개발하고 엔드포인트에 맞게 세세한 Rate Limit을 적용하기 위해서는 수정하고 재배포하는 일을 반복하게 된다. 이보다는 서버 내에서 적용하여 수정하면 빠르게 적용할 수 있다. 또한 이번 기능을 위해 Nginx를 추가하고, 설정하기에는 리소스가 크다고 생각했다.
    • Redis는 단일 발생 장애 지점이 될 수 있고, 비용도 발생하기에, 현재 Redis를 도입하는 것은 "굳이" 라는 생각이 들어 다른 방법을 택하게 되었다.
  • 그럼 Bucket4j만 사용하면 되는데 왜 Caffeine Cache와 함께 사용했나?
    • Bucket4j는 토큰 버킷 알고리즘을 쉽게 사용하라고 Bucket과 그에 맞는 알고리즘, 설정 등을 제공한다. 그럼 우리는 Bucket을 어딘가에 저장해야 한다. 여러 쓰레드에서 접근하기에 ConcurrentHashMap을 많이 사용하게 된다.
    • 하지만 키, 값이 영구적으로 남아있게 되고 메모리를 계속 차지할 가능성이 있다. 또한 추후 Redis로의 변경을 위해 Caffeine Cache를 도입하여 통계정보까지 확인하고자 한다.
    • TIL을 설정하고, Caffeine Cache에서 제공하는 스케줄러를 통해 만료된 데이터를 제거한다. TimerWheel이 만료 노드만 스캔해 제거하여 오버헤드가 매우 적다.

컨트롤러별 Rate Limit 다르게 적용하기 with 커스텀 어노테이션

  • 디도스 제어: 전체 API 대상, soft하게 적용, 1초 10번 ➡️ DEFAULT
  • 비용 발생 제어: 문자 인증 API 대상, 10분 5번 ➡️ SMS_SEND
  • 동일 IP 요청 제어: Guestbook 등록 API 대상, 1분 5번 ➡️ GUESTBOOK_WRITE

⬆️ RateLimitPlan Enum 정의

⬆️ 각 컨트롤러별 올바른 PLAN 적용

2-3. 동작 확인

k6를 통해 GUESTBOOK_WRITE 적용된 컨트롤러에 1분 내로 10번의 요청을 시도했다.
구현 의도에 맞게 같은 IP로는 5번의 요청까지만 적용되고, 이후 요청에서 Error가 반환되는 것을 볼 수 있다.
‼️ 전화번호가 겹치면 안되기에 0번부터 9번까지 iteration 번호를 적용해 다르게 요청하였다.

3-1. 피시방, 회사, 단체 등 공용 IP를 개별로 분리해야 한다.

만약 공용 IP를 쓰는 곳이라면 100명의 유저가 동시에 접속할 때, 정상적인 접근이지만 Rate Limit에서 차단하게 된다. 만약 jwt 토큰 또는 사용자 id를 기준으로 잡을 수 있는 API Rate Limit을 사용한다면 괜찮지만, 현재 로그인하지 않은 사용자가 등록을 할 수 있도록 하기 때문에 그럴 수 없다.
그럼 어떻게 해야할까? 나는 Device Id를 추가하기로 했다.
현재: PLAN 이름 + IP
개선: PLAN 이름 + IP + Device Id

3-2. 구현 방법 및 코드 개선

Device Id는 클라이언트에서 저장해놓고 매 요청마다 헤더에 추가하여 전송한다.
이 때 Device Id를 JWT 토큰으로 생성하고, 헤더로 받은 JWT 토큰을 파싱하여 uuid를 얻는다.

3-3. 개선 결과

key를 기준으로 변화된 내용을 정리
1. IP만 사용 ➡️ API마다 다른 기준을 적용 불가 ❌
2. PLAN + IP 사용 ➡️ API마다 다른 기준 적용 ✅, 공용 IP에서 유저 개별 적용 ❌
3. PLAN + IP + DeviceId ➡️ API마다 다른 기준 적용 ✅, 공용 IP에서 유저 개별 적용 ✅

profile
Backend-Developer

0개의 댓글