이야기를 디버깅하는 개발자.

개발과 창작 사이에서, 사람들이 자기만의 이야기를 만들 수 있는 도구와 기록을 만듭니다.

roslyn.dev 자세히보기

만드는 기록/서비스 운영기

회원가입 없는 서비스의 스팸 방지 최소 장치

Roslyn 2026. 9. 25. 09:00
반응형

회원가입을 요구하지 않는 서비스는 진입 장벽이 낮다는 장점이 있습니다. 대신 한 사람이 반복 요청을 보내도 누구인지 구분하기 어렵고, 공격과 정상 사용을 구분할 정보가 부족합니다.

저는 처음부터 복잡한 인증을 붙이기보다 서비스가 감당할 수 있는 행동의 범위를 정했습니다. 이번 글에서는 경로별 제한, 사용자 식별, 실패 응답, 관찰 지표를 최소 구성으로 나눠 보겠습니다.

막아야 할 행동을 먼저 정의합니다

스팸은 사용자라는 사람이 아니라 행동의 패턴으로 나타납니다. 짧은 시간에 같은 요청을 반복하거나, 파일 크기가 비정상적으로 크거나, 실패한 요청만 계속 보내는 식입니다. 무엇을 막을지 정하지 않고 방어 도구부터 붙이면 정상 사용도 함께 막힙니다.

저는 엔드포인트마다 비용과 피해를 적습니다. 읽기 요청과 데이터 생성 요청은 같은 제한을 쓰지 않습니다.

행동피해최소 장치

반복 읽기 대역폭·캐시 비용 캐시·경로별 제한
반복 쓰기 데이터 오염·알림 폭주 멱등 키·횟수 제한
큰 파일 저장·처리 비용 크기·시간·동시성 제한
실패 반복 로그·CPU 소모 백오프·차단·관찰

식별자는 완벽한 신원이 아니라 제한의 단위입니다

회원가입이 없으면 IP 주소, 쿠키, 임시 토큰, 요청 서명 등을 조합해 제한 단위를 만듭니다. 어느 하나도 완벽하지 않으므로, 제한을 우회할 수 있다는 사실을 전제로 피해 규모를 작게 만드는 것이 목표입니다.

IP만 사용하면 공유 네트워크의 정상 사용자를 함께 막을 수 있습니다. 반대로 쿠키만 믿으면 쉽게 지워집니다. 서비스 특성에 맞게 여러 신호를 사용하되 개인정보를 과도하게 저장하지 않습니다.

  • IP: 가장 단순하지만 공유망과 프록시의 영향을 받습니다.
  • 임시 토큰: 사용 흐름을 구분하지만 발급·폐기 정책이 필요합니다.
  • 멱등 키: 같은 작업의 중복 처리를 줄이는 데 유용합니다.

Rate Limiting은 사용자 경험과 함께 설계합니다

제한에 걸린 사용자가 이유를 알 수 없으면 정상 사용도 공격으로 느낍니다. 응답에는 재시도 가능한 시각과 요청 식별자를 포함하고, 내부 로그에는 어떤 정책에 걸렸는지 남깁니다.

ASP.NET Core나 Cloudflare의 Rate Limiting을 사용할 때도 기본값을 그대로 쓰지 않습니다. 경로·고객 유형·요청 비용별 정책을 나누고, 실제 부하 테스트에서 정상 사용자가 얼마나 자주 걸리는지 확인합니다.

  • 경로별: 로그인 없는 공개 기능과 관리자 기능을 다르게 제한합니다.
  • 비용별: 외부 API나 파일 변환에 더 엄격한 제한을 둡니다.
  • 완화: 잠시 뒤 재시도할 수 있는 요청은 차단 대신 대기시킵니다.

방어 장치도 오탐을 기록해야 개선할 수 있습니다

차단 수만 보면 방어가 잘되는 것처럼 보이지만 정상 사용자가 막힌 횟수는 보이지 않습니다. 제한에 걸린 요청의 경로·정책·응답 결과를 집계하고, 문의가 들어온 경우 해당 요청 식별자와 연결합니다.

로그에는 토큰이나 원문 입력을 남기지 않습니다. 어떤 정책이 작동했는지만 확인할 수 있으면 충분한 경우가 많습니다. 한 달 뒤에도 제한값을 그대로 둘지 기록을 보고 결정합니다.

운영 가능한 크기로 줄이는 순서

작은 도구나 서비스의 설계는 기능 목록을 늘리는 일보다 경계를 줄이는 일에 가깝습니다. 저는 먼저 입력과 출력, 실패했을 때 사용자가 되돌릴 수 있는 지점을 적습니다. 그 다음 실제 사용량이나 파일 한두 개로 기준선을 만들고, 가장 비싼 작업과 가장 위험한 작업을 따로 표시합니다. 이 과정을 거치면 ‘있으면 좋은 기능’과 ‘없으면 복구할 수 없는 기능’을 구분할 수 있습니다.

첫 버전은 관찰할 수 있어야 합니다. 요청 수·처리 시간·오류·복구 성공 여부처럼 다음 결정을 돕는 기록을 최소한으로 남깁니다. 자동화나 클라우드 기능을 붙일 때도 기본값과 상한을 먼저 정하고, 테스트용 자원과 운영 자원을 분리합니다. 실패했을 때 원상 복구할 수 없는 기능은 공개 범위를 좁혀 시작하는 편이 안전합니다.

  • 경계: 지원하는 입력·사용자·환경과 지원하지 않는 범위를 적습니다.
  • 기준선: 정상 입력 하나와 실패 입력 하나를 저장해 반복 테스트합니다.
  • 관찰: 비용·지연·오류·복구에 필요한 최소 지표만 수집합니다.
  • 되돌리기: 배포·삭제·마이그레이션을 취소하는 절차를 실제로 시험합니다.

다음 작업에서 다시 확인할 항목

한 번 적용한 방법이 언제나 같은 결과를 내는 것은 아닙니다. 사람과 서비스와 원고의 조건이 바뀌면 같은 원칙도 다른 판단을 요구합니다. 그래서 저는 글을 작성한 뒤 결론만 보관하지 않고, 결론이 성립한 조건과 다시 확인해야 할 조건을 함께 적습니다. 이 기록이 있으면 몇 달 뒤 글을 업데이트할 때 당시의 경험을 현재의 사실처럼 착각하지 않을 수 있습니다.

특히 버전·정책·가격·플랫폼 기능처럼 외부에서 바뀌는 내용은 조사한 날짜와 공식 문서의 주소를 남깁니다. 발행 시점에 문서가 달라졌다면 본문에 변경 사실을 표시하고, 직접 확인하지 못한 부분은 독자가 알 수 있도록 범위를 제한합니다. 경험을 공유하는 글도 다른 사람의 환경에 그대로 적용될 수 있으므로, 성공 사례보다 실패 조건을 함께 쓰는 편이 안전합니다.

마지막으로 이 글을 읽은 분이 바로 할 수 있는 행동은 하나로 줄입니다. 모든 항목을 한꺼번에 바꾸기보다 현재 상태를 기록하고, 작은 실험을 한 번 실행하고, 결과를 다시 적는 순서입니다. 그렇게 쌓인 기록이 다음 글의 소재가 되고, 처음의 판단을 더 정확하게 고쳐 쓰는 근거가 됩니다.

  • 범위 표시: 이 글의 결론이 적용되는 환경과 적용되지 않는 환경을 구분합니다.
  • 날짜 기록: 버전·정책·가격·문서를 확인한 날짜를 남깁니다.
  • 실패 조건: 어떤 상황에서는 이 방법을 쓰지 말아야 하는지 적습니다.
  • 작은 실행: 독자가 오늘 시도할 수 있는 가장 작은 행동을 고릅니다.
  • 후속 기록: 실행 결과와 다음에 바꿀 조건을 별도 메모로 남깁니다.

이 기록을 나중에 다시 읽을 때는 결과만 보지 않고 당시의 조건도 함께 확인합니다. 조건이 달라졌다면 결론을 그대로 복사하지 말고 현재의 입력과 제약을 다시 적습니다. 그 과정을 거쳐야 이 글이 단순한 경험담이 아니라 다음 판단을 돕는 작업 기록으로 남습니다.

마치며

회원가입 없는 서비스의 보안 목표는 모든 악용을 없애는 것이 아니라, 반복 요청이 서비스와 운영자의 시간을 모두 삼키지 못하게 하는 것입니다. 비용이 큰 공개 엔드포인트 하나부터 행동·식별·제한·관찰 네 칸을 설계해 보시기 바랍니다.

참고한 문서: ASP.NET Core Rate Limiting · Cloudflare Rate Limiting

반응형