Symfony 7.4 마스터하기: 속성 개선의 힘

발행: (2025년 12월 2일 오후 04:36 GMT+9)
9 min read
원문: Dev.to

Source: Dev.to

Introduction

2025년 11월에 출시된 Symfony 7.4는 프레임워크 역사상 중요한 이정표를 의미합니다. 최신 장기 지원(LTS) 버전으로서, PHP 8에서 네이티브 어트리뷰트가 도입된 이후 지속적으로 발전해 온 보다 표현력 있는 어트리뷰트 기반 아키텍처로의 전환을 확고히 합니다. 시니어 개발자와 아키텍트에게 Symfony 7.4는 단순히 “새 장난감”이 아니라 마찰을 없애는 도구입니다. 복잡한 YAML 설정과 애노테이션 파싱이 필요했던 시절은 사실상 끝났습니다. 이번 릴리스는 표준 PHP 어트리뷰트를 더 똑똑하고 유연하게 만들어, 이전에 보일러플레이트 코드가 필요했던 시나리오까지도 처리할 수 있게 함으로써 개발자 경험(DX)을 크게 향상시킵니다. 아래 예제들은 PHP 8.2+와 Symfony 7.4를 사용하고 있다고 가정합니다.

Routing: env option now accepts an array

Symfony 라우팅에서 특정 환경에만 라우트를 제한하면서 코드를 중복하거나 YAML로 돌아가야 했던 오래된 불편함이 있었습니다. 기존에는 #[Route] 어트리뷰트의 env 옵션이 문자열 하나만 받았습니다. Symfony 7.4에서는 env가 배열을 허용하게 되어, 디버깅 도구, 스모크 테스트, 혹은 devtest에는 존재하지만 prod에는 절대 없어야 하는 “백도어” 라우트를 정의할 때 특히 유용합니다.

// src/Controller/DebugController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class DebugController extends AbstractController
{
    #[Route(
        path: '/_debug/system-health',
        name: 'debug_system_health',
        env: ['dev', 'test'] // New in 7.4: array support
    )]
    public function healthCheck(): Response
    {
        return $this->json([
            'status' => 'ok',
            'environment' => $this->getParameter('kernel.environment')
        ]);
    }
}

Run APP_ENV=dev php bin/console debug:router — the route appears.
Run APP_ENV=prod php bin/console debug:router — the route is absent.

#[CurrentUser] attribute supports Union Types

현대 애플리케이션에서는 방화벽을 공유하지만 서로 다른 로직을 구현하는 여러 사용자 엔티티(예: AdminUserCustomer)가 흔히 존재합니다. 기존에는 #[CurrentUser] 어트리뷰트가 특정 클래스나 UserInterface만을 타입힌트하도록 강제했기 때문에, 컨트롤러 안에서 instanceof 검사를 직접 추가해야 했습니다.

Symfony 7.4는 MapCurrentUser 리졸버를 업데이트하여 Union Types를 지원하게 했으며, 이제 컨트롤러 메서드 시그니처에서 허용 가능한 모든 사용자 클래스를 직접 타입힌트할 수 있습니다.

// src/Controller/DashboardController.php
namespace App\Controller;

use App\Entity\AdminUser;
use App\Entity\Customer;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\CurrentUser;

class DashboardController extends AbstractController
{
    #[Route('/dashboard', name: 'app_dashboard')]
    public function index(
        #[CurrentUser] AdminUser|Customer $user
    ): Response {
        // Symfony automatically resolves the user and ensures it matches one of these types.
        // If the user is logged in but is NOT an AdminUser or Customer, 404/Access Denied logic applies.

        $template = match (true) {
            $user instanceof AdminUser => 'admin/dashboard.html.twig',
            $user instanceof Customer => 'customer/dashboard.html.twig',
        };

        return $this->render($template, ['user' => $user]);
    }
}

이 변경으로 컨트롤러 액션 시작 부분에서 필요했던 보일러플레이트 타입‑체크 코드를 크게 줄일 수 있습니다.

Method‑specific security with #[IsGranted]

#[IsGranted] 어트리뷰트는 선언형 보안의 기본 도구였습니다. 하지만 이전에는 HTTP 메서드와 무관하게 메서드 전체 실행에 적용되었습니다. GET 요청은 ROLE_USER만 허용하고, DELETE 요청은 ROLE_ADMIN만 허용하고 싶다면 로직을 서로 다른 메서드로 나누거나 메서드 내부에서 is_granted()를 사용해야 했습니다.

Symfony 7.4는 #[IsGranted]methods 옵션을 추가했습니다.

// src/Controller/ProductController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

#[Route('/products/{id}')]
class ProductController extends AbstractController
{
    #[Route('', name: 'product_action', methods: ['GET', 'DELETE'])]
    #[IsGranted('ROLE_USER', methods: ['GET'])]   // Checked only for GET requests
    #[IsGranted('ROLE_ADMIN', methods: ['DELETE'])] // Checked only for DELETE requests
    public function action(int $id): Response
    {
        // ... logic
        return new Response('Action completed');
    }
}

이제 보기와 삭제와 같이 같은 리소스에 대한 관련 로직을 하나의 컨트롤러 액션에 유지하면서, 메서드별로 엄격한 보안 정책을 적용할 수 있습니다.

Union Types in #[AsEventListener]

#[AsEventListener] 어트리뷰트는 이벤트 리스너 등록 방식을 혁신했습니다. Symfony 7.4에서는 이 어트리뷰트가 메서드 시그니처에서 Union Types를 지원하게 되었으며, 이는 “모든 이벤트를 잡아내는” 리스너나 여러 서로 다른 이벤트가 동일한 처리 로직을 트리거해야 하는 워크플로우에 매우 강력합니다(예: OrderCreatedOrderUpdated).

// src/EventListener/OrderActivityListener.php
namespace App\EventListener;

use App\Event\OrderCreatedEvent;
use App\Event\OrderUpdatedEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Psr\Log\LoggerInterface;

final class OrderActivityListener
{
    public function __construct(
        private LoggerInterface $logger
    ) {}

    #[AsEventListener]
    public function onOrderActivity(OrderCreatedEvent|OrderUpdatedEvent $event): void
    {
        // Symfony automatically registers this listener for BOTH events.
        // The $event variable is type‑safe.

        $orderId = $event->getOrder()->getId();
        $type = $event instanceof OrderCreatedEvent ? 'created' : 'updated';

        $this->logger->info("Order #{$orderId} was {$type}.");
    }
}

이전에는 타입힌트를 생략하거나(정적 분석 혜택을 잃음) 두 개의 별도 메서드를 등록해야 했습니다.

Automatic signature validation with #[IsSignatureValid]

서명된 URL(예: “이메일 인증을 위해 클릭하세요”)을 검증하려면 보통 UriSigner 서비스를 주입하고 직접 check()를 호출해야 했습니다. Symfony 7.4는 #[IsSignatureValid] 어트리뷰트를 도입해 HttpKernel 단계에서 이를 자동으로 처리합니다. 서명이 유효하지 않거나 만료된 경우, 컨트롤러 코드가 실행되기 전에 Symfony가 AccessDeniedHttpException을 발생시킵니다.

// src/Controller/InvitationController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\HttpKernel\Attribute\IsSignatureValid;

class InvitationController extends AbstractController
{
    #[Route('/invite/accept/{id}', name: 'app_invite_accept')]
    #[IsSignatureValid]
    public function accept(int $id): Response
    {
        // If we reach here, the URL signature is cryptographically valid.
        return new Response("Invitation $id accepted!");
    }
}

Generating a signed URL

// In a command or another controller
public function generateLink(UrlGeneratorInterface $urlGenerator, UriSigner $signer)
{
    $url = $urlGenerator->generate(
        'app_invite_accept',
        ['id' => 123],
        UrlGeneratorInterface::ABSOLUTE_URL
    );
    $signedUrl = $signer->sign($url);
    // Use this $signedUrl to test access.
}

Extending validation for external classes

아마도 Symfony 7.4에서 가장 인상적인 “시니어” 기능은 소유하지 않은 클래스(예: 벤더 라이브러리의 DTO)에도 PHP 어트리뷰트를 사용해 검증 제약조건(및 직렬화 메타데이터)을 부착할 수 있게 된 점입니다. 이전에는 XML이나 YAML 매핑이 필요했지만, 이제 “프록시” 클래스에 #[ExtendsValidationFor] 어트리뷰트를 적용해 외부 타입에 대한 제약조건을 선언할 수 있습니다.

The article cuts off here, but the core idea is that #[ExtendsValidationFor] enables you to enrich third‑party classes with Symfony’s validation system without modifying the original source.

Back to Blog

관련 글

더 보기 »