개발 slecs

광고문의 도배 차단한 이중 rate limiting 설계

목차

광고 문의 폼 신청이 계속 반복되거나 악의적으로 도배되는 상황을 마주했다. 이를 해결하기 위해 서버와 클라이언트 양쪽에 rate limiting을 엮었고, 그 과정에서 배운 여러 트레이드오프를 정리해본다.

문제: 폼 도배와 운영 비용

사실 처음엔 이 작업이 우선순위 높은 항목이 아니었다. 하지만 팀에서 "같은 IP에서 오는 중복 문의가 이상하게 많다"는 피드백을 받으니 상황이 달라졌다. 광고 문의 같은 비즈니스 임팩트 있는 폼은 정보가 정확할수록 좋은데, 도배성 신청들이 섞이면 운영팀의 필터링 비용이 늘고 진짜 고객 신청을 놓치기 쉬워진다. 특히 우리 서비스 특성상 광고주 문의는 매출과 직결되니, 여기서의 ux/신뢰도는 직접적인 비즈니스 신호다.

설계: 이중 방어

서버와 클라이언트 양쪽에 방어층을 두기로 결정했다.

계층 전략 제약 목적
서버 (IP 기반) 1시간 3건 / 24시간 10건 신뢰할 수 있는 상한선 도배 차단, 저장소/메일 발송 비용 절감
클라이언트 5분 쿨다운 UX 친화적, 우발적 중복 제출 방지 사용자 실수 차단, 동시 다중 클릭 방지

IP 기반 rate limit를 서버에 넣은 이유는 명확하다. 같은 IP가 1시간에 3회 이상 요청하면 그건 보통 의도된 행동이다. 24시간 10건이라는 상한선은 정상 사용자(아이디당 하루 몇 건 물어보는 정도)의 패턴을 분석해서 정했다. IP를 기준으로 한 것도 의도적인데, 이메일이나 회원ID로는 제약이 약하기 쉽고(폼에 fake 입력 가능), IP는 네트워크 레벨이라 스푸핑이 상대적으로 어렵기 때문이다.

다만 IP 기반의 약점도 있다. 같은 사무실/데이터센터에서 여러 직원이 문의하면 한 IP로 집계되어 거짓양성(false positive)이 생길 수 있다. 이게 리스크라는 걸 팀에서 지적했고, 우리는 그 대신 limit 수치를 넉넉하게 잡기로 했다. "완벽한 차단"보다는 "명백한 도배는 막되, 정상 사용자가 불편하지 않게"라는 균형점을 찾은 것.

클라이언트 쿨다운은 별도의 역할을 한다. 5분 동안 버튼을 비활성화하는 것인데, 이건 사용자가 "아 방금 눌렀는데 안 갔나?" 하고 여러 번 클릭하는 우발적 도배를 막는 게 목적이다. 또한 폼 제출 후 피드백(성공 메시지, 로딩 상태)이 명확해야 이 쿨다운이 의미 있다. 그래서 src/components/AdInquiryWidget.astro에서 UI 상태 관리가 중요했고, 코드리뷰에서도 "submit 후 즉시 disabled 상태 표시"를 꼼꼼히 봤다.

계층별 방어의 철학

이 설계에서 배운 가장 큰 깨달음은, rate limiting은 한 곳만으로는 불충분하다는 것이다.

  • 클라이언트 쿨다운만 있으면: 개발자 도구로 쉽게 우회 가능. 진짜 도배자를 못 막는다.
  • 서버 rate limit만 있으면: 정상 사용자도 실패 응답(429 Too Many Requests)을 받아야 하고, UX가 어색하다. "방금 보냈는데 왜 또 제출이 안 돼?" 하는 문의가 생긴다.

그래서 둘 다. 클라이언트는 "먼저 방어하되, 우회되면 서버가 최종 검증"이라는 구조다. 팀과 이 트레이드오프를 논의할 때, 한 멤버가 좋은 질문을 던졌다: "IP limit이 너무 타이트하면 VPN 사용자나 이동통신 사용자가 피해 볼 수 있지 않나?" 맞는 지적이고, 그래서 우리는 limit을 조금 널널하게 잡았다. 도배를 100% 막기보다는 80-90% 막되, 정상 사용자 불만을 최소화한 것.

운영 관점

src/pages/api/ad-inquiry.ts의 rate limit 로직이 들어가면서 같이 봐야 할 것들이 생겼다:
- rate limit 초과 시 요청 로깅 (패턴 분석용)
- 누적 count를 어디에 저장할지 (Redis vs in-memory vs 데이터베이스)
- limit 초과 시 응답 메시지 (사용자가 "아, 잠깐 기다려야 하는 거구나" 알 수 있게)
- 모니터링 대시보드 (실제로 block되는 요청이 얼마나 되는지, false positive는 없는지)

우리는 Redis를 선택했다. 빠르고, TTL 자동 만료 기능이 rate limit에 딱 맞기 때문이다. 하지만 이게 인프라 복잡도를 약간 늘린다는 트레이드오프도 있었고, 팀과 "Redis 도입 전에 먼저 DB로 프로토타입해보자"는 대화도 나눴다. 결국 비즈니스 우선순위(광고주 신청 품질) vs 인프라 복잡도를 저울질한 끝에 Redis로 가기로 했다.

다음 단계

앞으로 이 시스템이 잘 작동하는지 보려면 몇 주간의 데이터가 필요하다. 특히 false positive(정상 사용자가 block된 경우)가 생기는지, 그리고 도배성 요청이 실제로 줄었는지를 확인할 것. 그때 필요하면 limit 수치를 미세 조정하거나, 더 정교한 휴리스틱(요청 내용 패턴 분석 같은)을 추가할 수도 있다. 하지만 지금은 "최소한의 것으로 빠르게 배포하고, 실제 데이터로 판단하자"는 원칙을 지키려고 한다.


🛒 이 글과 어울리는 추천 상품

*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.

댓글 0

첫 댓글 달아줘.