2026년 API Platform과 Symfony: 아키텍처 설계 및 기술 면접 완벽 가이드

API Platform 4.2와 Symfony 7.4를 활용한 REST API 개발 최신 기법을 상세히 설명합니다. State Provider, State Processor, Object Mapper, JSON Streamer를 통한 32% 성능 향상까지, 기술 면접에서 자주 묻는 핵심 개념을 다룹니다.

API Platform Symfony 2026 아키텍처 및 면접 가이드

API Platform 4.2는 Symfony 애플리케이션이 REST API와 GraphQL API를 노출하는 방식을 근본적으로 변화시켰습니다. 이 버전에서는 깔끔한 리소스 분리를 위한 Symfony Object Mapper, 상당한 성능 향상을 제공하는 JSON Streamer, 그리고 재설계된 필터 시스템이 도입되었습니다. 기술 면접을 준비하는 개발자에게 이러한 아키텍처 패턴에 대한 이해는 시니어와 주니어를 구분하는 중요한 기준이 됩니다.

API Platform 4.2 요구사항

API Platform 4.2는 Symfony 7.4 또는 8.0이 필요합니다. Symfony 6.4 및 7.0-7.3에 대한 지원은 종료되었습니다. JSON Streamer는 컬렉션 엔드포인트에서 초당 최대 32% 더 많은 요청을 처리할 수 있습니다.

API Platform 4.2와 Symfony 설정

API Platform은 Symfony Flex를 통해 자동 구성으로 설치됩니다. 기본 설정은 대부분의 사용 사례를 처리하면서도 복잡한 도메인 요구사항에 대해 완전한 커스터마이징이 가능합니다.

bash
# Install API Platform
composer require api-platform/symfony

# The API documentation is available at /api/
# Open http://localhost:8000/api/ after starting the server
symfony serve

Flex 레시피는 직렬화 그룹, Doctrine 통합, OpenAPI 문서 생성을 자동으로 구성합니다. API 리소스는 엔티티 클래스에 단일 어트리뷰트를 추가하는 것만으로 CRUD 작업을 노출할 수 있습니다.

src/Entity/Book.phpphp
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')")
    ],
    normalizationContext: ['groups' => ['book:read']],
    denormalizationContext: ['groups' => ['book:write']]
)]
class Book
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    #[Assert\NotBlank]
    #[Groups(['book:read', 'book:write'])]
    private string $title;

    #[ORM\Column(type: 'text')]
    #[Groups(['book:read', 'book:write'])]
    private string $description;

    #[ORM\Column]
    #[Groups(['book:read'])]
    private \DateTimeImmutable $createdAt;

    // Getters and setters...
}

이 구성은 자동 검증, 직렬화, OpenAPI 문서를 갖춘 5개의 엔드포인트를 생성합니다.

State Provider: 모든 데이터 소스에서 데이터 가져오기

State Provider는 API Platform이 GET 작업에서 데이터를 검색하는 방식을 제어합니다. 기본 Doctrine Provider는 엔티티 가져오기를 처리하지만, 커스텀 Provider를 사용하면 외부 API, Elasticsearch 또는 캐시된 데이터와의 통합이 가능합니다.

src/State/BookStateProvider.phpphp
namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Repository\BookRepository;
use Psr\Cache\CacheItemPoolInterface;

final class BookStateProvider implements ProviderInterface
{
    public function __construct(
        private BookRepository $repository,
        private CacheItemPoolInterface $cache
    ) {}

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
    {
        // Single item retrieval
        if (isset($uriVariables['id'])) {
            $cacheKey = sprintf('book_%d', $uriVariables['id']);
            $item = $this->cache->getItem($cacheKey);
            
            if ($item->isHit()) {
                return $item->get();
            }
            
            $book = $this->repository->find($uriVariables['id']);
            $item->set($book)->expiresAfter(3600);
            $this->cache->save($item);
            
            return $book;
        }

        // Collection retrieval with custom filtering
        return $this->repository->findActiveBooks();
    }
}

특정 작업에 Provider를 등록하는 방법은 다음과 같습니다.

php
#[ApiResource(
    operations: [
        new GetCollection(provider: BookStateProvider::class),
        new Get(provider: BookStateProvider::class),
        // Other operations use default Doctrine provider
        new Post(),
        new Put(),
    ]
)]
class Book { /* ... */ }

State Processor: 비즈니스 로직이 포함된 뮤테이션 처리

State Processor는 POST, PUT, PATCH, DELETE 작업을 처리합니다. 역직렬화된 데이터를 받아 영속화 전에 비즈니스 로직을 적용합니다.

src/State/BookStateProcessor.phpphp
namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\Book;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;

final class BookStateProcessor implements ProcessorInterface
{
    public function __construct(
        private EntityManagerInterface $em,
        private MailerInterface $mailer,
        private ProcessorInterface $persistProcessor // Decorated Doctrine processor
    ) {}

    public function process(
        mixed $data,
        Operation $operation,
        array $uriVariables = [],
        array $context = []
    ): mixed {
        // Pre-persist business logic
        if ($data instanceof Book && $operation instanceof Post) {
            $data->setCreatedAt(new \DateTimeImmutable());
            $data->setSlug($this->generateSlug($data->getTitle()));
        }

        // Delegate to Doctrine processor
        $result = $this->persistProcessor->process($data, $operation, $uriVariables, $context);

        // Post-persist notification
        if ($operation instanceof Post) {
            $this->notifyNewBook($data);
        }

        return $result;
    }

    private function generateSlug(string $title): string
    {
        return strtolower(preg_replace('/[^a-zA-Z0-9]+/', '-', $title));
    }

    private function notifyNewBook(Book $book): void
    {
        $email = (new Email())
            ->to('catalog@example.com')
            ->subject('New book added: ' . $book->getTitle())
            ->text('A new book has been added to the catalog.');
        $this->mailer->send($email);
    }
}
Processor 데코레이션

기본 Doctrine Processor를 #[AsDecorator]로 데코레이션하면 영속화 동작을 유지하면서 커스텀 로직을 추가할 수 있습니다. 이 패턴은 ORM 작업의 중복을 방지합니다.

Object Mapper: API 리소스와 엔티티 분리

API Platform 4.2는 Symfony Object Mapper 컴포넌트를 통합하여 API 표현을 도메인 엔티티에서 분리합니다. 이 분리를 통해 서로 다른 읽기/쓰기 모델이 가능해지고 내부 엔티티 구조를 보호할 수 있습니다.

src/ApiResource/BookResource.phpphp
namespace App\ApiResource;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use App\Entity\Book;
use Symfony\Component\ObjectMapper\Attribute\Map;

#[ApiResource(
    shortName: 'Book',
    operations: [
        new GetCollection(),
        new Get()
    ]
)]
#[Map(target: Book::class)]
class BookResource
{
    public ?int $id = null;
    
    public string $title;
    
    public string $description;
    
    // Computed field not in entity
    public int $wordCount;
    
    // Formatted date for API consumers
    public string $publishedDate;
}

Mapper Provider는 엔티티를 리소스로 자동 변환합니다.

src/State/BookResourceProvider.phpphp
namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\ApiResource\BookResource;
use App\Repository\BookRepository;
use Symfony\Component\ObjectMapper\ObjectMapperInterface;

final class BookResourceProvider implements ProviderInterface
{
    public function __construct(
        private BookRepository $repository,
        private ObjectMapperInterface $mapper
    ) {}

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
    {
        if (isset($uriVariables['id'])) {
            $book = $this->repository->find($uriVariables['id']);
            return $book ? $this->toResource($book) : null;
        }

        return array_map(
            fn(Book $book) => $this->toResource($book),
            $this->repository->findAll()
        );
    }

    private function toResource(Book $book): BookResource
    {
        $resource = $this->mapper->map($book, BookResource::class);
        $resource->wordCount = str_word_count($book->getDescription());
        $resource->publishedDate = $book->getCreatedAt()->format('F j, Y');
        return $resource;
    }
}

Symfony 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

JSON Streamer: 32% 성능 향상

JSON Streamer 컴포넌트는 전체 데이터셋을 메모리에 로드하지 않고 대규모 컬렉션을 직렬화합니다. Sylius API에서의 벤치마크 결과, 초당 요청 수가 32.4% 증가했습니다.

리소스 또는 작업 수준에서 스트리밍을 활성화하는 방법은 다음과 같습니다.

php
#[ApiResource(
    operations: [
        new GetCollection(
            jsonStream: true,  // Enable JSON streaming
            paginationItemsPerPage: 100
        ),
        new Get()
    ]
)]
class Book { /* ... */ }

스트리밍이 특히 효과적인 경우는 다음과 같습니다.

  • 50개 이상의 항목이 있는 컬렉션 엔드포인트
  • 중첩된 관계가 있는 리소스
  • 대역폭이 제한된 모바일 클라이언트에 API 제공

OpenAPI 명세도 최적화되었습니다. JSON Schema 공유를 통해 파일 크기가 30% 감소하여 문서 로딩 시간이 개선되었습니다.

기술 면접: API Platform 아키텍처 관련 질문

Symfony 포지션 기술 면접에서는 API Platform 패턴이 자주 다뤄집니다. 이러한 질문들은 기본 CRUD 작업을 넘어선 프레임워크 아키텍처에 대한 이해를 평가합니다.

"State Provider와 Processor의 차이점을 설명해 주세요"

기대되는 답변: State Provider는 읽기 작업(GET)의 데이터 가져오기를 처리합니다. 엔티티, DTO 또는 배열을 반환합니다. State Processor는 쓰기 작업(POST, PUT, PATCH, DELETE)을 처리합니다. 역직렬화된 입력을 받아 영속화 전에 비즈니스 로직을 실행합니다. 이 분리는 CQRS 원칙을 따릅니다: 쿼리는 Provider를 통해, 명령은 Processor를 통해 처리됩니다.

"엔티티를 직접 노출하는 대신 커스텀 API 리소스를 사용하는 경우는 언제인가요?"

기대되는 답변: 커스텀 리소스는 다음과 같은 경우에 적용됩니다.

  • API 표현이 데이터베이스 스키마와 다른 경우
  • 계산된 필드가 여러 엔티티에서 집계를 필요로 하는 경우
  • 쓰기 모델과 읽기 모델에 서로 다른 구조가 필요한 경우
  • 내부 엔티티 필드를 API 소비자로부터 숨겨야 하는 경우
  • 엔티티가 발전하는 동안 버전 호환성을 위해 안정적인 계약이 필요한 경우

"API Platform은 검증을 어떻게 처리합니까?"

기대되는 답변: API Platform은 엔티티 속성에 대한 Symfony Validator 제약조건을 사용합니다. 검증은 State Processor가 실행되기 전 역직렬화 중에 자동으로 실행됩니다. 검증 그룹은 작업별로 적용되는 제약조건을 제어합니다. 커스텀 검증기는 표준 Symfony 메커니즘을 통해 통합됩니다.

php
#[ApiResource(
    operations: [
        new Post(validationContext: ['groups' => ['create']]),
        new Put(validationContext: ['groups' => ['update']])
    ]
)]
class Book
{
    #[Assert\NotBlank(groups: ['create', 'update'])]
    private string $title;

    #[Assert\Isbn(groups: ['create'])]
    private string $isbn;  // Required only on creation
}
면접에서 흔한 실수

지원자들은 종종 검증 그룹이나 커스텀 제약조건을 언급하지 않고 검증을 "자동"이라고만 설명합니다. 면접관은 작업별로 검증을 커스터마이징하는 방법에 대한 이해를 확인합니다.

"API Platform이 제공하는 보안 메커니즘은 무엇입니까?"

기대되는 답변: API Platform은 다음을 통해 Symfony Security와 통합됩니다.

  • 역할 기반 액세스를 위한 작업의 security 속성
  • 데이터 바인딩 후 객체 수준 검사를 위한 securityPostDenormalize
  • 복잡한 인가 로직을 위한 Voter
  • Symfony Rate Limiter 통합을 통한 속도 제한
php
#[ApiResource(
    operations: [
        new Get(
            security: "is_granted('ROLE_USER')"
        ),
        new Put(
            security: "is_granted('ROLE_ADMIN') or object.getOwner() == user",
            securityPostDenormalize: "is_granted('BOOK_EDIT', object)"
        )
    ]
)]

필터와 페이지네이션: 고급 쿼리 패턴

API Platform 필터를 통해 클라이언트는 URL 파라미터로 컬렉션을 쿼리할 수 있습니다. 4.2의 필터 시스템은 확장성을 향상시키기 위해 재설계되었습니다.

php
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Metadata\ApiFilter;

#[ApiResource]
#[ApiFilter(SearchFilter::class, properties: [
    'title' => 'partial',      // LIKE %value%
    'author.name' => 'exact'   // Nested property
])]
#[ApiFilter(DateFilter::class, properties: ['createdAt'])]
#[ApiFilter(OrderFilter::class, properties: ['title', 'createdAt'])]
class Book { /* ... */ }

생성되는 엔드포인트:

text
GET /api/books?title=symfony           # Search by title
GET /api/books?createdAt[after]=2026-01-01  # Date range
GET /api/books?order[createdAt]=desc   # Sorting

API Platform 리소스 테스트

API Platform은 기능 테스트를 단순화하는 테스트 클라이언트를 제공합니다. ApiTestCase 클래스는 API 응답에 특화된 어설션을 제공합니다.

tests/Api/BookTest.phpphp
namespace App\Tests\Api;

use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use App\Entity\Book;

class BookTest extends ApiTestCase
{
    public function testGetCollection(): void
    {
        $response = static::createClient()->request('GET', '/api/books');

        $this->assertResponseIsSuccessful();
        $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
        $this->assertJsonContains([
            '@context' => '/api/contexts/Book',
            '@type' => 'Collection'
        ]);
    }

    public function testCreateBook(): void
    {
        $response = static::createClient()->request('POST', '/api/books', [
            'json' => [
                'title' => 'Symfony Best Practices',
                'description' => 'A guide to modern Symfony development'
            ],
            'headers' => ['Authorization' => 'Bearer ' . $this->getToken()]
        ]);

        $this->assertResponseStatusCodeSame(201);
        $this->assertJsonContains(['title' => 'Symfony Best Practices']);
    }

    public function testCreateBookValidationFails(): void
    {
        $response = static::createClient()->request('POST', '/api/books', [
            'json' => ['description' => 'Missing title'],
            'headers' => ['Authorization' => 'Bearer ' . $this->getToken()]
        ]);

        $this->assertResponseStatusCodeSame(422);
        $this->assertJsonContains([
            'violations' => [
                ['propertyPath' => 'title', 'message' => 'This value should not be blank.']
            ]
        ]);
    }
}

API Platform과 Symfony 핵심 포인트

  • State Provider는 GET 작업을, State Processor는 뮤테이션을 처리합니다. 이 분리를 통해 커스텀 데이터 소스와 비즈니스 로직을 갖춘 깔끔한 아키텍처가 가능합니다
  • Object Mapper 컴포넌트는 API 리소스를 Doctrine 엔티티에서 분리하여 서로 다른 읽기/쓰기 모델을 가능하게 하고 내부 구조를 보호합니다
  • JSON Streamer는 전체 메모리 할당 없이 직렬화하여 컬렉션 엔드포인트에서 32% 성능 향상을 제공합니다
  • 보안은 표준 Symfony 메커니즘을 통해 통합됩니다: security 표현식, Voter, Rate Limiter 컴포넌트
  • 검증 그룹은 작업별로 제약조건 적용을 커스터마이징합니다
  • 필터는 자동으로 쿼리 파라미터를 노출하며, 검색, 날짜, 정렬 필터가 대부분의 사용 사례를 커버합니다
  • 면접 질문은 아키텍처 결정에 초점을 맞춥니다: 커스텀 Provider 사용 시기, 읽기/쓰기 모델 분리 방법, 보안 구현 패턴

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

오늘의 챌린지

Symfony 코드의 버그를 찾을 수 있나요

실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

Anthony Fillion-Maillet

작성자

Anthony Fillion-Maillet

SharpSkill 창업자

10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.

2026년 9월 8일 업데이트

태그

#symfony
#api-platform
#rest-api
#interview

공유

관련 기사