API Platform Symfony REST: 완벽 튜토리얼과 면접 대비 2026
API Platform 4.3을 사용하여 Symfony에서 REST API를 구축하는 방법을 설명합니다. State Provider/Processor 패턴, 커스텀 필터, 보안 설정, 그리고 기술 면접에서 자주 나오는 질문과 답변을 포함한 실전 가이드입니다.

API Platform 4.3은 OpenAPI 문서 자동 생성, 콘텐츠 협상, 그리고 읽기와 쓰기 작업을 분리하는 클린 아키텍처를 통해 Symfony를 REST API 강자로 변모시킵니다. 이 튜토리얼에서는 설정부터 고급 패턴, 그리고 시니어 개발자와 주니어 개발자를 구분하는 면접 질문까지 다룹니다.
API Platform 4는 GET 작업에 State Provider를, POST/PUT/PATCH/DELETE 작업에 State Processor를 사용합니다. 이러한 분리는 CQRS 원칙에 부합하며 테스트를 용이하게 합니다.
Symfony 7에 API Platform 설치하기
API Platform 설치에는 Symfony 7.2 이상이 필요합니다. 번들은 기본적으로 Doctrine ORM과 통합되지만, Provider/Processor 패턴을 통해 커스텀 데이터 소스도 지원합니다.
# Symfony Flex로 API Platform 설치
composer require api
# 설치 확인
php bin/console debug:router | grep apiapi 레시피는 api-platform/symfony와 함께 serializer, validator, property-access 컴포넌트를 설치합니다. Symfony Flex는 자동으로 /api 하위에 라우트를 구성합니다.
# config/packages/api_platform.yaml
api_platform:
title: 'My API'
version: '1.0.0'
formats:
jsonld: ['application/ld+json']
json: ['application/json']
docs_formats:
jsonld: ['application/ld+json']
jsonopenapi: ['application/vnd.openapi+json']
html: ['text/html']
defaults:
stateless: true
cache_headers:
vary: ['Content-Type', 'Authorization', 'Accept-Language']stateless: true 설정은 모든 API 엔드포인트에서 PHP 세션을 비활성화하여 메모리 오버헤드를 줄이고 수평 확장을 가능하게 합니다.
Attribute를 사용한 첫 번째 API 리소스 생성
API Platform 4는 PHP 8 Attribute를 사용하여 API 리소스를 선언합니다. 각 엔티티는 #[ApiResource] 속성을 통해 API 엔드포인트가 됩니다.
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Metadata\Delete;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity]
#[ApiResource(
operations: [
new GetCollection(),
new Get(),
new Post(security: "is_granted('ROLE_ADMIN')"),
new Put(security: "is_granted('ROLE_ADMIN')"),
new Delete(security: "is_granted('ROLE_ADMIN')")
],
paginationItemsPerPage: 30
)]
class Product
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Assert\NotBlank]
#[Assert\Length(min: 3, max: 255)]
private string $name;
#[ORM\Column(type: 'decimal', precision: 10, scale: 2)]
#[Assert\Positive]
private string $price;
#[ORM\Column]
private \DateTimeImmutable $createdAt;
public function __construct()
{
$this->createdAt = new \DateTimeImmutable();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getPrice(): string
{
return $this->price;
}
public function setPrice(string $price): self
{
$this->price = $price;
return $this;
}
public function getCreatedAt(): \DateTimeImmutable
{
return $this->createdAt;
}
}security 매개변수는 Symfony Expression Language를 사용하여 접근을 제어합니다. 이를 통해 일반 보안 voter와 완전히 통합됩니다.
커스텀 State Provider 구현
State Provider는 읽기 작업의 데이터 가져오기를 처리합니다. 커스텀 Provider를 통해 Doctrine 외의 데이터 소스와 통합하거나 비즈니스 로직을 추가할 수 있습니다.
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Repository\ProductRepository;
use Psr\Cache\CacheItemPoolInterface;
final class ProductStateProvider implements ProviderInterface
{
public function __construct(
private ProductRepository $repository,
private CacheItemPoolInterface $cache
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
if (isset($uriVariables['id'])) {
$cacheKey = 'product_' . $uriVariables['id'];
$item = $this->cache->getItem($cacheKey);
if ($item->isHit()) {
return $item->get();
}
$product = $this->repository->find($uriVariables['id']);
if ($product) {
$item->set($product)->expiresAfter(3600);
$this->cache->save($item);
}
return $product;
}
return $this->repository->findActiveProducts();
}
}Provider를 엔티티에 등록하려면 operation에 지정합니다.
#[ApiResource(
operations: [
new Get(provider: ProductStateProvider::class),
new GetCollection(provider: ProductStateProvider::class)
]
)]커스텀 State Processor를 통한 쓰기 작업
State Processor는 생성, 수정, 삭제 작업을 처리합니다. 이벤트 디스패치나 데이터 변환의 커스텀 로직에 이상적입니다.
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\Product;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use App\Message\ProductCreated;
final class ProductStateProcessor implements ProcessorInterface
{
public function __construct(
private EntityManagerInterface $em,
private MessageBusInterface $messageBus
) {}
public function process(
mixed $data,
Operation $operation,
array $uriVariables = [],
array $context = []
): Product {
if ($operation instanceof \ApiPlatform\Metadata\Post) {
$this->em->persist($data);
$this->em->flush();
$this->messageBus->dispatch(new ProductCreated($data->getId()));
return $data;
}
if ($operation instanceof \ApiPlatform\Metadata\Delete) {
$this->em->remove($data);
$this->em->flush();
return $data;
}
$this->em->flush();
return $data;
}
}Processor를 operation에 적용합니다.
#[ApiResource(
operations: [
new Post(processor: ProductStateProcessor::class),
new Put(processor: ProductStateProcessor::class),
new Delete(processor: ProductStateProcessor::class)
]
)]커스텀 필터와 정렬
API Platform은 내장 필터를 제공하지만, 커스텀 필터를 통해 필터링 로직을 완전히 제어할 수 있습니다.
namespace App\Filter;
use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\QueryBuilder;
final class PriceRangeFilter extends AbstractFilter
{
protected function filterProperty(
string $property,
mixed $value,
QueryBuilder $queryBuilder,
QueryNameGeneratorInterface $queryNameGenerator,
string $resourceClass,
?Operation $operation = null,
array $context = []
): void {
if ($property !== 'priceRange') {
return;
}
$alias = $queryBuilder->getRootAliases()[0];
$minParam = $queryNameGenerator->generateParameterName('minPrice');
$maxParam = $queryNameGenerator->generateParameterName('maxPrice');
if (isset($value['min'])) {
$queryBuilder
->andWhere("$alias.price >= :$minParam")
->setParameter($minParam, $value['min']);
}
if (isset($value['max'])) {
$queryBuilder
->andWhere("$alias.price <= :$maxParam")
->setParameter($maxParam, $value['max']);
}
}
public function getDescription(string $resourceClass): array
{
return [
'priceRange[min]' => [
'property' => 'price',
'type' => 'float',
'required' => false,
'description' => '최소 가격',
],
'priceRange[max]' => [
'property' => 'price',
'type' => 'float',
'required' => false,
'description' => '최대 가격',
],
];
}
}엔티티에 필터를 적용합니다.
use ApiPlatform\Metadata\ApiFilter;
use App\Filter\PriceRangeFilter;
#[ApiResource]
#[ApiFilter(PriceRangeFilter::class)]
class Product
{
// ...
}직렬화 그룹과 DTO
직렬화 그룹은 다양한 컨텍스트에서 노출되는 필드를 제어합니다. DTO는 API 응답과 내부 엔티티를 분리합니다.
use Symfony\Component\Serializer\Annotation\Groups;
#[ApiResource(
normalizationContext: ['groups' => ['user:read']],
denormalizationContext: ['groups' => ['user:write']]
)]
class User
{
#[Groups(['user:read'])]
private ?int $id = null;
#[Groups(['user:read', 'user:write'])]
private string $email;
#[Groups(['user:write'])]
private string $password;
#[Groups(['user:read'])]
private \DateTimeImmutable $createdAt;
}DTO 변환에는 State Provider와 Processor를 사용합니다.
namespace App\Dto;
final class ProductOutput
{
public function __construct(
public readonly int $id,
public readonly string $name,
public readonly string $formattedPrice,
public readonly string $createdAt
) {}
}namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Dto\ProductOutput;
use App\Repository\ProductRepository;
final class ProductOutputProvider implements ProviderInterface
{
public function __construct(private ProductRepository $repository) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?ProductOutput
{
$product = $this->repository->find($uriVariables['id']);
if (!$product) {
return null;
}
return new ProductOutput(
$product->getId(),
$product->getName(),
'₩' . number_format((float) $product->getPrice()),
$product->getCreatedAt()->format('Y년 m월 d일')
);
}
}보안과 API 인증
API Platform은 Symfony Security 및 JWT 인증과 통합됩니다. API Platform 4에서는 operation 레벨과 resource 레벨 모두에서 보안을 설정할 수 있습니다.
#[ApiResource(
security: "is_granted('ROLE_USER')",
operations: [
new GetCollection(),
new Get(security: "is_granted('ROLE_USER') and object.owner == user"),
new Post(security: "is_granted('ROLE_ADMIN')"),
new Put(
security: "is_granted('ROLE_ADMIN') or object.owner == user",
securityMessage: '이 리소스를 편집할 권한이 없습니다.'
),
new Delete(security: "is_granted('ROLE_ADMIN')")
]
)]
class Order
{
// ...
}JWT 인증 설정에는 lexik/jwt-authentication-bundle을 사용합니다.
# config/packages/lexik_jwt_authentication.yaml
lexik_jwt_authentication:
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
pass_phrase: '%env(JWT_PASSPHRASE)%'
token_ttl: 3600# config/packages/security.yaml
security:
firewalls:
api:
pattern: ^/api
stateless: true
jwt: ~
access_control:
- { path: ^/api/login, roles: PUBLIC_ACCESS }
- { path: ^/api, roles: ROLE_USER }Symfony 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
면접에서 자주 나오는 API Platform 질문
기술 면접에서는 API Platform에 대한 깊은 이해가 필요합니다. 다음 질문과 답변은 시니어 개발자 수준의 지식을 다룹니다.
State Provider와 State Processor의 차이점은 무엇입니까?
State Provider는 읽기 작업(GET, GetCollection)에서 데이터를 가져오는 역할을 담당합니다. 데이터베이스 쿼리, 캐시 읽기, 외부 API 호출 등을 처리합니다. 반면 State Processor는 쓰기 작업(POST, PUT, PATCH, DELETE)을 처리하며, 데이터 영속화, 이벤트 디스패치, 관련 서비스 호출을 수행합니다. 이러한 분리는 CQRS 패턴을 따르며, 읽기와 쓰기 로직을 독립적으로 확장하고 테스트할 수 있습니다.
API Platform에서 커스텀 operation을 생성하는 방법은?
커스텀 operation은 표준 CRUD 작업으로 대응할 수 없는 비즈니스 로직에 사용합니다. 자체 State Processor를 구현하고 operation에서 참조합니다.
#[ApiResource(
operations: [
new Post(
uriTemplate: '/products/{id}/publish',
controller: PublishProductController::class,
name: 'publish_product'
)
]
)]N+1 문제를 API Platform에서 해결하려면?
Doctrine extension을 사용하여 eager loading을 설정하거나, 커스텀 State Provider에서 쿼리를 최적화합니다. #[ApiResource(fetchPartial: true)] 옵션도 부분 fetch를 활성화하여 성능을 향상시킵니다.
// 최적화된 쿼리를 사용하는 Repository 메서드
public function findWithRelations(): array
{
return $this->createQueryBuilder('p')
->leftJoin('p.category', 'c')
->addSelect('c')
->getQuery()
->getResult();
}API Platform에서 유효성 검사는 어떻게 작동합니까?
API Platform은 Symfony Validator 컴포넌트를 사용합니다. 엔티티 속성에 유효성 검사 제약 조건을 추가하면 API Platform이 자동으로 요청 데이터를 검증합니다. 검증 오류는 표준화된 JSON 형식으로 반환됩니다. 그룹을 사용하여 operation별로 다른 유효성 검사 규칙을 적용할 수도 있습니다.
#[ApiResource(
operations: [
new Post(validationContext: ['groups' => ['create']]),
new Put(validationContext: ['groups' => ['update']])
]
)]API Platform 페이지네이션을 커스터마이즈하려면?
기본 페이지네이션은 resource 레벨에서 설정할 수 있습니다. 클라이언트 측 제어를 허용하거나 커스텀 Paginator를 구현할 수도 있습니다.
#[ApiResource(
paginationItemsPerPage: 30,
paginationMaximumItemsPerPage: 100,
paginationClientItemsPerPage: true
)]테스트와 CI/CD 통합
API Platform은 API Test를 통한 기능 테스트를 지원합니다.
namespace App\Tests\Api;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
class ProductTest extends ApiTestCase
{
public function testGetProducts(): void
{
$response = static::createClient()->request('GET', '/api/products');
$this->assertResponseIsSuccessful();
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
$this->assertJsonContains(['@context' => '/api/contexts/Product']);
}
public function testCreateProduct(): void
{
$response = static::createClient()->request('POST', '/api/products', [
'json' => [
'name' => '테스트 상품',
'price' => '1500.00'
],
'headers' => ['Authorization' => 'Bearer ' . $this->getToken()]
]);
$this->assertResponseStatusCodeSame(201);
$this->assertJsonContains(['name' => '테스트 상품']);
}
}결론
API Platform 4.3은 Symfony에서 REST API를 구축하기 위한 가장 효율적인 솔루션입니다. State Provider/Processor 아키텍처를 통해 관심사의 분리가 실현되며, 테스트와 유지보수가 용이해집니다. 이 튜토리얼에서 다룬 패턴들은 프로덕션 환경에서 검증되었으며, 면접에서도 자주 질문되는 내용입니다. 커스텀 필터, DTO를 통한 응답 변환, JWT 보안 통합을 마스터하면 확장 가능한 API 아키텍처를 설계할 수 있게 됩니다.
Symfony 코드의 버그를 찾을 수 있나요
실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

작성자
Anthony Fillion-MailletSharpSkill 창업자
10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.
2026년 8월 25일 업데이트
태그
공유
관련 기사

2026년 API Platform과 Symfony: 아키텍처 설계 및 기술 면접 완벽 가이드
API Platform 4.2와 Symfony 7.4를 활용한 REST API 개발 최신 기법을 상세히 설명합니다. State Provider, State Processor, Object Mapper, JSON Streamer를 통한 32% 성능 향상까지, 기술 면접에서 자주 묻는 핵심 개념을 다룹니다.

Symfony 8 완벽 가이드: PHP 8.4 레이지 오브젝트, 멀티스텝 폼, 2026년 면접 대비까지
Symfony 8은 PHP 8.4를 필수로 요구하며 네이티브 레이지 오브젝트, AbstractFlowType, 호출 가능 커맨드 등 다수의 신기능을 탑재했습니다. 주요 기능을 코드 예제와 함께 분석하고 2026년 면접 대비 포인트를 정리합니다.

Doctrine ORM: Symfony에서 관계 마스터하기
Symfony의 Doctrine ORM 관계에 대한 완벽 가이드. OneToMany, ManyToMany, 로딩 전략, 그리고 실용적인 예제를 통한 성능 최적화.