10분 안에 자동 생성 OpenAPI 문서가 포함된 버전 관리 Laravel API 만들기

발행: (2026년 6월 11일 PM 03:12 GMT+9)
7 분 소요
원문: Dev.to

출처: Dev.to

TL;DRdskripchenko/laravel-api 를 설치하고, 컨트롤러 하나만 작성하면 버전이 있는 API (/api/v1/...)와 그리고 /api/doc 에서 인터랙티브한 OpenAPI 3.0 문서를 자동으로 생성할 수 있습니다 — 문서 블록은 원래 작성하던 그대로 사용합니다. 그 뒤에 v2 를 복사‑붙여넣기 없이 바로 배포할 수 있습니다.

문제

성장하는 Laravel API에서는 두 가지가 점점 부패합니다:

버전 관리. v1 을 배포하고 나서 v2 에서는 세 개의 엔드포인트만 바꾸고 나머지 스무 개는 그대로 유지해야 합니다. V2 폴더를 복사‑붙여넣기 하면 버그 수정이 두 곳에 나뉘고, 아니면 컨트롤러에 if ($version === 2) 같은 분기문을 삽입해야 합니다.

문서. OpenAPI 스펙은 코드를 병합하는 순간부터 어긋나기 시작합니다. 어노테이션 라이브러리(#[OA\Get(...)], 거대한 YAML 파일) 를 사용하면 API 를 두 번 설명해야 합니다 — 코드에 한 번, 어트리뷰트에 한 번.

이 패키지의 핵심 아이디어: 컨트롤러 자체가 이미 자신을 설명한다는 것. 메서드 이름, 요청 필드, 응답 형태를 일반 PHPDoc 형태로 한 번만 작성하면, 패키지가 라우트와 문서를 모두 파생시킵니다. 버전 관리는 순수 PHP 상속으로 해결됩니다.

그럼 시작해봅시다.

우리가 만들 것

작은 tasks API:

  • POST /api/v1/task/list — 작업 목록 조회
  • POST /api/v1/task/create — 작업 생성
  • 인터랙티브 문서는 GET /api/doc (버전별 원시 스펙은 /api/doc/{version})

그리고 v2 에서는 v1 을 건드리지 않고 새로운 엔드포인트를 추가합니다.

전체 파일 수: 약 4개.

Step 0 — 설치

composer require dskripchenko/laravel-api
php artisan vendor:publish --tag=laravel-api-config
// config/laravel-api.php
return [
    'prefix'            => 'api',                            // → /api/...
    'uri_pattern'       => '{version}/{controller}/{action}',
    'available_methods' => ['get', 'post', 'put', 'patch', 'delete'],
    'openapi_path'      => 'public/openapi',
    'doc_middleware'    => [],                               // 여기서 /api/doc 을 잠글 수 있음
];

Step 1 — 컨트롤러 작성

특별한 것은 없습니다 — 패키지의 ApiController 를 상속하면 success(), error(), validationError(), created(), noContent(), notFound() 와 같은 응답 헬퍼를 바로 사용할 수 있습니다. docblock 자체가 문서가 됩니다:

<?php

namespace App\Api\V1\Controllers;

use App\Api\ApiController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class TaskController extends ApiController
{
    /**
     * List tasks
     *
     * @output array $tasks
     * @output integer $tasks.id
     * @output string $tasks.title
     * @output string $tasks.status
     *
     * @return JsonResponse
     */
    public function list(Request $request): JsonResponse
    {
        return $this->success([
            ['id' => 1, 'title' => 'Pack for Vietnam', 'status' => 'done'],
            ['id' => 2, 'title' => 'Write this article', 'status' => 'open'],
        ]);
    }

    /**
     * Create a task
     *
     * @input string $title Task title
     * @input string ?$status Initial status [open,done]
     *
     * @output integer $id New task id
     * @output string $title Task title
     *
     * @return JsonResponse
     */
    public function create(Request $request): JsonResponse
    {
        return $this->created([
            'id'    => 3,
            'title' => $request->input('title'),
        ]);
    }
}

알아두면 좋은 docblock 규칙

  • ?$page → 선택적 필드.
  • string $status ... [open,done] → 대괄호 안 목록이 스펙에서 enum 으로 변환됩니다.
  • @input integer(int64) $id / @input string(email) $email형식(format) 을 포함한 타입.
  • @input file $avatar → 파일 업로드; @input @User $user$ref 로 컴포넌트 스키마를 참조.

모든 응답은 일관된 래퍼에 감싸집니다:

{ "success": true, "payload": { ... } }

에러(ApiException 혹은 $this->error())는 다음 형태로 반환됩니다:

{ "success": false, "payload": { "errorKey": "string", "message": "string" } }

Step 2 — 버전, 모듈, 프로바이더 연결

세 개의 작은 클래스로 라우팅 레이어 전체를 구성합니다 — routes/api.php 에는 아무것도 추가하지 않습니다.

<?php
// app/Api/Modules/TaskModule.php
namespace App\Api\Modules;

class TaskModule
{
    public function getRoutes(): array
    {
        return [
            'task' => [                          // → /api/v1/task/{action}
                'controller' => \App\Api\V1\Controllers\TaskController::class,
                'actions'    => ['list', 'create'],
            ],
        ];
    }
}
<?php
// app/Api/V1/Api.php
namespace App\Api\V1;

use App\Api\Api;
use App\Api\Modules\TaskModule;

class Api extends \App\Api\Api
{
    protected function getModules(): array
    {
        return [
            TaskModule::class,
        ];
    }
}
<?php
// app/Api/Api.php
namespace App\Api;

abstract class Api
{
    abstract protected function getModules(): array;

    public function getRoutes(): array
    {
        $routes = [];
        foreach ($this->getModules() as $module) {
            $moduleInstance = new $module();
            $routes = array_merge($routes, $moduleInstance->getRoutes());
        }
        return $routes;
    }
}
// config/laravel-api.php (추가)
'modules' => [
    \App\Api\V1\Api::class,
],

Step 3 — 서비스 프로바이더 등록

<?php
// app/Providers/ApiServiceProvider.php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Api\Api;

class ApiServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton('api', function () {
            return new Api();
        });
    }

    public function boot()
    {
        // 라우트 자동 등록
        $this->app['router']->group([
            'prefix' => config('laravel-api.prefix'),
            'middleware' => config('laravel-api.doc_middleware'),
        ], function ($router) {
            foreach (config('laravel-api.modules') as $apiClass) {
                $api = new $apiClass();
                foreach ($api->getRoutes() as $controllerKey => $info) {
                    $controller = $info['controller'];
                    foreach ($info['actions'] as $action => $method) {
                        $uri = sprintf('%s/%s/%s', $controllerKey, $action);
                        $router->match($method, $uri, [$controller, $action]);
                    }
                }
            }
        });
    }
}

config/app.phpproviders 배열에 App\Providers\ApiServiceProvider::class 를 추가합니다.

Step 4 — 결과: 무료 OpenAPI 문서

브라우저에서 GET /api/doc 을 열어보세요. 원시 JSON 파일이 아니라 인터랙티브 API 레퍼런스( Scalar 로 렌더링) 가 표시됩니다. 버전 전환 스위처가 v1, v2, … 로 이미 연결돼 있습니다. 패키지는 컨트롤러를 스캔해 docblock을 읽고 완전한 OpenAPI 3.0 문서를 자동 생성합니다 — 파라미터, enum, 응답 스키마 등 모든 것이 포함됩니다. 이 문서는 코드와 동기화돼 있기 때문에 어긋날 수 없습니다.

CI, 클라이언트 생성기, 혹은 자체 Redoc/Stoplight 설정을 위해 원시 스펙이 필요하다면 각 버전은 GET /api/doc/{version} (예: /api/doc/v1) 에서 JSON 형태로 제공됩니다 — storage:link 나 별도 빌드 단계가 필요 없습니다.

TypeScript 클라이언트가 필요하나요? 다음 명령으로 생성할 수 있습니다:

php artisan api:generate-types
0 조회
Back to Blog

관련 글

더 보기 »