Permalink
Fetching contributors…
Cannot retrieve contributors at this time
215 lines (155 sloc) 7.52 KB
extends section current_index
_layouts.master
content
49

실전 프로젝트 3 - RESTful API

47강 - 중복 제거 리팩토링

앞 강에서 기존 컨트롤러의 HTTP 응답 메소드 부분을 API 관련 컨트롤러에서 오버라이드하는 과정에서, 상당히 많은 중복을 보았을 것이다. 이 중복을 제거하는 작업을 이번 강좌에서 해 볼 것이다.

API Response 패키지

API Response 에서 중복을 피하고 Response Payload 를 좀 더 편리하게 만들 수 있는, 이 실전 프로젝트 규모에 적절한 패키지를 찾아 봤지만.. 못 찾았다. App\Http\Controllers\Controller 나 별도 Trait 로 API Response 를 위한 공용 메소드를 정의하는 방법이 있기도 하지만, 필자가 Packagist 에 올려 놓은 appkr/api 패키지를 이용하도록 하자.

참고 Laravel/Lumen 월드에서 API 관련 패키지 중에서는 dingo/api 가 갑 (甲) 인데, 라라벨의 네이티브 클래스들을 꽤 많이 오버라이드하고 있어서 사용법을 다시 익혀야 하는 단점이 있다. 45강 - 기본 구조 잡기 서두에서 같이 고민했던 "단일 서버 vs. 복수 서버" 섹션을 기억할 것이다. dingo/api 는 API 전용 독립 서버, 즉 복수 서버 구조에 더 적합하다고 생각된다. 거의 라라벨 프레임웍 수준의 큰 프로젝트로 API 관련 a-Z 를 모두 담고 있고, 베스트 프랙티스를 실천하고 있으므로 꼭 한번 설치해서 사용해 보기 바란다.

설치

$ composer require "appkr/api:0.1.*"

ServiceProvider 를 설정하고 config 파일을 우리 프로젝트 안으로 끌고 오자.

// config/app.php

'providers' => [
    // ...
    Appkr\Api\ApiServiceProvider::class,
],
$ php artisan vendor:publish --provider="Appkr\Api\ApiServiceProvider"

설정

설정 파일을 확인해 보자.

// config/fractal.php

return [
    'pattern' => 'v1/*',
    'domain'  => 'api.myproject.dev',
    
    // ...
    
    'successFormat' => [
        'success' => [
            'code'    => ':code',
            'message' => ':message',
        ]
    ],
    
    'errorFormat' =>  [
        'error' => [
            'code'    => ':code',
            'message' => ':message',
        ]
    ],
];

pattern, domain : 이 패키지에서도 is_api_request() 란 Helper 를 포함하고 있는데, 이 Helper 에서 사용하는 설정 값들이다. 주의할 점은 이 패키지가 먼저 로드되고 난후, 우리가 정의한 Helper 가 로드되는데, 이 때 function_exists() 에 걸려서 우리 Helper 가 로드되지 않고,이 패키지의 is_api_request() 가 동작하게 된다는 점이다. PHP 네임스페이스가 필요한 이유를 방금 봤다.

successFormat : 200 번 대의 성공 응답을 할 때, 이 포맷이 사용된다. :code, :messageAppkr\Api\Response 클래스의 HTTP 응답 메소드에서 HTTP Status Code 와 메소드에 넘긴 메시지로 치환된다.

errorFormat : 400 번 이상의 에러 응답을 할 때, 이 포맷이 사용된다.

리팩토링

방금 끌어온 appkr/api 패키지에서는 json(array $payload) Helper 를 제공한다. 또, json() Helper 는 인자 없이 호출할 때는 Appkr\Api\Response 인스턴스를 리턴하기 때문에, 해당 클래스에 정의된

  • success(string $message)
  • error(string|array|\Exception $message)
  • respond***(string $message)
  • setStatusCode(int $statusCode)
  • setMeta(array $meta)
  • ...

등 다양한 메소드를 json()->success() 처럼 체인해서 사용할 수 있다. set*() 메소드는 다른 응답 메소드보다 먼저 체인되어야 한다는 점을 주의하자.

컨트롤러

하나씩 적용해 보자. json(array $payload) Helper 는 response()->json(array $payload) 와 같은 역할을 한다.

// app/Http/Controllers/Api/WelcomeController.php

class WelcomeController extends Controller
{
    /**
     * Get the index page
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function index()
    {
        return json([
            'name'    => 'myProject Api',
            'message' => 'Welcome to myProject Api. This is a base endpoint.',
            'version' => 'n/a',
            'links'   => [/* ... */],
        ]);
    }
}

unprocessableError(mixed $message) 메소드는 HTTP 응답 코드를 422 로 설정하고, 인자로 넘겨 받은 $message 를, config('fractal.errorFormat') 으로 정의한 형태로 치환해서 HTTP 응답을 내 보내는 역할을 한다.

setMeta(array $meta) 는 HTTP 응답을 위한 Payload 에 ['meta' => $meta] 를 추가해 준다.

created(string|array|Illuminate\Database\Eloquent\Model $primitive) 는 HTTP 응답 코드를 201 로 설정하고, 인자로 넘겨 받은 $primitive 의 형태에 따라 적절하게 포맷팅하여 HTTP 응답을 내보내는 일을 한다.

// app/Http/Controllers/Api/UsersController.php

class UsersController extends ParentController
{
    // ...

    protected function respondValidationError(Validator $validator)
    {
        return json()->unprocessableError($validator->errors()->all());
    }

    protected function respondCreated(User $user)
    {
        return json()->setMeta(['token' => \JWTAuth::fromUser($user)])->created();
    }
}

SessionsController 에서 위와 중복된 메소드는 설명을 생략했다.

unauthorizedError(mixed $message) 는 HTTP 응답 코드를 401로 설정하고, 넘겨 받은 $message 를 포맷팅해서 HTTP 응답을 반환한다.

// app/Http/Controllers/Api/SessionsController.php

class SessionsController extends ParentController
{
    // ...
    
    protected function respondLoginFailed()
    {
        return json()->unauthorizedError('invalid_credentials');
    }
}

PasswordsController::respondError() 는 어떤 에러가 넘어올 지 모르기 때문에, notFoundError(mixed $message) 를 쓰지 않고, 좀 더 일반적인 error() 메소드를 사용하였다.

setStatusCode(int $statusCode) 는 HTTP 응답 코드를 셋팅하는 역할을 한다. error() 메소드에는 'not_found' 란 스트링을 인자로 넘겨 주었다.

// app/Http/Controllers/Api/PasswordsController.php

class PasswordsController extends ParentController
{
    // ...
    
    protected function respondError($message, $statusCode = 400)
    {
        return json()->setStatusCode($statusCode)->error('not_found');
    }
}

App\Http\Controllers\Api\V1\ArticlesController 는 좀 더 다른 형태의 메소드를 사용해야 하기에, 다음 강좌에서 살펴 보기로 하자.

Handler

이번에는 라라벨의 글로벌 Exception Handling 을 하는 'App\Exceptions\Handler' 클래스에 끌어온 Response 의 메소드들을 적용하자.

class Handler extends ExceptionHandler
{
    public function render($request, Exception $e)
    {
        // ...
        
        if (is_api_request()) {
            // ...
            
            return json()->setStatusCode($code ?: 400)->error($message);
        }
    }
}