Waaseyaa의 도메인 라우팅: 거대한 디스패처를 작은 라우터로 교체
I’m happy to translate the article for you, but I’ll need the text of the post itself. Could you please paste the content you’d like translated (excluding the source link you’ve already provided)? Once I have the article text, I’ll translate it into Korean while preserving the original formatting, markdown, and technical terms.
Ahnii!
Waaseyaa는 1,000줄이 넘는 컨트롤러 디스패처를 가지고 있었습니다. 새로운 기능이 추가될 때마다 같은 파일에 조건문이 더해졌습니다. 이 글에서는 그 디스패처를 도메인‑특화 라우터들로 교체하는 방법을 다룹니다. 각 라우터는 라우팅 로직을 범위별로 구분하고 테스트 가능하게 만드는 두 메서드 인터페이스를 구현합니다.
graph TD
A[HTTP Request] --> B[Router]
B -->|matches URL to route| C[Dispatcher]
C -->|picks controller, injects context| D[Controller]
D --> E[Response]
라우터는 URL을 라우트 정의와 매핑합니다. 디스패처는 그 매핑을 받아 어떤 컨트롤러를 인스턴스화하고, 어떤 메서드를 호출하며, 요청 컨텍스트를 어떻게 전달할지 결정합니다. 대부분의 프레임워크에서는 프레임워크가 이를 처리해 주기 때문에 별로 생각하지 않게 됩니다.
The Problem
디스패처는 “어떤 코드를 실행할지”를 결정하는 장소가 되면서 긴 조건문 체인으로 변질됩니다. 모든 요청 타입을 처리하는 단일 디스패처는 조건문이 빠르게 누적됩니다. 결과는 다음과 같은 코드가 됩니다:
public function dispatch(Request $request): Response
{
$controller = $request->attributes->get('_controller');
if ($controller === 'entity_types') {
// 40 lines of entity type listing logic
} elseif (str_starts_with($controller, 'entity_type.')) {
// 60 lines of lifecycle management
} elseif ($controller === 'openapi') {
// 80 lines of OpenAPI spec generation
} elseif (str_contains($controller, 'SchemaController')) {
// 50 lines of schema handling
}
// ... and so on for every domain
}
엔티티 CRUD, 스키마 생성, 라이프사이클 관리, OpenAPI 문서 – 모두 하나의 클래스에 모여 있습니다. 새로운 기능이 추가될 때마다 또 다른 분기가 생기고, 어느 하나의 경로를 테스트하려면 모든 컨텍스트를 로드해야 합니다.
The Fix
해결책은 “더 좋은” 디스패처가 아니라 작고 집중된 라우터들입니다. 각 라우터는 하나의 도메인을 담당합니다.
도메인 라우터는 애플리케이션 요청 처리의 한 조각을 담당하는 작은 클래스입니다. 모든 도메인을 알고 있는 단일 디스패처 대신, 각 라우터는 두 가지 질문에 답합니다:
- “이 요청은 내 것인가?” – 요청을 검사하고 불리언을 반환합니다.
- “예, 처리한다.” – 작업을 수행하고 응답을 반환합니다.
interface DomainRouterInterface
{
// "Is this request mine?" — inspects the request, returns a boolean.
public function supports(Request $request): bool;
// "Yes, handle it." — does the work, returns a response.
public function handle(Request $request): Response;
}
디스패처는 등록된 라우터들을 순서대로 반복합니다. supports()가 true를 반환하는 첫 번째 라우터가 승리합니다. 이는 책임 연쇄(Chain of Responsibility) 패턴을 명시적인 계약과 함께 구현한 것입니다.
Source: …
예시 라우터
엔티티‑타입 라이프사이클 라우터
Waaseyaa를 사용하면 엔티티 타입(예: “Article”, “User”, “Comment”)을 정의할 수 있습니다. 때때로 하나를 비활성화해야 할 수도 있고, 콘텐츠 타입을 폐기하거나 꺼진 타입을 다시 활성화해야 할 수도 있습니다. 이 라우터가 바로 그 작업을 담당합니다.
final class EntityTypeLifecycleRouter implements DomainRouterInterface
{
use JsonApiResponseTrait; // 일관된 JSON:API 응답 포맷팅
public function __construct(
// Registry: 어떤 엔티티 타입이 존재하고 어떤 기능을 갖는지 알고 있음
private readonly EntityTypeManager $entityTypeManager,
// 비활성화/활성화 상태 변화를 처리
private readonly EntityTypeLifecycleManager $lifecycleManager,
) {}
public function supports(Request $request): bool
{
// Prefix match: "entity_type."으로 시작하는 모든 것이 여기로 라우팅됨.
// 나중에 entity_type.archive를 추가해도 이 메서드 수정이 필요 없음.
$controller = $request->attributes->get('_controller', '');
return $controller === 'entity_types'
|| str_starts_with($controller, 'entity_type.');
}
public function handle(Request $request): Response
{
$controller = $request->attributes->get('_controller', '');
return match ($controller) {
'entity_types' => $this->listTypes($request),
'entity_type.disable' => $this->disableType($request),
'entity_type.enable' => $this->enableType($request),
default => $this->jsonApiResponse(404, []),
};
}
}
하나의 클래스, 하나의 도메인, 격리된 상태에서 완전하게 테스트 가능.
스키마 라우터
API는 또한 OpenAPI 스펙과 각 엔티티 타입에 대한 스키마 정의를 제공해야 합니다. 이는 라이프사이클 관리와는 다른 도메인이므로 별도의 라우터를 사용합니다.
final class SchemaRouter implements DomainRouterInterface
{
use JsonApiResponseTrait;
public function __construct(
private readonly EntityTypeManager $entityTypeManager,
// 스키마 엔드포인트는 엔티티 자체와 동일한 접근 규칙을 적용함.
// 엔티티 타입에 접근할 수 없으면 해당 스키마도 읽을 수 없음.
private readonly EntityAccessHandler $accessHandler,
) {}
public function supports(Request $request): bool
{
$controller = $request->attributes->get('_controller', '');
return $controller === 'openapi'
|| str_contains($controller, 'SchemaController');
}
public function handle(Request $request): Response
{
$controller = $request->attributes->get('_controller', '');
if ($controller === 'openapi') {
return $this->generateOpenApiSpec($request);
}
// SchemaController 라우트는 엔티티 타입을 라우트 파라미터로 가짐
$entityType = $request->attributes->get('entity_type_id', '');
return $this->showSchema($entityType, $request);
}
}
두 라우터 모두 요청으로부터 타입이 지정된 컨텍스트를 가져오지만 자체적으로 파싱을 수행하지는 않습니다.
그 컨텍스트는 어디서 오는가?
Middleware가 상위에서 인증, 본문 파싱 및 컨텍스트 조립을 라우터가 요청을 보기 전에 수행합니다:
// By the time handle() runs, the request carries everything the router needs.
// No token parsing, no JSON decoding, no service lookups.
$account = $request->attributes->get('_account'); // authenticated user
$storage = $request->attributes->get('_broadcast_storage'); // storage backend
$body = $request->attributes->get('_parsed_body'); // already decoded JSON, etc.
이러한 분리를 통해:
- 라우터는 작고 단일 도메인에 집중합니다.
- 디스패칭은
DomainRouterInterface구현체들을 순회하는 간단한 루프가 됩니다. - 테스트는 각 라우터를 격리된 상태에서 수행하기 쉬워집니다 – 라우터가 기대하는 요청 속성만 제공하면 됩니다.
TL;DR
단일 디스패처를 도메인‑특화 라우터 컬렉션으로 교체하고, 각 라우터가 두 개의 메서드 계약(supports + handle)을 구현하도록 합니다. 이렇게 하면:
- 코드가 더 깔끔하고 유지보수가 쉬워집니다.
- 테스트가 더 빠르고 집중됩니다.
- 책임 연쇄(Chain of Responsibility) 패턴을 적용하기에 자연스러운 위치가 됩니다.
다음 프로젝트에서 한 번 시도해 보세요 – 첫 번째 라우터를 추가한 뒤 바로 차이를 느낄 수 있을 겁니다!
$t->attributes->get('_parsed_body'); // deserialized JSON
$context = $request->attributes->get('_waaseyaa_context'); // framework context
인프라 작업은 한 번만 수행되고, 상류에서 처리됩니다. 라우터는 도메인 로직에만 집중합니다.
Source: …
새로운 도메인에 대한 스켈레톤
대량 가져오기 작업을 처리하고 싶다고 가정해 보세요:
final class BulkImportRouter implements DomainRouterInterface
{
use JsonApiResponseTrait;
public function supports(Request $request): bool
{
return str_starts_with(
$request->attributes->get('_controller', ''),
'bulk_import.',
);
}
public function handle(Request $request): Response
{
$controller = $request->attributes->get('_controller', '');
return match ($controller) {
'bulk_import.csv' => $this->importCsv($request),
'bulk_import.json' => $this->importJson($request),
default => $this->jsonApiResponse(404, []),
};
}
}
- 기존 라우터를 변경할 필요가 없습니다.
- 디스패처를 수정할 필요가 없습니다.
- 라우터 컬렉션에 등록하면 디스패처가 자동으로 인식합니다.
- 인터페이스는 새로운 도메인이 추가된다는 것을 보장합니다.
천 줄에 달하던 디스패처는 사라지고, 그 자리에 경계가 명확한 작은 클래스들이 생겨 각각을 독립적으로 테스트할 수 있게 됩니다:
$router = new EntityTypeLifecycleRouter($entityTypeManager, $lifecycleManager);
$request = new Request();
$request->attributes->set('_controller', 'entity_type.disable');
$request->attributes->set('_parsed_body', ['entity_type_id' => 'article']);
$response = $router->handle($request);
// No framework boot, no database, no middleware chain
entity_type.disable 요청을 보면, 정확히 어떤 파일이 이를 처리하는지 알 수 있습니다. 거대한 클래스 안의 switch 문을 추적할 필요가 없습니다.
Baamaapii