이미지 로딩 중...

Next.js 실전 운영 9편 - Rate Limiting과 DDoS 방어 - 슬라이드 1/11
A

AI Generated

2025. 11. 8. · 5 Views

Next.js 실전 운영 9편 - Rate Limiting과 DDoS 방어

실제 서비스 운영에서 필수적인 Rate Limiting과 DDoS 방어 전략을 배워봅니다. Next.js API Routes에서 요청 제한을 구현하고, 악의적인 트래픽으로부터 서비스를 보호하는 방법을 실무 중심으로 다룹니다.


목차

  1. Rate Limiting 기본 개념 - 무제한 요청을 막는 첫 번째 방어선
  2. Next.js API Route에 Rate Limiting 적용 - 실전 구현
  3. IP 기반 vs 사용자 기반 Rate Limiting - 올바른 식별 전략
  4. DDoS 방어 기본 전략 - 분산 공격에 대응하기
  5. CAPTCHA 통합 - 봇과 사람 구분하기
  6. Redis를 활용한 분산 Rate Limiting - 다중 서버 환경
  7. Cloudflare를 활용한 엣지 레벨 보호 - 서버 도달 전 차단
  8. 로깅과 모니터링 - 공격 패턴 분석하기
  9. 테스트와 검증 - Rate Limiting 동작 확인
  10. 실전 체크리스트 - 프로덕션 배포 전 확인사항

1. Rate Limiting 기본 개념 - 무제한 요청을 막는 첫 번째 방어선

시작하며

여러분의 API가 갑자기 느려지거나, 서버가 다운되는 경험을 해보신 적 있나요? 한 사용자가 1초에 수백 번씩 API를 호출하거나, 악의적인 봇이 무차별적으로 요청을 보내면 서버는 금방 마비됩니다.

이런 문제는 실제 서비스 운영에서 매우 흔하게 발생합니다. 의도적인 공격이 아니더라도, 버그가 있는 프론트엔드 코드가 무한 루프로 API를 호출하거나, 크롤러가 과도하게 데이터를 수집하려 할 때 서버 비용이 폭증하고 정상 사용자의 경험이 나빠집니다.

바로 이럴 때 필요한 것이 Rate Limiting입니다. 특정 시간 동안 허용할 요청 수를 제한하여, 서버 리소스를 보호하고 모든 사용자에게 공정한 서비스를 제공할 수 있습니다.

개요

간단히 말해서, Rate Limiting은 특정 사용자나 IP가 일정 시간 동안 보낼 수 있는 요청의 개수를 제한하는 기술입니다. 실제 서비스에서는 필수적인 보안 장치입니다.

예를 들어, 로그인 API는 1분에 5회로 제한하여 무차별 대입 공격을 막고, 검색 API는 1초에 10회로 제한하여 서버 부하를 관리할 수 있습니다. 이를 통해 서버 비용을 절감하고, DDoS 공격의 피해를 최소화할 수 있습니다.

기존에는 모든 요청을 무조건 처리했다면, 이제는 요청의 빈도를 추적하고 임계값을 초과하면 거부할 수 있습니다. Rate Limiting의 핵심 특징은 첫째, IP나 사용자 ID 기반으로 개별 추적이 가능하고, 둘째, 시간 윈도우(1분, 1시간 등)를 설정할 수 있으며, 셋째, 초과 시 적절한 에러 응답(429 Too Many Requests)을 반환한다는 점입니다.

이러한 특징들이 서비스의 안정성과 보안을 크게 향상시킵니다.

코드 예제

// lib/rate-limit.ts
import { LRUCache } from 'lru-cache';

type RateLimitOptions = {
  interval: number; // 시간 윈도우 (밀리초)
  uniqueTokenPerInterval: number; // 추적할 고유 토큰 수
};

export function rateLimit(options: RateLimitOptions) {
  // LRU 캐시로 요청 기록 저장
  const tokenCache = new LRUCache({
    max: options.uniqueTokenPerInterval || 500,
    ttl: options.interval || 60000,
  });

  return {
    check: (limit: number, token: string) => {
      const tokenCount = (tokenCache.get(token) as number[]) || [0];

      // 현재 요청 수가 제한을 초과하면 거부
      if (tokenCount[0] === 0) {
        tokenCache.set(token, [1]);
      } else if (tokenCount[0] >= limit) {
        return { success: false };
      } else {
        tokenCache.set(token, [tokenCount[0] + 1]);
      }

      return { success: true };
    },
  };
}

설명

이것이 하는 일: IP나 사용자별로 요청 횟수를 추적하여, 설정한 제한을 초과하면 추가 요청을 차단하는 미들웨어를 만듭니다. 첫 번째로, LRU(Least Recently Used) 캐시를 생성하여 각 토큰(IP 주소나 사용자 ID)별 요청 횟수를 메모리에 저장합니다.

LRU 캐시는 메모리 효율적이며, 오래된 기록은 자동으로 삭제되어 메모리 누수를 방지합니다. max 옵션으로 최대 추적 가능한 토큰 수를 설정하고, ttl로 각 기록의 유효 시간을 지정합니다.

그 다음으로, check 메서드가 실행되면서 해당 토큰의 현재 요청 횟수를 확인합니다. 캐시에서 토큰을 조회하여 첫 요청이면 카운트를 1로 초기화하고, 이미 제한에 도달했으면 success: false를 반환하여 요청을 거부합니다.

제한 내라면 카운트를 증가시키고 요청을 허용합니다. 마지막으로, 이 함수는 재사용 가능한 rate limiter 인스턴스를 반환하여 여러 API 라우트에서 동일한 설정을 공유할 수 있습니다.

각 API마다 다른 제한(예: 로그인은 5회/분, 검색은 100회/분)을 적용할 수도 있습니다. 여러분이 이 코드를 사용하면 서버 리소스 보호, 악의적 공격 방어, 공정한 자원 분배라는 세 가지 핵심 이점을 얻을 수 있습니다.

특히 비용이 많이 드는 API(데이터베이스 조회, 외부 API 호출 등)를 보호하는 데 매우 효과적입니다.

실전 팁

💡 환경별 다른 제한 설정: 개발 환경에서는 제한을 느슨하게(1000회/분), 프로덕션에서는 엄격하게(100회/분) 설정하여 개발 편의성과 보안을 모두 확보하세요.

💡 인증된 사용자 우대: 로그인한 사용자는 더 높은 제한(1000회/시간)을, 비로그인 사용자는 낮은 제한(100회/시간)을 적용하여 서비스 품질을 차별화하세요.

💡 Redis로 확장: 서버가 여러 대라면 메모리 캐시 대신 Redis를 사용하여 모든 서버가 동일한 rate limit 상태를 공유하도록 하세요.

💡 점진적 차단(Sliding Window): 고정된 시간 윈도우 대신 슬라이딩 윈도우를 사용하면 시간 경계에서의 burst 공격을 더 효과적으로 막을 수 있습니다.

💡 명확한 에러 메시지: 429 응답에 Retry-After 헤더를 포함하여 클라이언트가 언제 다시 시도해야 하는지 알려주면 사용자 경험이 개선됩니다.


2. Next.js API Route에 Rate Limiting 적용 - 실전 구현

시작하며

API를 만들었지만, 보호 장치 없이 그냥 배포하면 위험합니다. 누군가 악의적으로 공격하거나, 실수로 무한 루프가 발생하면 여러분의 서버는 순식간에 다운될 수 있습니다.

실제로 많은 스타트업이 초기에 Rate Limiting 없이 서비스를 런칭했다가, 갑작스러운 트래픽 폭증으로 서버 비용이 수백만 원 청구되거나 서비스가 마비되는 경험을 합니다. 특히 무료 체험이나 공개 API를 제공할 때 더욱 위험합니다.

바로 이럴 때 필요한 것이 API Route에 직접 Rate Limiting을 적용하는 것입니다. Next.js의 API Routes는 미들웨어 패턴으로 쉽게 보호할 수 있습니다.

개요

간단히 말해서, 각 API Route에서 요청을 처리하기 전에 rate limiter를 먼저 실행하여 허용 여부를 판단하는 것입니다. 실무에서는 민감한 엔드포인트(로그인, 회원가입, 결제)에 우선적으로 적용합니다.

예를 들어, 로그인 API에 1분당 5회 제한을 걸면 무차별 대입 공격을 효과적으로 차단할 수 있고, 검색 API에 초당 10회 제한을 걸면 서버 부하를 예측 가능한 수준으로 유지할 수 있습니다. 기존에는 요청이 오면 바로 데이터베이스를 조회했다면, 이제는 rate limit 체크 → 통과 시에만 비즈니스 로직 실행 순서로 처리합니다.

핵심 특징은 첫째, IP 주소로 사용자를 식별하고, 둘째, 제한 초과 시 429 상태 코드를 반환하며, 셋째, 남은 요청 횟수를 헤더로 알려줄 수 있다는 점입니다. 이를 통해 클라이언트는 자신의 상태를 파악하고 적절히 대응할 수 있습니다.

코드 예제

// pages/api/login.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { rateLimit } from '@/lib/rate-limit';

// 1분당 5회 제한 설정
const limiter = rateLimit({
  interval: 60 * 1000, // 1분
  uniqueTokenPerInterval: 500,
});

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  // IP 주소로 사용자 식별
  const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;

  // Rate limit 체크
  const result = await limiter.check(5, ip as string);

  if (!result.success) {
    return res.status(429).json({
      error: '너무 많은 요청입니다. 잠시 후 다시 시도해주세요.'
    });
  }

  // 정상 처리: 로그인 로직
  const { email, password } = req.body;
  // ... 로그인 처리

  return res.status(200).json({ success: true });
}

설명

이것이 하는 일: 로그인 API에 1분당 5회 요청 제한을 적용하여, 무차별 대입 공격을 차단하고 서버를 보호합니다. 첫 번째로, rate limiter 인스턴스를 API 파일 최상단에서 생성합니다.

이렇게 하면 서버가 시작될 때 한 번만 생성되고, 모든 요청에서 동일한 인스턴스를 재사용하여 메모리를 효율적으로 사용합니다. interval은 시간 윈도우, uniqueTokenPerInterval은 동시에 추적할 수 있는 최대 IP 수입니다.

그 다음으로, 요청이 들어오면 x-forwarded-for 헤더나 소켓 주소에서 실제 클라이언트 IP를 추출합니다. 프록시나 로드 밸런서 뒤에 있을 때는 x-forwarded-for가 실제 IP를 담고 있으므로 이를 우선 사용합니다.

이 IP를 토큰으로 사용하여 rate limiter에 전달합니다. 마지막으로, limiter.check(5, ip)로 해당 IP의 요청 횟수를 확인하고, 제한을 초과했으면 즉시 429 에러를 반환하여 요청을 거부합니다.

제한 내라면 정상적으로 로그인 로직을 실행합니다. 이 패턴을 사용하면 데이터베이스 조회 전에 악의적 요청을 차단하여 DB 부하도 크게 줄일 수 있습니다.

여러분이 이 코드를 사용하면 로그인 API의 보안이 크게 향상되고, 무차별 대입 공격으로 인한 계정 탈취 위험이 감소하며, 서버 리소스 낭비를 막을 수 있습니다. 특히 비밀번호 재시도 제한은 보안 모범 사례이자 OWASP 권장사항입니다.

실전 팁

💡 사용자별 제한: 로그인 성공 후에는 IP 대신 사용자 ID로 rate limit을 적용하면, 같은 IP의 다른 사용자들이 영향받지 않습니다.

💡 동적 제한 조정: 의심스러운 활동(여러 계정 시도)이 감지되면 해당 IP의 제한을 더 엄격하게(1회/분) 조정하는 적응형 전략을 사용하세요.

💡 화이트리스트: 관리자 IP나 신뢰할 수 있는 서비스는 제한에서 제외하여 운영 효율성을 높이세요.

💡 로깅과 모니터링: rate limit에 걸린 요청을 로깅하여 공격 패턴을 분석하고, 정상 사용자가 불편을 겪는지 확인하세요.

💡 Graceful Degradation: 429 에러 시 클라이언트에 백오프 전략(exponential backoff)을 구현하여 자동으로 재시도하도록 안내하세요.


3. IP 기반 vs 사용자 기반 Rate Limiting - 올바른 식별 전략

시작하며

Rate Limiting을 적용할 때 가장 중요한 결정 중 하나는 "누구를 기준으로 제한할 것인가?"입니다. 같은 공용 Wi-Fi를 쓰는 수십 명이 모두 차단당할 수도 있고, 악의적인 사용자가 계정을 여러 개 만들어 제한을 우회할 수도 있습니다.

실제로 카페나 공항 같은 공용 네트워크에서는 많은 사람이 동일한 공인 IP를 공유합니다. 이때 IP 기반으로만 제한하면 한 사람의 과도한 요청이 모든 사람의 접근을 막아버릴 수 있습니다.

반대로 사용자 ID 기반만 사용하면 로그인 전 공격(회원가입, 비밀번호 찾기)을 막을 수 없습니다. 바로 이럴 때 필요한 것이 상황에 맞는 올바른 식별 전략입니다.

인증 전후, API 종류, 위험도에 따라 다른 전략을 사용해야 합니다.

개요

간단히 말해서, 인증이 필요 없는 엔드포인트는 IP 기반, 인증된 요청은 사용자 ID 기반, 그리고 민감한 작업은 IP + 사용자 ID 조합으로 제한하는 것입니다. 각 방식에는 장단점이 있습니다.

IP 기반은 로그인 전 공격을 막을 수 있지만 공용 네트워크에서 문제가 생기고, 사용자 ID 기반은 정확한 제한이 가능하지만 다중 계정 공격에 취약합니다. 예를 들어, 로그인 API는 IP 기반, 프로필 수정은 사용자 ID 기반, 결제는 둘 다 체크하는 식으로 조합하면 효과적입니다.

기존에는 하나의 식별 방법만 사용했다면, 이제는 엔드포인트의 특성과 위험도에 따라 적절한 전략을 선택할 수 있습니다. 핵심 특징은 첫째, IP는 익명 요청 보호에 유용하고, 둘째, 사용자 ID는 정확한 개인별 제한이 가능하며, 셋째, 조합 전략으로 다층 방어가 가능하다는 점입니다.

이러한 접근은 보안과 사용자 경험의 균형을 맞춥니다.

코드 예제

// lib/advanced-rate-limit.ts
import { rateLimit } from './rate-limit';

const ipLimiter = rateLimit({
  interval: 60 * 1000,
  uniqueTokenPerInterval: 500,
});

const userLimiter = rateLimit({
  interval: 60 * 1000,
  uniqueTokenPerInterval: 5000,
});

export async function checkRateLimit(
  req: NextApiRequest,
  strategy: 'ip' | 'user' | 'both',
  limit: number
): Promise<{ allowed: boolean; reason?: string }> {
  const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
  const userId = req.session?.user?.id; // 세션에서 사용자 ID 추출

  if (strategy === 'ip' || strategy === 'both') {
    const ipResult = await ipLimiter.check(limit, ip as string);
    if (!ipResult.success) {
      return { allowed: false, reason: 'IP rate limit exceeded' };
    }
  }

  if (strategy === 'user' || strategy === 'both') {
    if (!userId) {
      return { allowed: false, reason: 'Authentication required' };
    }
    const userResult = await userLimiter.check(limit, userId);
    if (!userResult.success) {
      return { allowed: false, reason: 'User rate limit exceeded' };
    }
  }

  return { allowed: true };
}

설명

이것이 하는 일: 엔드포인트의 특성에 맞게 IP 기반, 사용자 기반, 또는 두 가지 모두를 조합한 rate limiting 전략을 선택할 수 있는 유연한 시스템을 만듭니다. 첫 번째로, IP와 사용자를 위한 별도의 rate limiter 인스턴스를 생성합니다.

각각 다른 캐시를 사용하여 독립적으로 관리되며, uniqueTokenPerInterval을 다르게 설정하여 메모리 사용을 최적화합니다. IP는 500개, 사용자는 5000개를 추적할 수 있도록 설정했습니다.

그 다음으로, checkRateLimit 함수가 전략(strategy) 파라미터에 따라 적절한 검사를 수행합니다. 'ip' 전략은 IP만, 'user'는 사용자 ID만, 'both'는 둘 다 체크합니다.

IP는 요청 헤더에서, 사용자 ID는 세션 객체에서 추출하며, 각각 독립적인 limiter로 검증합니다. 마지막으로, 어느 하나라도 제한을 초과하면 즉시 allowed: false를 반환하고 구체적인 이유를 포함합니다.

이를 통해 API 핸들러는 왜 요청이 거부되었는지 알 수 있고, 적절한 에러 메시지를 클라이언트에 전달할 수 있습니다. 'both' 전략은 특히 결제나 민감한 데이터 수정 같은 고위험 작업에 효과적입니다.

여러분이 이 코드를 사용하면 각 API의 특성에 맞는 최적의 보호를 제공하고, 공용 네트워크 문제와 다중 계정 공격을 모두 방어하며, 정상 사용자의 불편을 최소화할 수 있습니다. 특히 글로벌 서비스에서는 지역별 네트워크 특성을 고려한 유연한 전략이 필수적입니다.

실전 팁

💡 Fingerprinting 추가: IP와 사용자 ID 외에 브라우저 fingerprint(User-Agent, Accept-Language 등)를 조합하면 더 정교한 식별이 가능합니다.

💡 지역별 차별화: VPN이나 프록시가 많이 사용되는 지역은 사용자 기반 전략을 우선하고, 신뢰할 수 있는 지역은 IP 기반을 사용하세요.

💡 계층적 제한: 전체 IP에 100회/분 제한, 개별 사용자에 10회/분 제한을 동시에 적용하여 burst와 sustained attack을 모두 방어하세요.

💡 API 키 활용: 공개 API의 경우 API 키를 발급하고 이를 기준으로 제한하면 더 정확한 사용량 추적이 가능합니다.


4. DDoS 방어 기본 전략 - 분산 공격에 대응하기

시작하며

여러분의 서비스가 갑자기 수천, 수만 개의 다른 IP에서 동시에 공격받는다면 어떻게 하시겠습니까? 일반적인 Rate Limiting은 IP당 제한이므로, 공격자가 수천 개의 봇넷을 동원하면 각 IP가 제한 내에서 요청하더라도 전체 트래픽이 폭증합니다.

실제로 DDoS(Distributed Denial of Service) 공격은 분산된 수많은 장치에서 동시에 요청을 보내 서버를 마비시킵니다. 2022년 구글이 기록한 최대 DDoS 공격은 초당 4600만 건의 요청이었습니다.

중소 규모 서비스는 훨씬 작은 규모의 공격에도 쉽게 다운됩니다. 바로 이럴 때 필요한 것이 다층 DDoS 방어 전략입니다.

개별 IP 제한을 넘어, 전체 트래픽 모니터링, 패턴 감지, 자동 차단이 필요합니다.

개요

간단히 말해서, DDoS 방어는 비정상적인 트래픽 패턴을 감지하고, 의심스러운 요청을 필터링하며, 공격 시 자동으로 엄격한 모드로 전환하는 시스템입니다. 여러 계층에서 방어가 필요합니다.

첫째, CDN/프록시 레벨(Cloudflare, AWS Shield), 둘째, 애플리케이션 레벨(Next.js 미들웨어), 셋째, 인프라 레벨(방화벽, 로드 밸런서)에서 각각 보호 장치를 구축합니다. 예를 들어, Cloudflare는 초당 수백만 건의 요청을 필터링하고, 애플리케이션은 비즈니스 로직 레벨의 악용을 막으며, 방화벽은 네트워크 레벨 공격을 차단합니다.

기존에는 공격이 발생한 후 수동으로 대응했다면, 이제는 실시간으로 패턴을 분석하고 자동으로 방어 모드를 활성화할 수 있습니다. 핵심 특징은 첫째, 전체 요청률(requests per second)을 모니터링하고, 둘째, 의심스러운 패턴(같은 User-Agent, 순차적 IP 등)을 감지하며, 셋째, 임계값 초과 시 자동으로 제한을 강화한다는 점입니다.

이를 통해 대규모 공격에도 서비스를 유지할 수 있습니다.

코드 예제

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

let requestCount = 0;
let lastReset = Date.now();
const THRESHOLD = 1000; // 초당 1000 요청 임계값
const suspiciousIPs = new Set<string>();

export function middleware(request: NextRequest) {
  const now = Date.now();
  const ip = request.ip || request.headers.get('x-forwarded-for') || 'unknown';

  // 1초마다 카운터 리셋
  if (now - lastReset > 1000) {
    requestCount = 0;
    lastReset = now;
  }

  requestCount++;

  // 전체 요청률이 임계값 초과 시 공격 모드 활성화
  if (requestCount > THRESHOLD) {
    // 의심스러운 IP를 더 엄격하게 제한
    if (suspiciousIPs.has(ip)) {
      return new NextResponse('Service temporarily unavailable', {
        status: 503,
        headers: { 'Retry-After': '60' }
      });
    }
    // 새로운 IP는 경고 후 추적 목록에 추가
    suspiciousIPs.add(ip);
  }

  // CAPTCHA 챌린지 추가 (임계값의 80% 도달 시)
  if (requestCount > THRESHOLD * 0.8) {
    const challenge = request.headers.get('x-captcha-token');
    if (!challenge) {
      return NextResponse.json(
        { error: 'CAPTCHA required', challengeUrl: '/api/captcha' },
        { status: 403 }
      );
    }
  }

  return NextResponse.next();
}

설명

이것이 하는 일: Next.js 미들웨어에서 전체 트래픽을 실시간으로 모니터링하고, DDoS 공격 패턴을 감지하면 자동으로 방어 조치를 활성화합니다. 첫 번째로, 전역 변수로 초당 요청 수를 추적합니다.

매 초마다 카운터를 리셋하여 현재 트래픽 수준을 파악하고, 임계값(예: 1000 req/s)과 비교합니다. 이는 개별 IP 제한과 별개로, 전체 서버 용량을 보호하기 위한 장치입니다.

정상 트래픽이 이를 초과하면 서버 스케일 업이 필요하다는 신호입니다. 그 다음으로, 임계값을 초과하면 "공격 모드"로 전환하여 의심스러운 IP를 Set에 저장하고 추적합니다.

이미 추적 중인 IP의 추가 요청은 즉시 503 에러로 거부하고, Retry-After 헤더로 60초 후 재시도를 안내합니다. 이는 정상 클라이언트와 봇을 구분하는 데 도움이 됩니다(봇은 Retry-After를 무시하고 계속 요청).

마지막으로, 임계값의 80%에 도달하면 예방 조치로 CAPTCHA 챌린지를 요구합니다. 정상 사용자는 한 번의 CAPTCHA로 계속 이용할 수 있지만, 자동화된 봇은 이를 통과하기 어렵습니다.

이는 공격 초기 단계에서 효과적으로 봇 트래픽을 걸러냅니다. 여러분이 이 코드를 사용하면 대규모 분산 공격에도 서버가 완전히 다운되는 것을 방지하고, 정상 사용자의 접근을 최대한 유지하며, 공격 IP를 식별하여 영구 차단 목록에 추가할 수 있습니다.

특히 Cloudflare 같은 CDN과 함께 사용하면 네트워크 레벨과 애플리케이션 레벨 양쪽에서 보호받을 수 있습니다.

실전 팁

💡 Cloudflare 필수: Next.js 미들웨어는 애플리케이션 레벨 방어이므로, 반드시 Cloudflare나 AWS Shield 같은 인프라 레벨 DDoS 방어를 함께 사용하세요.

💡 요청 패턴 분석: 같은 User-Agent, 같은 Referer, 순차적 IP 주소 등 봇 특유의 패턴을 감지하여 더 빠르게 차단하세요.

💡 Rate Limit 동적 조정: 평소에는 100 req/s, 공격 감지 시 10 req/s로 자동 조정하여 정상 트래픽은 유지하면서 공격을 억제하세요.

💡 알림 시스템: 임계값 초과 시 Slack, 이메일, SMS로 즉시 알림을 보내 관리자가 상황을 모니터링하고 수동 대응할 수 있도록 하세요.

💡 블랙리스트 공유: 공격 IP를 외부 블랙리스트 DB(AbuseIPDB)에 보고하고, 다른 서비스의 블랙리스트를 가져와 사전에 차단하세요.


5. CAPTCHA 통합 - 봇과 사람 구분하기

시작하며

아무리 Rate Limiting을 강화해도, 사람처럼 행동하는 정교한 봇은 막기 어렵습니다. 느린 속도로 요청하거나, 다양한 IP를 사용하면 일반 사용자와 구분하기 힘듭니다.

실제로 현대의 봇은 매우 지능적입니다. 헤드리스 브라우저를 사용하고, 실제 사용자의 행동 패턴을 모방하며, IP를 계속 바꿉니다.

이런 봇은 Rate Limiting만으로는 막을 수 없고, 대량의 스팸 가입, 재고 사재기, 티켓 매크로 같은 악용이 발생합니다. 바로 이럴 때 필요한 것이 CAPTCHA(Completely Automated Public Turing test to tell Computers and Humans Apart)입니다.

사람만 통과할 수 있는 챌린지를 제시하여 봇을 효과적으로 걸러냅니다.

개요

간단히 말해서, CAPTCHA는 이미지 선택, 퍼즐 맞추기, 또는 보이지 않는 행동 분석을 통해 요청이 실제 사람에게서 왔는지 확인하는 시스템입니다. 실무에서는 Google reCAPTCHA v3나 hCaptcha가 널리 사용됩니다.

reCAPTCHA v3는 사용자 상호작용 없이 백그라운드에서 점수를 매기고, 의심스러운 경우에만 챌린지를 보여줍니다. 예를 들어, 회원가입, 로그인, 댓글 작성, 결제 같은 악용되기 쉬운 작업에 적용하면 봇 트래픽을 90% 이상 줄일 수 있습니다.

기존에는 모든 요청을 신뢰했다면, 이제는 의심스러운 요청에 추가 검증을 요구하여 봇을 선별적으로 차단할 수 있습니다. 핵심 특징은 첫째, 사용자 경험을 해치지 않으면서(reCAPTCHA v3는 보이지 않음) 봇을 차단하고, 둘째, 점수 기반으로 위험도를 판단하며, 셋째, 머신러닝으로 계속 진화하는 봇 패턴에 대응한다는 점입니다.

이는 현대적인 봇 방어의 핵심입니다.

코드 예제

// pages/api/signup.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { checkRateLimit } from '@/lib/advanced-rate-limit';

async function verifyCaptcha(token: string): Promise<boolean> {
  const secretKey = process.env.RECAPTCHA_SECRET_KEY;

  const response = await fetch(
    'https://www.google.com/recaptcha/api/siteverify',
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: `secret=${secretKey}&response=${token}`,
    }
  );

  const data = await response.json();

  // reCAPTCHA v3는 0.0 ~ 1.0 점수 반환 (1.0 = 사람일 확률 높음)
  return data.success && data.score > 0.5;
}

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  // Rate limit 체크
  const rateLimit = await checkRateLimit(req, 'ip', 10);
  if (!rateLimit.allowed) {
    return res.status(429).json({ error: rateLimit.reason });
  }

  // CAPTCHA 검증
  const captchaToken = req.body.captchaToken;
  const isHuman = await verifyCaptcha(captchaToken);

  if (!isHuman) {
    return res.status(403).json({
      error: '봇으로 판단되었습니다. 다시 시도해주세요.'
    });
  }

  // 정상 처리: 회원가입 로직
  const { email, password } = req.body;
  // ... 가입 처리

  return res.status(201).json({ success: true });
}

설명

이것이 하는 일: 회원가입 API에 CAPTCHA 검증을 추가하여, Rate Limiting만으로는 막을 수 없는 정교한 봇 공격을 차단합니다. 첫 번째로, verifyCaptcha 함수가 클라이언트에서 받은 CAPTCHA 토큰을 Google의 검증 API로 전송합니다.

이 토큰은 프론트엔드에서 reCAPTCHA 라이브러리가 생성한 것으로, 사용자의 행동 패턴(마우스 움직임, 클릭 패턴, 브라우징 히스토리 등)을 분석한 결과를 담고 있습니다. 그 다음으로, Google API는 success(토큰 유효성)와 score(0.0~1.0, 사람일 확률)를 반환합니다.

점수가 0.5 이상이면 사람으로 간주하고, 미만이면 봇으로 판단합니다. 이 임계값은 서비스의 보안 요구사항에 따라 조정할 수 있습니다(더 엄격하게는 0.7, 더 느슨하게는 0.3).

마지막으로, Rate Limiting과 CAPTCHA를 모두 통과한 요청만 실제 회원가입 로직을 실행합니다. 이는 다층 방어 전략으로, Rate Limit은 대량 공격을, CAPTCHA는 정교한 봇을 각각 차단합니다.

정상 사용자는 아무것도 느끼지 못하지만(v3는 보이지 않음), 봇은 효과적으로 걸러집니다. 여러분이 이 코드를 사용하면 봇 가입을 90% 이상 줄이고, 스팸 계정 생성을 막으며, 실제 사용자 데이터의 품질을 크게 향상시킬 수 있습니다.

특히 프로모션이나 경품 이벤트 시 봇의 악용을 효과적으로 방지합니다.

실전 팁

💡 점수별 차등 대응: 0.7 이상은 바로 통과, 0.3~0.7은 추가 인증 요구, 0.3 미만은 차단하는 식으로 세밀하게 조정하세요.

💡 프론트엔드 통합: 회원가입 폼에 reCAPTCHA v3 스크립트를 로드하고, 폼 제출 시 자동으로 토큰을 생성하여 전송하도록 구현하세요.

💡 대체 CAPTCHA: reCAPTCHA 대신 hCaptcha(프라이버시 중시)나 Cloudflare Turnstile(무료, 간편)도 고려해보세요.

💡 로깅과 분석: 거부된 요청의 CAPTCHA 점수를 로깅하여 임계값이 적절한지, 정상 사용자가 차단당하는지 모니터링하세요.

💡 Fallback 전략: Google API가 다운되거나 느릴 때를 대비해, 타임아웃 설정과 fallback 로직(일시적으로 통과 또는 대체 검증)을 준비하세요.


6. Redis를 활용한 분산 Rate Limiting - 다중 서버 환경

시작하며

여러분의 서비스가 성장하여 서버를 여러 대로 확장했을 때, 메모리 기반 Rate Limiting은 문제가 됩니다. 서버 A에서 100번 요청하고 서버 B에서 100번 더 요청하면 총 200번이 허용되어 제한이 무의미해집니다.

실제로 로드 밸런서 뒤에 여러 서버가 있으면, 각 서버가 독립적인 메모리를 가지므로 동일한 IP의 요청 횟수를 공유할 수 없습니다. 이는 공격자가 제한을 서버 수만큼 곱하여 우회할 수 있다는 뜻입니다.

10대 서버라면 제한이 사실상 10배가 됩니다. 바로 이럴 때 필요한 것이 Redis 같은 중앙 집중식 저장소입니다.

모든 서버가 동일한 Redis 인스턴스를 공유하여 일관된 Rate Limiting을 적용할 수 있습니다.

개요

간단히 말해서, Redis는 모든 서버가 공유하는 초고속 인메모리 데이터베이스로, Rate Limiting 카운터를 중앙에서 관리하는 역할을 합니다. Redis의 장점은 첫째, 모든 서버가 동일한 상태를 공유하고, 둘째, 원자적 연산(INCR, EXPIRE)으로 동시성 문제 없이 카운터를 증가시키며, 셋째, 초 단위 TTL로 자동으로 오래된 데이터를 삭제한다는 점입니다.

예를 들어, 3대의 Next.js 서버가 동일한 Redis를 바라보면, 어느 서버로 요청이 가든 동일한 제한이 적용됩니다. 기존에는 서버마다 독립적인 제한을 적용했다면, 이제는 모든 서버에서 일관된 제한을 강제할 수 있습니다.

핵심 특징은 첫째, 원자적 연산으로 race condition을 방지하고, 둘째, TTL로 메모리 관리가 자동이며, 셋째, Redis Cluster로 수평 확장이 가능하다는 점입니다. 이는 대규모 프로덕션 환경의 필수 요소입니다.

코드 예제

// lib/redis-rate-limit.ts
import Redis from 'ioredis';

const redis = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: parseInt(process.env.REDIS_PORT || '6379'),
  password: process.env.REDIS_PASSWORD,
});

export async function checkRedisRateLimit(
  key: string,
  limit: number,
  windowSeconds: number
): Promise<{ allowed: boolean; remaining: number }> {
  const redisKey = `rate_limit:${key}`;

  // 원자적 연산으로 카운터 증가
  const current = await redis.incr(redisKey);

  // 첫 요청이면 TTL 설정 (자동 만료)
  if (current === 1) {
    await redis.expire(redisKey, windowSeconds);
  }

  // 제한 확인
  if (current > limit) {
    return {
      allowed: false,
      remaining: 0
    };
  }

  return {
    allowed: true,
    remaining: limit - current
  };
}

// 사용 예시
export async function apiHandler(req: NextApiRequest, res: NextApiResponse) {
  const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;

  // 1분에 60회 제한
  const result = await checkRedisRateLimit(ip as string, 60, 60);

  if (!result.allowed) {
    return res.status(429).json({
      error: 'Too many requests',
      retryAfter: 60
    });
  }

  // X-RateLimit 헤더 추가
  res.setHeader('X-RateLimit-Limit', '60');
  res.setHeader('X-RateLimit-Remaining', result.remaining.toString());

  // 정상 처리
  return res.status(200).json({ success: true });
}

설명

이것이 하는 일: 다중 서버 환경에서 Redis를 중앙 저장소로 사용하여, 모든 서버가 동일한 Rate Limiting 상태를 공유하고 일관되게 적용합니다. 첫 번째로, Redis 클라이언트를 생성하고 환경 변수로 연결 정보를 설정합니다.

ioredis 라이브러리는 자동 재연결, 클러스터 지원, 파이프라인 등 프로덕션에 필요한 기능을 제공합니다. 한 번만 인스턴스를 생성하여 모든 요청에서 재사용합니다.

그 다음으로, INCR 명령으로 카운터를 원자적으로 증가시킵니다. 이는 여러 서버가 동시에 같은 키를 증가시켜도 race condition 없이 정확한 카운트를 보장합니다.

만약 현재 값이 1이면(첫 요청), EXPIRE로 TTL을 설정하여 지정된 시간 후 자동으로 키가 삭제되도록 합니다. 이로써 메모리 누수를 방지합니다.

마지막으로, 현재 카운트가 제한을 초과하면 allowed: false를 반환하고, 그렇지 않으면 남은 요청 횟수와 함께 허용합니다. API 핸들러는 이를 받아 X-RateLimit-Remaining 헤더로 클라이언트에 알려주어, 클라이언트가 얼마나 더 요청할 수 있는지 파악할 수 있게 합니다.

여러분이 이 코드를 사용하면 서버를 몇 대로 확장하든 일관된 Rate Limiting을 유지하고, 공격자가 로드 밸런서를 이용한 우회를 할 수 없으며, Redis의 높은 성능(수만 TPS)으로 지연 없이 빠르게 처리할 수 있습니다. 특히 AWS ElastiCache나 Redis Cloud를 사용하면 관리 부담 없이 프로덕션 환경을 구축할 수 있습니다.

실전 팁

💡 Sliding Window 알고리즘: 고정 윈도우 대신 ZADDZREMRANGEBYSCORE로 슬라이딩 윈도우를 구현하면 더 정확한 제한이 가능합니다.

💡 Redis 고가용성: Redis Sentinel이나 Cluster를 사용하여 Redis 서버 장애 시에도 서비스가 중단되지 않도록 구성하세요.

💡 Fallback 전략: Redis 연결 실패 시 메모리 기반으로 fallback하여 일부 서버에서라도 제한을 적용하도록 하세요.

💡 배치 처리: 여러 키를 한 번에 확인해야 한다면 Redis Pipeline이나 Lua 스크립트로 네트워크 왕복을 줄이세요.

💡 모니터링: Redis의 메모리 사용량, 초당 명령 수, 응답 시간을 모니터링하여 병목을 사전에 발견하세요.


7. Cloudflare를 활용한 엣지 레벨 보호 - 서버 도달 전 차단

시작하며

아무리 애플리케이션에서 Rate Limiting과 DDoS 방어를 잘 구현해도, 공격 트래픽이 여러분의 서버에 도달하면 이미 대역폭과 서버 리소스를 소비합니다. 초당 수백만 건의 요청이 서버까지 오면 Rate Limiting을 체크하는 것조차 부담입니다.

실제로 대규모 DDoS 공격은 네트워크 대역폭을 포화시키거나, 서버의 CPU/메모리를 고갈시켜 정상 요청조차 처리할 수 없게 만듭니다. 애플리케이션 레벨 방어만으로는 이런 공격을 막을 수 없고, 서버 앞단에서 필터링이 필요합니다.

바로 이럴 때 필요한 것이 Cloudflare 같은 CDN/WAF 서비스입니다. 전 세계에 분산된 엣지 서버에서 트래픽을 먼저 검사하고, 악의적인 요청은 서버에 도달하기 전에 차단합니다.

개요

간단히 말해서, Cloudflare는 여러분의 서버 앞에 있는 거대한 방어막으로, DDoS 공격, 봇 트래픽, SQL 인젝션 등을 자동으로 감지하고 차단합니다. Cloudflare의 핵심 기능은 첫째, 전 세계 320개 이상의 데이터센터에서 트래픽을 분산하고, 둘째, 초당 수천만 건의 공격을 필터링하며, 셋째, WAF 규칙으로 애플리케이션 레벨 공격도 막는다는 점입니다.

예를 들어, SQL 인젝션 시도, XSS 공격, 봇 크롤링을 자동으로 감지하여 차단하고, 정상 트래픽만 서버로 전달합니다. 기존에는 모든 트래픽이 직접 서버로 왔다면, 이제는 Cloudflare를 통과한 깨끗한 트래픽만 받게 됩니다.

핵심 특징은 첫째, 무제한 DDoS 방어(Free 플랜도 포함), 둘째, Bot Management로 악성 봇 차단, 셋째, 캐싱으로 서버 부하 감소입니다. 이는 현대 웹 서비스의 표준 아키텍처입니다.

코드 예제

// next.config.js - Cloudflare 최적화 설정
module.exports = {
  // Cloudflare의 CF-Connecting-IP 헤더 신뢰
  async headers() {
    return [
      {
        source: '/api/:path*',
        headers: [
          {
            key: 'X-Frame-Options',
            value: 'DENY',
          },
          {
            key: 'X-Content-Type-Options',
            value: 'nosniff',
          },
        ],
      },
    ];
  },
};

// lib/get-real-ip.ts - Cloudflare 환경에서 실제 IP 추출
export function getRealIP(req: NextApiRequest): string {
  // Cloudflare는 CF-Connecting-IP 헤더에 실제 IP 전달
  const cfIP = req.headers['cf-connecting-ip'];
  if (cfIP && typeof cfIP === 'string') {
    return cfIP;
  }

  // Fallback: X-Forwarded-For
  const forwarded = req.headers['x-forwarded-for'];
  if (forwarded) {
    const ips = (Array.isArray(forwarded) ? forwarded[0] : forwarded).split(',');
    return ips[0].trim();
  }

  return req.socket.remoteAddress || 'unknown';
}

// Cloudflare WAF 규칙 예시 (Cloudflare Dashboard에서 설정)
/*
Rule 1: Rate Limiting
- When: Request rate > 100 req/min per IP
- Then: Challenge with CAPTCHA

Rule 2: Bot Detection
- When: Bot Score < 30 (likely bot)
- Then: Block

Rule 3: Geo Blocking
- When: Country not in [KR, US, JP]
- Then: Challenge or Block

Rule 4: SQL Injection
- When: Request contains SQL patterns
- Then: Block
*/

설명

이것이 하는 일: Next.js 서버 앞에 Cloudflare를 배치하여, 네트워크 레벨과 애플리케이션 레벨 공격을 서버 도달 전에 차단하고, 정상 트래픽만 전달받습니다. 첫 번째로, Cloudflare를 DNS로 설정하면 모든 트래픽이 자동으로 Cloudflare의 엣지 네트워크를 거칩니다.

사용자가 여러분의 도메인에 접속하면, 가장 가까운 Cloudflare 데이터센터로 연결되고, Cloudflare가 설정된 규칙에 따라 트래픽을 검사합니다. DDoS 공격, 알려진 봇, 악성 IP는 이 단계에서 자동으로 차단됩니다.

그 다음으로, Cloudflare는 실제 클라이언트 IP를 CF-Connecting-IP 헤더에 담아 전달합니다. 여러분의 Next.js 서버는 Cloudflare의 IP만 보므로, 이 헤더를 사용해야 실제 사용자 IP를 알 수 있습니다.

getRealIP 함수는 이를 안전하게 추출하여 Rate Limiting에 사용합니다. 마지막으로, Cloudflare Dashboard에서 WAF 규칙, Rate Limiting, Bot Management를 설정합니다.

예를 들어, "1분에 100회 이상 요청하면 CAPTCHA 챌린지", "봇 점수 30 미만이면 차단", "특정 국가에서만 접근 허용" 같은 규칙을 GUI로 쉽게 만들 수 있습니다. 이 규칙들은 엣지에서 실행되어 서버 부하가 전혀 없습니다.

여러분이 이 설정을 사용하면 서버 비용을 크게 줄이고(캐싱과 공격 차단), 대역폭 초과 비용을 방지하며, 대규모 DDoS 공격에도 서비스를 유지하고, 전 세계 사용자에게 빠른 응답 속도를 제공할 수 있습니다. Cloudflare Free 플랜도 무제한 DDoS 방어를 제공하므로, 모든 서비스에 기본적으로 적용할 것을 권장합니다.

실전 팁

💡 Cache Everything: 정적 자산뿐 아니라 API 응답도 Cloudflare에 캐싱하면(Cache-Control 헤더 활용) 서버 부하를 90% 이상 줄일 수 있습니다.

💡 Bot Fight Mode: Free 플랜에서도 사용 가능한 Bot Fight Mode를 활성화하면 알려진 악성 봇을 자동 차단합니다.

💡 Argo Smart Routing: 유료 기능으로, Cloudflare의 프라이빗 네트워크를 통해 최적 경로로 라우팅하여 응답 속도를 30% 개선합니다.

💡 Workers로 엣지 로직: Cloudflare Workers에서 Rate Limiting이나 인증을 구현하면 서버에 전혀 부담 없이 보안을 강화할 수 있습니다.

💡 Analytics 활용: Cloudflare Analytics로 공격 패턴, 봇 트래픽, 지역별 접속을 분석하여 WAF 규칙을 최적화하세요.


8. 로깅과 모니터링 - 공격 패턴 분석하기

시작하며

Rate Limiting과 DDoS 방어를 구축했지만, 실제로 얼마나 많은 공격이 차단되는지, 정상 사용자가 불편을 겪지는 않는지 알 수 없다면 개선할 수 없습니다. 눈에 보이지 않는 위협은 대응하기 어렵습니다.

실제 운영에서는 공격이 지속적으로 발생하고, 패턴도 계속 변합니다. 어제는 로그인 API를 공격했는데 오늘은 검색 API를 공격하거나, 특정 국가에서 집중적으로 공격이 들어올 수 있습니다.

이런 변화를 감지하고 대응하려면 체계적인 로깅과 모니터링이 필수입니다. 바로 이럴 때 필요한 것이 종합적인 보안 모니터링 시스템입니다.

Rate Limit 히트, 차단된 IP, CAPTCHA 실패 등을 기록하고 시각화하여 실시간으로 보안 상태를 파악합니다.

개요

간단히 말해서, 보안 이벤트를 구조화된 형태로 로깅하고, 대시보드로 시각화하며, 이상 징후 발생 시 알림을 보내는 시스템입니다. 효과적인 모니터링은 세 가지 레이어로 구성됩니다.

첫째, 애플리케이션 로그(Winston, Pino로 구조화된 JSON 로깅), 둘째, 메트릭 수집(Prometheus, Datadog으로 숫자 추적), 셋째, 실시간 알림(Slack, PagerDuty로 긴급 상황 통지)입니다. 예를 들어, 1분에 100번 이상 Rate Limit이 발생하면 Slack으로 알림을 보내 관리자가 즉시 대응할 수 있습니다.

기존에는 공격이 발생해도 모르고 지나갔다면, 이제는 실시간으로 감지하고 패턴을 분석하여 선제적으로 방어할 수 있습니다. 핵심 특징은 첫째, 구조화된 로그로 검색과 분석이 쉽고, 둘째, 시계열 메트릭으로 추세를 파악하며, 셋째, 임계값 기반 알림으로 즉각 대응할 수 있다는 점입니다.

이는 보안뿐 아니라 전체 시스템 안정성에도 필수적입니다.

코드 예제

// lib/security-logger.ts
import winston from 'winston';

export const securityLogger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  defaultMeta: { service: 'rate-limiter' },
  transports: [
    new winston.transports.File({
      filename: 'logs/security.log',
      maxsize: 10485760, // 10MB
      maxFiles: 5,
    }),
  ],
});

export function logRateLimitHit(
  ip: string,
  endpoint: string,
  limit: number
) {
  securityLogger.warn('Rate limit exceeded', {
    event: 'rate_limit_hit',
    ip,
    endpoint,
    limit,
    timestamp: new Date().toISOString(),
  });
}

export function logSuspiciousActivity(
  ip: string,
  reason: string,
  metadata?: Record<string, any>
) {
  securityLogger.error('Suspicious activity detected', {
    event: 'suspicious_activity',
    ip,
    reason,
    ...metadata,
    timestamp: new Date().toISOString(),
  });
}

// API Route에서 사용
export async function protectedHandler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const ip = getRealIP(req);
  const result = await checkRedisRateLimit(ip, 60, 60);

  if (!result.allowed) {
    // Rate limit 히트 로깅
    logRateLimitHit(ip, req.url || 'unknown', 60);

    // 반복적 위반 시 의심스러운 활동으로 기록
    const violations = await getViolationCount(ip);
    if (violations > 5) {
      logSuspiciousActivity(ip, 'Repeated rate limit violations', {
        violations,
        userAgent: req.headers['user-agent'],
      });
    }

    return res.status(429).json({ error: 'Too many requests' });
  }

  return res.status(200).json({ success: true });
}

// Prometheus 메트릭 예시 (prom-client 사용)
import { Counter, Histogram } from 'prom-client';

export const rateLimitHits = new Counter({
  name: 'rate_limit_hits_total',
  help: 'Total number of rate limit hits',
  labelNames: ['endpoint', 'ip'],
});

export const requestDuration = new Histogram({
  name: 'api_request_duration_seconds',
  help: 'API request duration',
  labelNames: ['endpoint', 'status'],
});

설명

이것이 하는 일: Rate Limiting과 보안 이벤트를 체계적으로 로깅하고, 메트릭으로 수치화하여, 공격 패턴을 분석하고 이상 징후를 실시간으로 감지합니다. 첫 번째로, Winston 로거를 설정하여 모든 보안 이벤트를 JSON 형식으로 파일에 기록합니다.

JSON 형식은 나중에 Elasticsearch나 Splunk 같은 로그 분석 도구로 쉽게 검색하고 분석할 수 있습니다. maxsizemaxFiles로 로그 로테이션을 자동화하여 디스크 공간을 관리합니다.

그 다음으로, logRateLimitHitlogSuspiciousActivity 함수로 각 보안 이벤트를 구조화하여 기록합니다. IP, 엔드포인트, 제한값, 타임스탬프 등 핵심 정보를 포함하여, 나중에 "어느 IP가 어느 엔드포인트를 몇 번 공격했는지" 분석할 수 있습니다.

반복적으로 제한에 걸리는 IP는 자동으로 의심스러운 활동으로 에스컬레이션됩니다. 마지막으로, Prometheus 메트릭으로 숫자 데이터를 수집합니다.

Counter는 누적 횟수(총 몇 번 Rate Limit이 걸렸는지), Histogram은 분포(요청 처리 시간의 p50, p95, p99)를 추적합니다. 이 메트릭을 Grafana로 시각화하면 "지난 1시간 동안 Rate Limit 히트가 급증했다"는 것을 그래프로 즉시 파악할 수 있습니다.

여러분이 이 시스템을 사용하면 공격을 실시간으로 감지하고, 어느 엔드포인트가 가장 많이 공격받는지 파악하여 보호를 강화하며, 정상 사용자가 Rate Limit에 걸리는 false positive를 줄일 수 있습니다. 특히 로그 분석을 통해 공격 IP를 영구 블랙리스트에 추가하거나, 공격 패턴에 맞춰 WAF 규칙을 업데이트할 수 있습니다.

실전 팁

💡 로그 집계: Elasticsearch + Kibana나 AWS CloudWatch Logs Insights로 여러 서버의 로그를 한곳에 모아 통합 분석하세요.

💡 이상 탐지: 로그 패턴을 머신러닝으로 분석하여 새로운 공격 유형을 자동으로 감지하는 AWS GuardDuty나 Datadog Security Monitoring을 활용하세요.

💡 알림 규칙: "1분에 100회 이상 Rate Limit 히트" 시 Slack 알림, "동일 IP가 10개 이상 엔드포인트 공격" 시 PagerDuty 긴급 호출로 계층화하세요.

💡 리텐션 정책: 보안 로그는 최소 30일, 규정 준수가 필요하면 1년 이상 보관하되, 비용을 고려해 S3 Glacier로 아카이빙하세요.

💡 대시보드 구축: Grafana로 실시간 대시보드를 만들어 "현재 초당 요청 수", "Rate Limit 히트 비율", "상위 공격 IP" 등을 한눈에 보세요.


9. 테스트와 검증 - Rate Limiting 동작 확인

시작하며

Rate Limiting을 구현했지만, 실제로 제대로 작동하는지 확인하지 않으면 안심할 수 없습니다. 설정 실수로 제한이 너무 낮아 정상 사용자가 차단당하거나, 너무 높아서 공격을 막지 못할 수 있습니다.

실제로 많은 팀이 Rate Limiting을 배포했지만 테스트를 제대로 하지 않아서, 프로덕션에서 문제를 발견하고 긴급 롤백하는 경우가 있습니다. 특히 Redis 연결 실패, TTL 설정 오류, IP 추출 버그 등은 테스트 없이는 발견하기 어렵습니다.

바로 이럴 때 필요한 것이 체계적인 테스트입니다. 단위 테스트, 통합 테스트, 부하 테스트를 통해 Rate Limiting이 모든 시나리오에서 정확히 작동하는지 검증합니다.

개요

간단히 말해서, Rate Limiting 로직을 자동화된 테스트로 검증하고, 실제 부하 상황을 시뮬레이션하여 프로덕션 배포 전에 문제를 찾아냅니다. 세 가지 레벨의 테스트가 필요합니다.

첫째, 단위 테스트로 rate limiter 함수가 올바른 값을 반환하는지, 둘째, 통합 테스트로 API Route와 Redis가 제대로 연동되는지, 셋째, 부하 테스트로 초당 수천 건의 요청에도 안정적으로 작동하는지 확인합니다. 예를 들어, Jest로 "60초에 60번 요청하면 통과, 61번째는 거부" 시나리오를 테스트합니다.

기존에는 수동으로 몇 번 클릭해보는 것이 전부였다면, 이제는 자동화된 테스트로 모든 엣지 케이스를 빠짐없이 검증할 수 있습니다. 핵심 특징은 첫째, 단위 테스트로 빠른 피드백을, 둘째, 통합 테스트로 실제 환경 검증을, 셋째, 부하 테스트로 성능과 안정성을 확인한다는 점입니다.

이는 신뢰할 수 있는 보안 시스템의 필수 요소입니다.

코드 예제

// __tests__/rate-limit.test.ts
import { checkRedisRateLimit } from '@/lib/redis-rate-limit';
import Redis from 'ioredis-mock'; // 테스트용 Mock Redis

// Mock Redis를 사용하여 실제 Redis 없이 테스트
jest.mock('ioredis', () => require('ioredis-mock'));

describe('Rate Limiting', () => {
  beforeEach(() => {
    // 각 테스트 전에 Redis 초기화
    const redis = new Redis();
    redis.flushall();
  });

  test('제한 내 요청은 모두 허용', async () => {
    const ip = '192.168.1.1';

    // 10번 요청, 모두 성공해야 함
    for (let i = 0; i < 10; i++) {
      const result = await checkRedisRateLimit(ip, 10, 60);
      expect(result.allowed).toBe(true);
      expect(result.remaining).toBe(10 - (i + 1));
    }
  });

  test('제한 초과 요청은 거부', async () => {
    const ip = '192.168.1.2';

    // 10번은 성공
    for (let i = 0; i < 10; i++) {
      await checkRedisRateLimit(ip, 10, 60);
    }

    // 11번째는 실패해야 함
    const result = await checkRedisRateLimit(ip, 10, 60);
    expect(result.allowed).toBe(false);
    expect(result.remaining).toBe(0);
  });

  test('다른 IP는 독립적으로 카운트', async () => {
    const ip1 = '192.168.1.3';
    const ip2 = '192.168.1.4';

    // IP1에서 10번 요청
    for (let i = 0; i < 10; i++) {
      await checkRedisRateLimit(ip1, 10, 60);
    }

    // IP2는 여전히 요청 가능해야 함
    const result = await checkRedisRateLimit(ip2, 10, 60);
    expect(result.allowed).toBe(true);
  });
});

// 부하 테스트 예시 (k6 스크립트)
/*
import http from 'k6/http';
import { check } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 100 }, // 100 VUs로 증가
    { duration: '1m', target: 100 },  // 1분간 유지
    { duration: '10s', target: 0 },   // 종료
  ],
  thresholds: {
    'http_req_duration': ['p(95)<500'], // 95%가 500ms 이하
    'rate_limit_blocks': ['rate>0.5'],  // 50% 이상 차단되어야 함
  },
};

export default function () {
  const res = http.post('https://yourapp.com/api/login', {
    email: 'test@example.com',
    password: 'password',
  });

  check(res, {
    'rate limited': (r) => r.status === 429,
  });
}
*/

설명

이것이 하는 일: 자동화된 테스트로 Rate Limiting이 모든 상황에서 정확히 작동하는지 검증하고, 프로덕션 배포 전에 버그를 발견합니다. 첫 번째로, Jest 테스트 프레임워크로 단위 테스트를 작성합니다.

ioredis-mock을 사용하여 실제 Redis 서버 없이도 로컬에서 빠르게 테스트할 수 있습니다. beforeEach로 각 테스트 전에 Redis를 초기화하여 테스트 간 간섭을 방지하고, 독립적으로 실행되도록 합니다.

그 다음으로, 세 가지 핵심 시나리오를 테스트합니다. 첫째, 제한 내 요청이 모두 허용되는지(10번 제한에 10번 요청 성공), 둘째, 제한 초과 시 거부되는지(11번째는 실패), 셋째, 다른 IP가 독립적으로 카운트되는지(IP1이 제한에 걸려도 IP2는 무관)를 검증합니다.

각 assertion은 정확한 동작을 보장합니다. 마지막으로, k6 같은 부하 테스트 도구로 실제 프로덕션 수준의 트래픽을 시뮬레이션합니다.

동시에 100명의 사용자가 로그인을 시도할 때, Rate Limiting이 제대로 작동하고, 응답 시간이 허용 범위 내인지, 메모리 누수가 없는지 확인합니다. 이는 실제 공격 상황을 미리 테스트하는 것과 같습니다.

여러분이 이 테스트를 사용하면 코드 변경 시 Rate Limiting이 깨지지 않았는지 자동으로 확인하고(CI/CD), 프로덕션 배포 전에 버그를 발견하여 사고를 예방하며, 성능 병목을 사전에 파악하여 최적화할 수 있습니다. 특히 테스트 커버리지 100%를 목표로 하면 안심하고 리팩토링할 수 있습니다.

실전 팁

💡 엣지 케이스 테스트: 동시 요청(race condition), Redis 연결 실패, 시간 경계(59초와 61초), TTL 만료 직후 등 복잡한 상황을 테스트하세요.

💡 CI/CD 통합: GitHub Actions나 GitLab CI에서 모든 PR마다 자동으로 테스트를 실행하여 회귀 버그를 막으세요.

💡 E2E 테스트: Playwright로 실제 브라우저에서 로그인을 10번 시도하고 11번째에 429 에러가 뜨는지 확인하는 End-to-End 테스트도 추가하세요.

💡 부하 테스트 자동화: 정기적으로(주 1회) 부하 테스트를 실행하여 트래픽 증가에 따른 성능 저하를 조기에 발견하세요.

💡 Chaos Engineering: 프로덕션에서 의도적으로 Redis를 다운시켜보는 등 장애 시나리오를 테스트하여 fallback 로직을 검증하세요.


10. 실전 체크리스트 - 프로덕션 배포 전 확인사항

시작하며

여러분이 Rate Limiting과 DDoS 방어를 모두 구현했지만, 프로덕션에 배포하기 전에 빠뜨린 부분은 없는지 확인이 필요합니다. 작은 설정 실수가 큰 장애로 이어질 수 있습니다.

실제로 많은 팀이 "Rate Limiting을 켰는데 정상 사용자가 차단당했다", "Redis가 다운되자 전체 서비스가 멈췄다", "로그가 너무 많이 쌓여서 디스크가 꽉 찼다" 같은 문제를 겪습니다. 이런 문제는 대부분 배포 전 체크리스트로 예방할 수 있습니다.

바로 이럴 때 필요한 것이 종합 체크리스트입니다. 설정, 테스트, 모니터링, 알림, 백업 계획까지 모든 것을 점검하고 프로덕션에 안전하게 배포합니다.

개요

간단히 말해서, 프로덕션 배포 전에 반드시 확인해야 할 항목들을 체계적으로 정리한 목록입니다. 체크리스트는 크게 다섯 영역을 다룹니다.

첫째, 기능 검증(모든 엔드포인트에 Rate Limiting 적용 확인), 둘째, 성능 검증(부하 테스트 통과), 셋째, 보안 검증(IP 위조 방지, CAPTCHA 작동), 넷째, 운영 준비(모니터링, 알림, 로깅), 다섯째, 장애 대응(Fallback, 롤백 계획)입니다. 각 항목을 하나씩 체크하면서 누락을 방지합니다.

기존에는 "대충 돌아가면 배포"였다면, 이제는 체계적인 검증 후 자신 있게 배포할 수 있습니다. 핵심 특징은 첫째, 빠뜨리기 쉬운 항목을 명시적으로 리스트업하고, 둘째, 각 항목의 중요도와 검증 방법을 제시하며, 셋째, 장애 시나리오까지 대비한다는 점입니다.

이는 안정적인 프로덕션 운영의 기본입니다.

코드 예제

# Rate Limiting & DDoS 방어 프로덕션 체크리스트

## 1. 기능 검증
- [ ] 모든 민감한 엔드포인트(로그인, 회원가입, 결제)에 Rate Limiting 적용
- [ ] IP 기반, 사용자 기반 전략이 각각 올바르게 적용됨
- [ ] CAPTCHA가 제한 초과 시 자동으로 활성화됨
- [ ] 429 에러 응답이 Retry-After 헤더를 포함함
- [ ] X-RateLimit-* 헤더가 클라이언트에 전달됨

## 2. 성능 검증
- [ ] 부하 테스트로 초당 1000 요청 처리 확인
- [ ] Redis 응답 시간이 10ms 이하 유지됨
- [ ] Rate Limiting 체크가 전체 응답 시간의 5% 미만 차지
- [ ] 메모리 누수 없이 24시간 안정 운영 확인
- [ ] 서버 여러 대에서 일관된 제한 적용됨 (Redis 공유)

## 3. 보안 검증
- [ ] CF-Connecting-IP나 X-Forwarded-For로 실제 IP 추출
- [ ] IP 헤더 위조 공격 방어 (프록시 설정 확인)
- [ ] CAPTCHA 토큰 검증이 서버 사이드에서 수행됨
- [ ] 관리자/신뢰 IP는 화이트리스트에 추가됨
- [ ] SQL 인젝션, XSSWAF 규칙 활성화 (Cloudflare)

## 4. 모니터링 & 로깅
- [ ] Rate Limit 히트가 로그로 기록됨 (IP, 엔드포인트, 시간)
- [ ] Prometheus 메트릭으로 실시간 추적 가능
- [ ] Grafana 대시보드에서 트래픽 상태 확인 가능
- [ ] Slack/Email 알림이 임계값 초과 시 발송됨
- [ ] 로그 로테이션 설정으로 디스크 공간 관리됨

## 5. 장애 대응
- [ ] Redis 다운 시 Fallback 전략 있음 (메모리 또는 일시 우회)
- [ ] Rate Limiting 버그 발견 시 즉시 비활성화 가능 (Feature Flag)
- [ ] 정상 사용자 차단 시 수동 화이트리스트 추가 절차 마련
- [ ] 대규모 공격 시 엄격 모드 전환 스크립트 준비
- [ ] Cloudflare "Under Attack Mode" 활성화 방법 숙지

## 6. 문서화
- [ ] Rate Limit 정책 문서화 (엔드포인트별 제한)
- [ ] API 문서에 429 에러 응답 명시
- [ ] 운영 매뉴얼에 공격 대응 절차 작성
- [ ] 온콜 팀에 알림 에스컬레이션 정책 공유
- [ ] 사용자 FAQ"너무 많은 요청" 해결 방법 추가

## 7. 법적/규정 준수
- [ ] GDPR/CCPA 준수 (IP 로깅 명시, 데이터 보관 기간)
- [ ] 보안 로그 최소 30일 보관
- [ ] 개인정보 처리방침에 Rate Limiting 언급
- [ ] 차단된 사용자 이의 제기 절차 마련

설명

이것이 하는 일: Rate Limiting과 DDoS 방어 시스템을 프로덕션에 배포하기 전에 반드시 확인해야 할 모든 항목을 체계적으로 점검하여, 장애와 보안 사고를 예방합니다. 첫 번째로, 기능 검증 섹션에서 Rate Limiting이 실제로 모든 필요한 곳에 적용되었는지 확인합니다.

코드에 구현했지만 특정 엔드포인트에만 적용하고 다른 곳을 빠뜨리는 실수가 흔합니다. 429 에러 응답이 Retry-After 헤더를 포함하는지도 중요합니다 - 이게 없으면 클라이언트가 무한 재시도를 할 수 있습니다.

그 다음으로, 성능과 보안 검증에서 실제 프로덕션 수준의 부하에도 안정적인지, IP 위조 공격에 취약하지 않은지 확인합니다. 특히 프록시나 로드 밸런서 설정이 잘못되면 모든 요청이 같은 IP로 보여서 Rate Limiting이 무용지물이 됩니다.

또한 Cloudflare를 사용한다면 CF-Connecting-IP를 신뢰하도록 설정해야 합니다. 마지막으로, 모니터링과 장애 대응 섹션에서 문제 발생 시 빠르게 감지하고 대응할 수 있는지 확인합니다.

Redis가 다운되면 어떻게 할지(Fallback to memory), 정상 사용자가 실수로 차단당하면 어떻게 화이트리스트에 추가할지, 대규모 공격이 오면 어떻게 "Under Attack Mode"로 전환할지 등 시나리오별 대응 계획이 필요합니다. 여러분이 이 체크리스트를 사용하면 프로덕션 배포 시 불안감을 없애고, 장애 발생 확률을 크게 줄이며, 문제 발생 시 신속하게 대응할 수 있습니다.

특히 팀 전체가 이 체크리스트를 공유하면 누가 배포하든 동일한 품질을 보장할 수 있습니다. 프로덕션 배포는 한 번에 완벽하기 어려우므로, 이 체크리스트를 계속 업데이트하며 개선해 나가세요.

실전 팁

💡 점진적 롤아웃: 모든 사용자에게 한 번에 적용하지 말고, 5% → 25% → 50% → 100%로 단계적으로 활성화하며 문제를 조기에 발견하세요.

💡 Feature Flag 활용: LaunchDarkly나 환경 변수로 Rate Limiting을 즉시 on/off할 수 있게 하여, 문제 발생 시 코드 배포 없이 비활성화하세요.

💡 Canary 배포: 새 Rate Limiting 설정을 일부 서버에만 먼저 배포하여, 정상 작동 확인 후 전체 서버로 확대하세요.

💡 Runbook 작성: "429 에러 급증 시 대응 방법", "Redis 다운 시 복구 절차" 같은 운영 매뉴얼을 미리 작성하여 온콜 팀이 빠르게 대응하도록 하세요.

💡 정기 리뷰: 분기마다 Rate Limit 설정을 리뷰하여 트래픽 증가에 맞춰 조정하고, 로그를 분석하여 불필요하게 차단된 사용자가 없는지 확인하세요.


#Next.js#RateLimiting#DDoS방어#APIProtection#SecurityBestPractices

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.