실전 프로젝트 3 - RESTful API

49강 - API Rate Limit

Why?

API 엔드포인트의 사용량을 제한하는 이유는 여러가지이다.

  • DDoS 공격으로 부터 서비스를 보호한다.
  • API 클라이언트 간에 API Resource 의 사용에 공평성을 제공한다. 가령, 소수의 Heavy 클라이언트로 인해 다른 선량한 클라이언트가 피해를 입지 않도록 말이다.
  • API 를 통해 받는 데이터 자체가 돈과 직결되는 경우, 그 Business Model 로서의 역할을 한다.

적용

라라벨 5.2 를 사용하고 있다면, \Illuminate\Routing\Middleware\ThrottleRequests 라는 HTTP Middleware 가 이미 내장되어 있다. 우리의 ArticlesController 에 적용하기 전에 Route Middleware 로 잘 등록되어 있나 확인해 보자.

// app/Http/Kernel.php

class Kernel extends HttpKernel
{
    // ...

    protected $routeMiddleware = [
        // ...
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
    ];
}

적용하자.

// app/Http/Controllers/Api/V1/ArticlesController.php

class ArticlesController extends ParentController
{
    public function __construct()
    {
        $this->middleware('jwt.auth', ['except' => ['index', 'show']]);
        $this->middleware('throttle:60,1');

        parent::__construct();
    }

    // ...
}

아주 간단하다. throttle:60,1 이란, 클라이언트당 1 분에 60번 요청을 허용하겠다는 의미이다. throttle:3,1 정도로 수정하고 GET /v1/articles 요청을 여러번 연달아 해 보자. 네번째 요청에서 아래와 같은 응답을 받고, 1분 후에 다시 사용 가능한 상태가 되는 것을 확인할 수 있다.

HTTP/1.1 429 Too Many Requests
Host: api.myproject.dev:8000
# ...
Retry-After: 60
X-RateLimit-Limit: 3
X-RateLimit-Remaining: 0

다듬기

API 응답 포맷의 일관성을 위해, text/application 이 아니라 application/json 으로 응답하도록 수정하자. 라라벨 내장 \Illuminate\Routing\Middleware\ThrottleRequests 를 상속 받아 \App\Http\Middleware\ThrottleApiRequests 를 만들고 여기서 Rate Limit 에 걸렸을 경우 JSON 을 응답하게 수정하면 될 것이다.

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Routing\Middleware\ThrottleRequests;

class ThrottleApiRequests extends ThrottleRequests
{
    public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
    {
        $key = $this->resolveRequestSignature($request);

        if ($this->limiter->tooManyAttempts($key, $maxAttempts, $decayMinutes)) {
            return json()->setHeaders([
                'Retry-After' => $this->limiter->availableIn($key),
                'X-RateLimit-Limit' => $maxAttempts,
                'X-RateLimit-Remaining' => 0,
            ])->tooManyRequestsError();
        }

        // ...
    }
}

새로 만들어진 HTTP Middleware 를 등록하고, ArticlesController 에 적용하자.

// app/Http/Kernel.php

class Kernel extends HttpKernel
{
    // ...

    protected $routeMiddleware = [
        // ...
        'throttle.api' => \App\Http\Middleware\ThrottleApiRequests::class,
    ];
}
// app/Http/Controllers/Api/V1/ArticlesController.php

class ArticlesController extends ParentController
{
    public function __construct()
    {
        $this->middleware('jwt.auth', ['except' => ['index', 'show']]);
        $this->middleware('throttle.api:60,1');

        parent::__construct();
    }

    // ...
}

이제 throttle.api:3,1 정도로 수정하고 테스트해 보면, 우리의 일관된 포맷의 JSON 응답을 볼 수 있을 것이다.

HTTP/1.1 429 Too Many Requests
Host: api.myproject.dev:8000
Content-Type: application/json
# ...
Retry-After: 60
X-RateLimit-Limit: 3
X-RateLimit-Remaining: 0

{
  "error": {
    "code": 429,
    "message": "Too Many Requests"
  }
}

추가 적용

API 컨트롤러들, SessionsController, UsersController, PasswordsController 등에도 적용하자. 필자의 경우 throttle.api:10,1 로 정의했다. 상용에서는 서비스를 하면서 여러가지 통계 정보들에 기반해서 클라이언트 당 허용할 적절한 Rate Limit 값을 정해야 한다.

참고 Laravel 5.2 를 이용하지 않는다면, graham-campbell/throttle 패키지를 이용하자.




comments powered by Disqus
목록 토글
keyboard_arrow_up