# Symfony 8 완벽 가이드: PHP 8.4 레이지 오브젝트, 멀티스텝 폼, 2026년 면접 대비까지 > Symfony 8은 PHP 8.4를 필수로 요구하며 네이티브 레이지 오브젝트, AbstractFlowType, 호출 가능 커맨드 등 다수의 신기능을 탑재했습니다. 주요 기능을 코드 예제와 함께 분석하고 2026년 면접 대비 포인트를 정리합니다. - Published: 2026-04-19 - Updated: 2026-04-19 - Author: SharpSkill - Tags: symfony, php, symfony-8, php-84, lazy-objects - Reading time: 12 min --- 2025년 11월에 릴리스된 Symfony 8은 Symfony 7.x 주기 동안 누적된 모든 지원 중단(deprecation) 항목을 제거하고, PHP 8.4 이상을 필수 요구사항으로 설정했습니다. 이 요구사항 덕분에 네이티브 레이지 오브젝트, 프로퍼티 훅, 새로운 HTML5 파서 등 PHP 언어 수준의 기능이 프레임워크 내부에서 직접 활용됩니다. 이 글에서는 일상적인 개발 워크플로에 영향을 미치는 주요 기능, 면접 준비에 도움이 되는 포인트, 그리고 프로덕션 업그레이드 시 주의해야 할 사항을 코드와 함께 상세히 분석합니다. > **Symfony 8 핵심 요약** > > Symfony 8.0은 PHP 8.4 이상을 필수로 하며, 네이티브 멀티스텝 폼(`AbstractFlowType`), `#[Argument]`와 `#[Option]` 속성을 활용한 호출 가능 콘솔 커맨드, 3개의 새로운 JSON/ObjectMapper 컴포넌트를 제공합니다. 또한 DI 컨테이너와 Doctrine 모두에서 프록시 코드 생성이 PHP 8.4 네이티브 레이지 오브젝트로 대체되었습니다. ## 네이티브 레이지 오브젝트가 프록시 코드 생성을 대체하다 PHP 8.4에서는 `ReflectionClass::newLazyGhost()`와 `ReflectionClass::newLazyProxy()`를 통해 엔진 수준의 레이지 오브젝트가 도입되었습니다. Symfony 8은 이 기능을 직접 활용하여, DependencyInjection 컴포넌트가 더 이상 레이지 서비스용 프록시 클래스를 생성하지 않습니다. 대신 첫 번째 프로퍼티 접근 시 초기화되는 고스트 오브젝트가 생성됩니다. 실질적인 영향은 상당합니다. 기존에 Symfony의 프록시 기반 레이지 로딩과 호환되지 않았던 `final` 클래스와 `readonly` 클래스가 별도의 우회 방법 없이 정상적으로 동작합니다. Doctrine도 동일한 혜택을 받아 엔티티 프록시의 코드 생성이 불필요해졌으며, 10년 이상 지속된 유지보수 부담이 해소되었습니다. ```php // src/Service/HeavyReportGenerator.php namespace App\Service; use Symfony\Component\DependencyInjection\Attribute\Lazy; #[Lazy] final readonly class HeavyReportGenerator { public function __construct( private DatabaseConnection $db, private PdfEngine $pdf, private CacheInterface $cache, ) { // Constructor runs only when a method is actually called } public function generate(int $reportId): string { $data = $this->db->fetchReport($reportId); return $this->pdf->render($data); } } ``` `#[Lazy]` 속성은 해당 서비스를 고스트 오브젝트로 인스턴스화하도록 지정합니다. 컨테이너는 `HeavyReportGenerator`와 외형상 동일한 셸을 주입합니다. 생성자와 3개의 주입된 의존성은 `generate()`가 호출될 때에만 실행됩니다. 조건부로 사용되는 서비스(특정 관리자 작업에서만 트리거되는 보고서 생성 등)의 경우, 매 요청마다 발생하던 불필요한 데이터베이스 연결과 메모리 할당이 제거됩니다. `#[Autowire(lazy: true)]` 변형을 사용하면, 서비스 클래스 자체를 수정하지 않고도 호출 측에서 레이지 주입이 가능합니다. ```php // src/Controller/DashboardController.php namespace App\Controller; use Symfony\Component\DependencyInjection\Attribute\Autowire; class DashboardController { public function __construct( #[Autowire(lazy: true)] private HeavyReportGenerator $reportGenerator, ) {} } ``` 면접에서는 고스트 오브젝트와 버추얼 프록시의 차이에 대한 질문이 자주 출제됩니다. 고스트 오브젝트는 원래 인스턴스를 그 자리에서 초기화합니다. 버추얼 프록시는 완전히 초기화된 별도의 인스턴스에 처리를 위임합니다. Symfony는 기본적으로 고스트를 사용하지만, 팩토리가 관련된 경우 자동으로 프록시로 전환합니다. ## AbstractFlowType을 활용한 멀티스텝 폼 Symfony 8 이전에는 멀티스텝 폼 구현을 위해 CraueFormFlowBundle 같은 서드파티 번들이 필요했습니다. 이제 프레임워크가 `AbstractFlowType`을 자체적으로 제공합니다. 이는 스텝별 유효성 검사, 조건부 분기, 내장 네비게이션을 갖춘 네이티브 폼 플로우 엔진입니다. ```php // src/Form/UserSignUpType.php namespace App\Form; use Symfony\Component\Form\Flow\AbstractFlowType; use Symfony\Component\Form\Flow\FormFlowBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; class UserSignUpType extends AbstractFlowType { public function buildFormFlow(FormFlowBuilderInterface $builder, array $options): void { $builder->addStep('personal', UserPersonalType::class); $builder->addStep('professional', UserProfessionalType::class); $builder->addStep('account', UserAccountType::class); $builder->add('navigator', NavigatorFlowType::class); } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => UserSignUp::class, 'step_property_path' => 'currentStep', ]); } } ``` 각 스텝 이름은 유효성 검사 그룹으로도 기능합니다. `UserSignUp` 데이터 클래스는 활성 스텝만 검증하기 위해 그룹 제약 조건이 적용된 `#[Valid]`를 사용합니다. ```php // src/DTO/UserSignUp.php namespace App\DTO; use Symfony\Component\Validator\Constraints as Assert; class UserSignUp { public function __construct( #[Assert\Valid(groups: ['personal'])] public Personal $personal = new Personal(), #[Assert\Valid(groups: ['professional'])] public Professional $professional = new Professional(), #[Assert\Valid(groups: ['account'])] public Account $account = new Account(), public string $currentStep = 'personal', ) {} } ``` 컨트롤러는 표준 Symfony 폼 처리 패턴을 따릅니다. `isFinished()`는 마지막 스텝의 유효성 검사를 통과한 후에만 `true`를 반환합니다. ```php // src/Controller/SignUpController.php #[Route('/signup')] public function __invoke(Request $request): Response { $flow = $this->createForm(UserSignUpType::class, new UserSignUp()) ->handleRequest($request); if ($flow->isSubmitted() && $flow->isValid() && $flow->isFinished()) { $this->userService->register($flow->getData()); return $this->redirectToRoute('app_signup_success'); } return $this->render('signup/flow.html.twig', [ 'form' => $flow->getStepForm(), ]); } ``` 네비게이션 버튼(`NextFlowType`, `PreviousFlowType`, `FinishFlowType`, `ResetFlowType`)은 `include_if`를 통한 조건부 렌더링, `skip`을 통한 스텝 건너뛰기, `back_to`를 통한 직접 네비게이션을 지원합니다. 커스텀 JavaScript 상태 관리 없이도 실무에서 사용되는 대부분의 위저드 패턴을 구현할 수 있습니다. ## 입력 속성을 활용한 호출 가능 커맨드 콘솔 커맨드에서 더 이상 `Command` 기본 클래스를 상속하거나 `configure()`를 오버라이드할 필요가 없습니다. `__invoke()` 메서드가 PHP 속성을 통해 인자와 옵션을 직접 전달받습니다. ```php // src/Command/CreateUserCommand.php namespace App\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\Argument; use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand( name: 'app:create-user', description: 'Create a new user account', )] class CreateUserCommand { public function __invoke( SymfonyStyle $io, #[Argument(description: 'The username for the new account')] string $username, #[Argument(description: 'The email address')] string $email, #[Option(shortcut: 'r', description: 'Role to assign')] string $role = 'ROLE_USER', #[Option(description: 'Activate immediately')] bool $activate = false, ): int { // User creation logic $io->success(sprintf('User "%s" created with role %s.', $username, $role)); return Command::SUCCESS; } } ``` Symfony 8에서는 Backed Enum이 인자와 옵션에서 지원됩니다. 문자열 입력이 해당하는 Enum 케이스로 자동 변환됩니다. ```php // src/Enum/CloudRegion.php enum CloudRegion: string { case UsEast = 'us-east'; case UsWest = 'us-west'; case EuWest = 'eu-west'; } // In the command public function __invoke( SymfonyStyle $io, #[Argument] CloudRegion $region, #[Option] ?ServerSize $size = null, ): int { $io->info(sprintf('Deploying to %s', $region->value)); return Command::SUCCESS; } ``` 파라미터가 많은 커맨드의 경우, `#[MapInput]`을 사용하여 입력을 DTO에 매핑할 수 있습니다. ```php // src/Command/Input/DeployInput.php class DeployInput { #[Argument] public CloudRegion $region; #[Option] public string $branch = 'main'; #[Option(shortcut: 'f')] public bool $force = false; } // src/Command/DeployCommand.php #[AsCommand(name: 'app:deploy')] class DeployCommand { public function __invoke( SymfonyStyle $io, #[MapInput] DeployInput $input, ): int { // Access $input->region, $input->branch, $input->force return Command::SUCCESS; } } ``` 이 패턴으로 `configure()` + `execute()`의 보일러플레이트가 제거되며, DTO를 직접 생성하여 커맨드를 테스트할 수 있게 됩니다. ## JsonStreamer, JsonPath, ObjectMapper 컴포넌트 Symfony 8에는 3개의 새로운 독립형 컴포넌트가 포함되어 있습니다. **JsonStreamer**는 전체 문서를 메모리에 로드하지 않고 대용량 JSON 페이로드를 처리합니다. 메가바이트 규모의 응답을 다루는 API(대량 데이터 내보내기, 분석 피드, 로그 수집 등)에서 `json_decode()`의 메모리 한계 문제를 해결합니다. **JsonPath**는 중첩된 JSON 구조를 탐색하기 위한 RFC 9535의 서브셋을 구현합니다. 전체 응답을 디코딩하고 배열을 수동으로 순회하는 대신, 경로 표현식으로 대상 데이터를 직접 추출할 수 있습니다. **ObjectMapper**는 PHP 속성을 사용하여 DTO와 배열/JSON 간의 변환을 수행하며, 수작업으로 작성하던 하이드레이션 로직을 대체합니다. Serializer 컴포넌트와 결합하면 API 요청/응답 매핑의 전체 라이프사이클을 처리할 수 있습니다. > **면접 출제 경향** > > 2026년 Symfony 면접에서는 레이지 오브젝트(고스트 vs 프록시), `AbstractFlowType`의 라이프사이클, `configure()`에서 호출 가능 커맨드로의 전환이 빈출 주제입니다. 이러한 패턴에 대한 이해는 레거시 지식이 아닌 현재 프레임워크 버전에 대한 숙련도를 보여줍니다. ## 보안 및 개발자 경험 개선사항 CSRF 보호가 더 이상 서버 측 세션을 필요로 하지 않습니다. 새로운 스테이트리스 CSRF 구현은 HTTP 캐싱과 호환되어, API 우선 아키텍처 및 CDN 캐시 페이지에서 활용 가능합니다. Messenger 컴포넌트는 **메시지 서명**을 지원합니다. 페이로드에 대한 암호화 서명으로 프로듀서와 컨슈머 간의 위변조를 방지합니다. 메시지 무결성이 중요한 시스템(결제 처리, 감사 추적 등)에서 커스텀 서명 미들웨어를 대체할 수 있습니다. 새로운 유효성 검사 제약 조건은 문자 세트, MAC 주소, ISO 주 번호, 단어 수, YAML 구문, 슬러그, Twig 템플릿, 동영상 파일 등을 지원합니다. `#[Slug]` 제약 조건 하나만으로도 자주 사용되던 커스텀 밸리데이터를 제거할 수 있습니다. 시큐리티 보터(Security Voter)가 프로파일러에서 **결정 이유를 설명**하게 되었습니다. 기존에 어떤 보터가 접근을 거부했는지 추측해야 했던 인가 로직 디버깅이, Symfony 프로파일러 툴바에서의 직접 조회로 바뀌었습니다. > **업그레이드 경로** > > Symfony 8.0은 7.x 주기의 모든 지원 중단 항목을 삭제하여, 13,202줄의 하위 호환성 코드를 제거했습니다. 권장 접근 방식은 먼저 Symfony 7.4로 업그레이드하고, `php bin/phpunit --display-deprecations`를 실행하여 모든 경고를 수정한 후 8.0으로 전환하는 것입니다. 7.4를 건너뛸 경우 제거된 API로 인한 런타임 오류가 발생할 수 있습니다. ## PHP 8.4 프로퍼티 훅과 Symfony에서의 활용 프로퍼티 훅(클래스 프로퍼티에 직접 정의되는 `get`과 `set`)은 Symfony 생태계와 자연스럽게 통합됩니다. Doctrine 엔티티는 계산된 프로퍼티에 훅을 사용할 수 있습니다. 폼 타입은 데이터 변환에 훅을 활용할 수 있습니다. 유효성 검사 제약 조건은 훅이 적용된 프로퍼티에서 수정 없이 동작합니다. ```php // src/Entity/Product.php class Product { public string $name { set(string $value) { $this->name = trim($value); } } public float $price { set(float $value) { if ($value < 0) { throw new \InvalidArgumentException('Price cannot be negative'); } $this->price = $value; } } public float $priceWithTax { get => $this->price * 1.20; } } ``` 프로퍼티 훅으로 인해 getter/setter 쌍의 필요성이 줄어들고, 불변 조건 적용이 프로퍼티 정의 내부로 이동합니다. Symfony 폼에서는 트랜스포머 보일러플레이트가 감소합니다. Doctrine 엔티티에서는 계산 값이 의존하는 데이터 가까이에 위치하게 됩니다. ## 면접에서 준비해야 할 질문 Symfony 8로 인해 면접에서 출제되는 내용이 변화하고 있습니다. 다음 주제들이 [Symfony 면접 준비](/technologies/symfony/interview-questions/architecture-patterns)에서 빈번하게 등장합니다. - **레이지 오브젝트**: 고스트 오브젝트와 버추얼 프록시의 차이를 설명할 수 있는가. Symfony는 어떤 경우에 어느 것을 선택하는가. PHP 8.4에서 `final readonly` 서비스의 레이지 로딩이 가능해진 이유는 무엇인가. - **멀티스텝 폼**: `AbstractFlowType`은 스텝별 유효성 검사를 어떻게 처리하는가. `step_property_path`의 역할은 무엇인가. 조건부 스텝은 어떻게 동작하는가. - **호출 가능 커맨드**: 호출 가능 커맨드에서 `configure()`를 대체하는 것은 무엇인가. `#[MapInput]`은 어떻게 동작하는가. `#[Option]` 기본값의 규칙은 무엇인가. - **새 컴포넌트**: JsonStreamer가 `json_decode()`보다 선호되는 경우는 언제인가. ObjectMapper가 Serializer로는 해결하지 못하는 어떤 문제를 해결하는가. - **업그레이드 전략**: 8.0 이전에 7.4로 업그레이드하는 것이 권장되는 이유는 무엇인가. 13,202줄의 지원 중단 코드 제거가 하위 호환성에 있어 의미하는 바는 무엇인가. 이러한 주제들은 실제 코드로 연습하는 것이 중요합니다. [Symfony 콘솔 커맨드 모듈](/technologies/symfony/interview-questions/console-commands)과 [Doctrine 심화 모듈](/technologies/symfony/interview-questions/doctrine-advanced)이 가장 빈번하게 테스트되는 영역을 다루고 있습니다. ## 결론 - Symfony 8은 PHP 8.4를 필수로 하며 네이티브 레이지 오브젝트를 전면 도입하여, DI 컨테이너와 Doctrine ORM 모두에서 프록시 코드 생성을 제거 - `AbstractFlowType`이 멀티스텝 폼용 서드파티 번들을 대체하며, 스텝별 유효성 검사 그룹과 내장 네비게이션 버튼을 제공 - `#[Argument]`, `#[Option]`, `#[MapInput]`을 활용한 호출 가능 커맨드가 보일러플레이트를 제거하고 Backed Enum과 DTO를 지원 - 3개의 새 컴포넌트(JsonStreamer, JsonPath, ObjectMapper)가 외부 의존성 없이 JSON 처리와 오브젝트 매핑을 수행 - 스테이트리스 CSRF, 메시지 서명, 시큐리티 보터 디버깅, 20개 이상의 새로운 유효성 검사 제약 조건이 보안과 개발자 경험을 강화 - PHP 8.4의 프로퍼티 훅이 Doctrine 엔티티, Symfony 폼, 유효성 검사 제약 조건과 투명하게 통합 - 업그레이드 경로는 Symfony 7.4를 거쳐야 함: 8.0으로 전환하기 전에 모든 지원 중단 경고를 수정할 것 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/symfony/symfony-8-new-features-php-84-lazy-objects