Laravel, DTO ์์ฑํ๊ธฐ
๐ก PHP 8 ์ด์์ ๋ฒ์ ์์ ์ ํจํ ์์ ์ฝ๋๋ค์ด ์์ฑ๋์ด ์์ต๋๋ค.
๋ผ๋ผ๋ฒจ์์ DTO(Data Transfer Object)๋ฅผ ์์ฑํ๊ธฐ ์ํด ํด๋น ๊ธ์์๋ spatie/laravel-data ์คํ์์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํฉ๋๋ค. PHP์ ๊ธฐ๋ณธ ํด๋์ค ๋ง์ผ๋ก๋ ์ถฉ๋ถํ์ง๋ง ํ๋ก๊ทธ๋๋ฐํฑํ๊ณ ๋ผ๋ผ๋ฒจ๊ณผ ํธํ์ด ์ ๋์ด ์์ฃผ ์ฌ์ฉํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค ํ๋์ ๋๋ค. ๐
์คํ์์ค ๊ฐ๋ฐ ํ์ฌ spatie์์ ๊ฐ๋ฐ๋ laravel-data๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ์๊ฐํ๊ณ ์์ต๋๋ค.
Powerful data objects for laravel
… can be used in various ways. Using this package you only need to describe your data once:
instead of a form request, you can use a data objectinstead of an API transformer, you can use a data objectinstead of manually writing a typescript definition, you can use… ๐ฅ a data object
Laravel์ ์ํ ๊ฐ๋ ฅํ ๋ฐ์ดํฐ ๊ฐ์ฒด
… ์ฌ๋ฌ ๊ฐ์ง ๋ฐฉ์์ผ๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค. ์ด ํจํค์ง๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฐ์ดํฐ๋ฅผ ํ ๋ฒ๋ง ์ ์ํ๋ฉด ๋ฉ๋๋ค:
ํผ ์์ฒญ ๋์ ๋ฐ์ดํฐ ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.API ๋ณํ๊ธฐ ๋์ ๋ฐ์ดํฐ ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.์ง์ ํ์ ์คํฌ๋ฆฝํธ ์ ์๋ฅผ ์์ฑํ๋ ๋์ … ๐ฅ ๋ฐ์ดํฐ ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
์ค์น
composer require spatie/laravel-data
๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
๊ทธ๋ผ laravel-data ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํตํด ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ ๋ฐฉ๋ฒ์ ๋ํด ๊ฐ๋จํ ์์๋ณด๊ฒ ์ต๋๋ค.
<?php
namespace App\Http\Controllers\Requests;
use Spatie\LaravelData\Data;
class Feedback extends Data
{
public function __construct(
public readonly int $id,
public readonly int $point,
) {
}
}
๊ธฐ๋ณธ์ ์ธ Data ํด๋์ค ์์ฑ ๋ฐฉ๋ฒ์ ์์ ๊ฐ์ต๋๋ค.. Spatie\LaravelData\Data
ํด๋์ค๋ฅผ ์์ ๋ฐ๊ณ ์ ์ํ ๋ฐ์ดํฐ ํ์
๋ค์ ์์ฑ์์ ๋์ดํฉ๋๋ค. ๋ฐ์ดํฐ ํด๋์ค๊ฐ ๊ฐ์ฒด๋ก ์์ฑ๋ ์ดํ์๋ ์์ฑ ๊ฐ์ด ๋ณํ์ง ์๋๋ค๋ "๋ถ๋ณ ๊ฐ์ฒด ํจํด"์ ์ ์ฉํ๊ธฐ ์ํด readonly
์์ฑ์ ์ถ๊ฐ์ ์ผ๋ก ์์ฑํด ์ฃผ์์ต๋๋ค.
๋ถ๋ณ ๊ฐ์ฒด ํจํด์ด๋? ๋ถ๋ณ ๊ฐ์ฒด ํจํด์ ๊ฐ์ฒด์ ์ํ๊ฐ ํ ๋ฒ ์ค์ ๋๋ฉด ๋ณ๊ฒฝํ ์ ์๋๋ก ํ๋ ๋์์ธ ํจํด์ผ๋ก, ์ด๋ ์ฝ๋์ ์์ ์ฑ๊ณผ ์์ธก ๊ฐ๋ฅ์ฑ์ ๋์ฌ์ค๋๋ค. Java์์๋ final ํค์๋, C#์์๋ readonly ํค์๋๋ฅผ ์ฌ์ฉํด ๊ตฌํํ ์ ์์ต๋๋ค. ๋ถ๋ณ ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ๋ฉด ๋์์ฑ ๋ฌธ์ ๊ฐ ์ค์ด๋ค๊ณ , ๊ฐ์ฒด๋ฅผ ์์ ํ๊ฒ ๊ณต์ ํ ์ ์๋ ์ฅ์ ์ด ์์ต๋๋ค.
์์ฑ๋ ๋ฐ์ดํฐ ํด๋์ค๋ ์๋์ ๊ฐ์ด from
static ๋ฉ์๋๋ฅผ ํตํด ๊ฐ์ฒด๋ฅผ ์์ฑํ ์ ์๋ค.
$feedback = Feedback::from([
'id' => 1,
'point' => 5,
]);
PHPStorm IDE ๊ธฐ์ค์ผ๋ก from
๋ฉ์๋ ์์์์ ์ฐ๊ด ๋ฐฐ์ด ์์ฑ์ ์๋์์ฑ์ด ์ง์๋๊ธฐ ๋๋ฌธ์ ์ ํ ๋ถํธํจ์ด ์์ต๋๋ค.
(VSCode๋ ์์ฆ ์ฌ์ฉํ์ง ์์ ์ ๋ชจ๋ฅด๊ฒ ๋ค์. ์์ฉ ์ํํธ์จ์ด์ ๋ง์ ์์๋ฒ๋ฆฐ ์ดํ์ VSCode๋ฅผ ์ ์ฌ์ฉํ์ง ์๊ฒ๋๋ค์. ๐ฅฒ)
Request ๋ฐ Validation ํด๋์ค๋ก ๋ถ๋ฆฌ
์ ํต์ ์ธ ๋ผ๋ผ๋ฒจ์ Request
๋ฅผ ๋ฐ์ ์ ํจ์ฑ(Validation) ๊ฒ์ฌ๋ฅผ ์งํํ๋ ๋ฐฉ๋ฒ์ ์๋์ ๊ฐ์ต๋๋ค.
<?php
use Illuminate\Http\Request;
class Controller {
public function getFeedback(Request $request)
{
$validated = $request->validate([
'id' => 'required|integer',
'point' => 'require|integer',
]);
...
}
}
๊ฐ๊ฒฐํ๊ณ ์์ฑํ๊ธฐ ๋น ๋ฅธ ๋ฐฉ๋ฒ์ ๋๋ค. ํ์ง๋ง, ์ ๋ ๊ฐ์ธ์ ์ผ๋ก ๋ณต์กํ ์ ํจ์ฑ ๊ฒ์ฌ๊ฐ ๋ค์ด๊ฐ ์ฝ๋๋ ๊ธฐ๋ณธ ๋ก์ง์์ ๋ง์ด ๋ฒ์ด๋๊ธฐ ๋๋ฌธ์ ๋ถ๋ฆฌ ๋์์ผ๋ฉด ์ข๊ฒ ๋ค๋ ๋์ฆ๊ฐ ์์์ต๋๋ค. ์ด๋ด๋ ๋ค์๊ณผ ๊ฐ์ด spatie/laravel-data ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํตํด ๋ถ๋ฆฌ๊ฐ ๊ฐ๋ฅํฉ๋๋ค.
<?php
namespace App\Http\Controllers\Requests;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\Validation\In;
use Spatie\LaravelData\Attributes\Validation\IntegerType;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\Validation\Max;
use Spatie\LaravelData\Attributes\Validation\Required;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
#[MapInputName(SnakeCaseMapper::class)]
class PointRequest extends Data
{
public function __construct(
#[Required, IntegerType]
public readonly int $id,
#[Required, In([1, 2, 3])]
public readonly int $point,
#[Nullable, StringType, Max(50)]
public readonly ?string $userName = null,
) {
}
}
์ฝ๋๋ฅผ ์ดํด๋ณด๋ฉด ๊ธฐ๋ณธ์ ์ผ๋ก Spatie\LaravelData\Data
ํด๋์ค๋ฅผ ์์ ๋ฐ๊ณ , PHP 8์์ ์ถ๊ฐ๋ Attributes์ ์ด์ฉํด์ PointRequest
ํด๋์ค์ ์ถ๊ฐ์ ์ธ ๋์์ด ๋๋๋ก ์ฝ๋๊ฐ ์์ฑ๋์์ต๋๋ค.
#[MapInputName(SnakeCaseMapper::class)]
์ด๋ API ์์ฒญ ํ๋ผ๋ฉํฐ๋ค์ ๊ท์น์ snake_case
๋ก ์์ฒญ๋์ง๋ง ๋ด๋ถ ์ฝ๋๋ camelCase ๊ท์น์ ๋ง์ถ๊ธฐ ์ํ ๋ถ๊ฐ์ ์ธ ๊ธฐ๋ฅ์
๋๋ค. ์๋ฅผ ๋ค๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
์์ฒญ | ์ค๋ช |
---|---|
/api/point?id=1&point=5&user_name=Hello | user_name ํ๋ผ๋ฉํฐ๋ public readonly ?string $userName ๊ณผ ๋งคํ๋จ |
์ด๋ ๊ฒ ์์ฑํ Request ํด๋์ค๋ ์ปจํธ๋กค๋ฌ์์ ๋ค์๊ณผ ๊ฐ์ด ์์ฑ์ด ๊ฐ๋ฅํฉ๋๋ค. ์ ํํ Controller์ Request ํ์ผ์ด ๋ถ๋ฆฌ๋์ด ๋ณด๋ค ๊ท๋ชจ๊ฐ ํฐ ์ ํ๋ฆฌ์ผ์ด์ ์์๋ ๊ด๋ฆฌ๋ฅผ ๋ณด๋ค ํจ์จ์ ์ผ๋ก ํ ์ ์๋๋ก ๋์์ค๋๋ค.
<?php
use App\Http\Controllers\Requests\PointRequest;
class Controller {
public function getFeedback(PointRequest $request)
{
dd($request); // validation์ด ์ํ๋ ํ ์์ฑ๋ ๊ฐ์ฒด
...
}
}
์ปจํธ๋กค๋ฌ ๋ ์ด์ด๊น์ง ์ง์ ํ๊ธฐ ์ ์ ์ ํจ์ฑ ๊ฒ์ฌ๊ฐ ์ํ๋ฉ๋๋ค. (์ ํจ์ฑ ๊ฒ์ฌ๊ฐ ํต๊ณผ๋์ง ๋ชปํ๋ฉด ๋ผ๋ผ๋ฒจ์ 422 Unprocessable Entity๋ฅผ ๋ฐํํจ)
๋ณธ๊ฒฉ DTO(Data Transfer Object) ๊ฐ์ฒด๋ก์ ์ด์ฉ
DTO ๊ฐ์ฒด๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค. ๊ธฐ๋ณธ ์ฌ์ฉ ๋ฐฉ๋ฒ๊ณผ ๋น์ทํฉ๋๋ค. ์ฌ๊ธฐ์๋ ๋ฆฌ์คํธ ํํ์ ๋ฐ์ดํฐ๋ฅผ DTO๋ฅผ ๋ง๋ค์ด ์ด๋ป๊ฒ DTO ์ธ์คํด์ค๋ฅผ ์์ฑํ๋์ง์ ๋ํด ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
BagDto ํด๋์ค๋ฅผ ์์ฑํด ๋ด ๋๋ค.
<?php
namespace App\Services\Product\Dto;
use Carbon\Carbon;
use App\Services\Product\Dto\BagItemDto;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\DataCollection;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
use Illuminate\Support\Collection;
#[MapOutputName(SnakeCaseMapper::class)]
class BagDto extends Data
{
public function __construct(
public readonly int $count,
/** @var Collection<BagItemDto> */
public readonly Collection $items,
) {
}
}
array
ํ์
๋๋ ๋ผ๋ผ๋ฒจ์ ๊ธฐ๋ณธ Collection
ํด๋์ค๋ฅผ ์ด์ฉํด ๋ฆฌ์คํธ ํ์
์ ์ ์ํ ์ ์์ต๋๋ค. ์ฌ๊ธฐ์ PHPDoc์ ์ด์ฉํด ํ์
์ ๋ช
์ํด์ฃผ๋๊ฑธ ๊ถ์ฅํฉ๋๋ค. ๊ทธ๋์ผ IDE์์ ์ ํํ ํ์
์ถ๋ก ์ด ๊ฐ๋ฅํฉ๋๋ค. (PHP๊ฐ Generic์ ์์ง ์ง์ํ์ง ์๋ ๊ฒ์ ๋งค์ฐ ์ํ๊น๊ฒ ์๊ฐํฉ๋๋ค. ๐ฅฒ)
๊ทธ๋ฆฌ๊ณ BadItemDto ํด๋์ค ํ์ผ์ ์๋์ ๊ฐ์ด ์์ฑํฉ๋๋ค.
<?php
namespace App\Services\Product\Dto;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
#[MapOutputName(SnakeCaseMapper::class)]
class BagItemDto extends Data
{
public function __construct(
public readonly int $id,
public readonly string $itemName,
) {
}
}
๋ ๊ฐ์ง์ Dto ํด๋์ค๋ฅผ ๋ง๋ค์๊ณ , ๋ฐ์ดํฐ๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ๋งคํํ ์ ์์ต๋๋ค.
$items = [
['id' => 1, 'itemName' => 'item1'],
['id' => 2, 'itemName' => 'item2'],
];
$bag = BagDto::from([
'count' => 2,
'items' => $items,
]);
BagDto::from();
๋ฉ์๋๋ฅผ ํธ์ถํจ์ผ๋ก์จ $items
์ ์ฐ๊ด๋ฐฐ์ด ๋ฆฌ์คํธ๊ฐ ์๋์ผ๋ก ๋ด๋ถ ๋ฆฌ์คํธ ๊ฐ์ฒด๋ก ์์ฑ๋๊ฒ ๋ฉ๋๋ค.
๋ํ, ๊ฐ Dto ํด๋์ค ํ์ผ ์๋จ์ ์์ฑ๋ผ ์๋ #[MapOutputName(SnakeCaseMapper::class)]
์ฝ๋๋ฅผ ํตํด ๊ฐ์ฒด๊ฐ ๋ผ๋ผ๋ฒจ์์ ์ถ๋ ฅ๋ ๋ ๋ค์์ ์์์ ๊ฐ์ด snake_case๋ก ๋ณํ๋์ด ์ถ๋ ฅ๋ฉ๋๋ค.
์ด๋ ๋ด๋ถ์ ์ปจ๋ฒค์ ์ camelCase๋ก ์ ์งํ๋ฉด์ API ์๋ต ์ปจ๋ฒค์ ์ snake_case๋ก ์ปจ๋ฒค์ ์ ์ ์งํ๋๋ฐ ์ ์ฉํฉ๋๋ค. ๋ผ๋ผ๋ฒจ Resources ๊ธฐ๋ณธ ๊ธฐ๋ฅ์ ํ์ฉํด๋ ๋์ง๋ง, DTO ์์ฑ์ ํตํด ์ด๋ฐ ์ธ์ธํ ๋ถ๋ถ๊น์ง ์ปจํธ๋กคํ ์ ์๋ค๋ ์ ์ด ์ ๊ฒ ํฐ ์ฅ์ ์ผ๋ก ๋ณด์๊ณ , Resources ๊ธฐ๋ฅ ๋์ Attributes์ ํตํด ์ปจ๋ฒค์ ์ ์ ์งํ๊ณ ์์ต๋๋ค.
์กฐ๊ธ ๋ ํด๋์ค ์นํ์ ์ธ ์ฝ๋๊ฐ ๋๋ ๊ฒ ๊ฐ์ต๋๋ค. ๐
Typescript Definition ์๋ ์์ฑํ๊ธฐ
์ ์์ฑ๋ DTO ํ์ผ๋ค์ Attributes๋ฅผ ์ถ๊ฐํ๋ฉด ๋ค์์ ๋ช ๋ น์ด๋ฅผ ํตํด Typescript ํ์ ์ ์ ํ์ผ ๋ํ ์ถ๋ ฅ ๊ฐ๋ฅํ ๋ถ๊ฐ์ ์ธ ๊ธฐ๋ฅ์ด ์กด์ฌํฉ๋๋ค.
php artisan typescript:transform
์! ํด๋น ๋ถ๊ฐ ๊ธฐ๋ฅ์ ์ฌ์ฉํ๊ธฐ ์ํด์๋ spatie/laravel-typescript-transformer ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น๊ฐ ํ์ํฉ๋๋ค.
composer require --dev spatie/laravel-typescript-transformer
์ฆ ์๋์ DTO ํด๋์ค ํ์ผ์
<?php
namespace App\Http\Controllers\Requests;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\Validation\In;
use Spatie\LaravelData\Attributes\Validation\IntegerType;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\Validation\Max;
use Spatie\LaravelData\Attributes\Validation\Required;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
#[MapInputName(SnakeCaseMapper::class), TypeScript]
class PointRequest extends Data
{
public function __construct(
#[Required, IntegerType]
public readonly int $id,
#[Required, In([1, 2, 3])]
public readonly int $point,
#[Nullable, StringType, Max(50)]
public readonly ?string $userName = null,
) {
}
}
(TypeScript
Attributes๋ฅผ ์ถ๊ฐ๋จ)
export interface PointRequest {
id: number;
point: '1' | '2' | '3';
userName?: string;
}
์ ๊ฐ์ด ๋ณํ๋ ํ์ผ์ ์ถ๋ ฅํฉ๋๋ค.
์ด๋ ํ์ฌ์ ๊ฐ๋ฐ ํ๊ฒฝ์ ๋ฐ๋ผ ํ๋ก ํธ์๋ ๊ฐ๋ฐ์์ ํ์ ํ ๋ ๋์์ด ๋ ์๋ ๊ทธ๋ค์ง ํ์ํ์ง ์์ ์๋ ์์ต๋๋ค. ๐
์ฌํํธ: ๊ฐ์ ๋ณ๊ฒฝํ์ฌ ๊ฐ์ฒด ๋ณต์ ํ๊ธฐ
์์ ๋ชจ๋ ์์๋ค์ "๋ถ๋ณ ๊ฐ์ฒด ํจํด"์ ์ ์ฉํ๊ณ ์๊ธฐ ๋๋ฌธ์ readonly
์์ฑ์ ์ ์ํด ์ฃผ์์ต๋๋ค. ๋น์ฐํ ๊ฐ์ด ๋ณํ ํ์๊ฐ ์๋ ๊ฒฝ์ฐ์ ์๋ก์ด ๊ฐ์ผ๋ก ์์ฑ์ ๋ฎ์ผ๋ ค๊ณ ํ๋ฉด ์ค๋ฅ๊ฐ ๋ฐ์ํฉ๋๋ค. ๊ทธ๋ด๋ ์๋์ ๊ฐ์ ๋ฐฉ๋ฒ์ด ๋์์ด ๋ ์๋ ์์ต๋๋ค.
#[MapOutputName(SnakeCaseMapper::class)]
class BagItemDto extends Data
{
public function __construct(
public readonly int $id,
public readonly string $itemName,
) {
}
public function withItemName(string $itemName): self
{
$bagItem = $this->with(); // ์๋์ ๋ฐ์ดํฐ๋ค
$bagItem['itemName'] = $itemName; // ๊ฐ ๋ณ๊ฒฝ
return self::from($bagItem); // ์๋ก์ด ์ธ์คํด์ค ๋ฐํ
}
}
$bagItem = BagItemDto::from([
'id' => 1,
'itemName' => 'item1',
]);
$bagItem->withItemName('item2');
๋ง์น๋ฉฐ
์ด๋๊น์ง๋ ํด๋น ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฑํํ๊ณ ์ฌ์ฉํ๊ณ ๋ ์ ํ์ฌํญ์ ๋๋ค. ๊ท๋ชจ์ ๋ฐ๋ผ์ ๊ทธ๋ฆฌ๊ณ ํ์์ฑ์ ๋ฐ๋ผ์ ๊ธฐ๋ณธ์ ์ธ ๋ผ๋ผ๋ฒจ ํ๋ ์์ํฌ์ ๊ธฐ๋ฅ๋ง์ผ๋ก ์ถฉ๋ถํ ์ ์์ต๋๋ค.
๋ง์ ๊ฐ๋ฐ์๋ถ๋ค๊ณผ ๊ฐ์ด ํ์ ์ด ๋์ด์ผ ํ๋ ์ํฉ์ด๋ผ๋ฉด ๋ชจ๋์ ๋์๋ฅผ ๊ตฌํด์ผ ํ ๊ฒ์ด๊ณ , 1์ธ ๊ท๋ชจ์์๋ ๋ถํ์ํ ๋ ธ๋์ด ๋ ์๋ ์์ต๋๋ค.
์ถฉ๋ถํ ๊ฒํ ํ ์ฌ์ฉํ์๋ ํ๋ช ํ ๊ฐ๋ฐ์๊ฐ ๋์๊ธธ ๋ฐ๋๋๋ค. :)
๋ชจ๋ ๊ธ์ ๋ค์์ ๋ฌธ์ ๋งํฌ์์ ๋ณด๋ค ์ ํํ ๋ด์ฉ ๊ทธ๋ฆฌ๊ณ ์ถ๊ฐ์ ์ธ ๋ถ๊ฐ ๊ธฐ๋ฅ์ ํ์ธํ ์ ์์ต๋๋ค.
https://spatie.be/docs/laravel-data/v4/introduction
์ฐธ์กฐ
ํด๋น ๊ธ์ ์ ๊ฐ์ธ ์๋ฒ ํ์ด์ง๋ฅผ ์ฐธ์กฐํ์ฌ ์ฌ์์ฑ ๋์์ต๋๋ค.