계층형 아키텍처에서 클린 아키텍처로
의존성이 안쪽을 향해야 하는 이유를 이해합니다.
계층형 아키텍처에서 클린 아키텍처로은(는) CoddyKit의 무료 PHP Academy 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 PHP Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. PHP Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
클린 아키텍처를 사용하는 이유
고전적인 3계층 PHP 구조인 컨트롤러 → 서비스 → 저장소 → 데이터베이스는 이미 알고 계실 것입니다. 이 구조는 작동하지만 비즈니스 로직이 Eloquent, Doctrine, HTTP 요청, 프레임워크 수명 주기에 결합됩니다. 클린 아키텍처는 의존성의 방향을 뒤집어 도메인이 인프라를 전혀 알지 못하게 합니다. 그 결과 사용 사례를 테스트할 수 있고, 어댑터를 교체할 수 있으며, 프레임워크 업그레이드에도 견디는 코드베이스를 얻습니다.
의존성 규칙
클린 아키텍처의 유일한 규칙은 소스 코드의 의존성이 오직 안쪽을 향해야 한다는 것입니다. 안쪽 원(엔터티, 사용 사례)은 바깥쪽 원(컨트롤러, ORM, 프레임워크)을 절대 참조해서는 안 됩니다. 실행 시 제어 흐름은 인터페이스를 통해 바깥쪽으로 향할 수 있지만, 컴파일/가져오기 시점에는 안쪽 요소가 바깥쪽 요소를 가져오지 않습니다.
- 엔터티: 기업 규칙
- 사용 사례: 애플리케이션 규칙
- 어댑터: 컨트롤러, 프레젠터, 게이트웨이
- 프레임워크 및 드라이버: DB, HTTP, 웹
결합된 계층 구조 예제
대부분의 PHP 애플리케이션이 제공하는 서비스가 바로 이런 형태입니다. 도메인 로직이 Eloquent와 HTTP 응답에 뒤엉켜 있다는 점에 주목하십시오. 데이터베이스와 프레임워크 없이는 할인 규칙을 단위 테스트할 수 없습니다.
<?php
class OrderService
{
public function place(Request $request)
{
$user = User::find($request->user_id); // Eloquent
$total = 0;
foreach ($request->items as $i) {
$total += Product::find($i['id'])->price * $i['qty'];
}
if ($user->is_vip) {
$total *= 0.9; // business rule trapped in infra code
}
Order::create(['user_id' => $user->id, 'total' => $total]);
return response()->json(['total' => $total]);
}
}엔터티: 프레임워크가 없는 도메인
엔터티는 전사적인 규칙을 인코딩하며 어떤 것에도 의존하지 않습니다. 주석도 없고 ORM의 기본 클래스도 없는 순수한 PHP입니다. 테스트에서 완전히 생성할 수 있습니다.
<?php
final class Money
{
public function __construct(public readonly int $cents) {
if ($cents < 0) throw new InvalidArgumentException('negative money');
}
public function multiply(float $factor): self {
return new self((int) round($this->cents * $factor));
}
}
final class Order
{
/** @param array<int,int> $lineCents */
public function __construct(private array $lineCents, private bool $vip) {}
public function total(): Money {
$sum = array_sum($this->lineCents);
$money = new Money($sum);
return $this->vip ? $money->multiply(0.9) : $money;
}
}
echo (new Order([1000, 2000], true))->total()->cents, PHP_EOL; // 2700사용 사례가 작업 흐름을 소유합니다
사용 사례(인터랙터)는 엔터티를 조정하고 인터페이스(포트)를 통해서만 외부 세계와 통신합니다. 요청 DTO를 받고 응답 DTO를 반환하며, HTTP 객체를 반환하지 않습니다.
<?php
interface OrderRepository {
public function save(Order $order): void;
}
final class PlaceOrder
{
public function __construct(private OrderRepository $orders) {}
public function execute(array $lineCents, bool $vip): int {
$order = new Order($lineCents, $vip);
$this->orders->save($order);
return $order->total()->cents;
}
}경계는 인터페이스입니다
사용 사례는 필요한 OrderRepository 인터페이스를 선언합니다. 인터페이스는 안쪽 원에 있고, 구체적인 Eloquent/Doctrine 구현은 바깥쪽에 있으며 안쪽을 의존합니다. 이는 아키텍처 경계에 적용한 의존성 역전 원칙입니다.
소스 의존성의 방향: EloquentOrderRepository → OrderRepository(인터페이스)이며, 그 반대는 허용되지 않습니다.
<?php
// Lives in infrastructure layer, points INWARD to the domain interface
final class EloquentOrderRepository implements OrderRepository
{
public function save(Order $order): void {
OrderModel::create(['total' => $order->total()->cents]);
}
}인프라 없이 테스트하기
사용 사례가 인터페이스에 의존하므로 테스트에서는 가짜 구현을 주입합니다. 데이터베이스와 프레임워크 부팅이 필요 없으므로, 순수한 비즈니스 동작을 검증하는 단위 테스트를 마이크로초 단위로 빠르게 실행할 수 있습니다.
<?php
final class InMemoryOrders implements OrderRepository {
public array $saved = [];
public function save(Order $o): void { $this->saved[] = $o; }
}
$repo = new InMemoryOrders();
$useCase = new PlaceOrder($repo);
$total = $useCase->execute([1000, 2000], true);
assert($total === 2700);
assert(count($repo->saved) === 1);
echo "PASS total=$total saved=" . count($repo->saved) . PHP_EOL;컨트롤러는 얇은 어댑터가 됩니다
이제 컨트롤러는 어댑터입니다. HTTP를 사용 사례 호출로 변환하고 결과를 다시 HTTP로 변환합니다. 비즈니스 규칙은 담지 않습니다. REST를 CLI나 큐 작업자로 교체해도 사용 사례는 변경되지 않습니다.
<?php
final class OrderController
{
public function __construct(private PlaceOrder $placeOrder) {}
public function store(Request $request): JsonResponse {
$total = $this->placeOrder->execute(
lineCents: $request->input('lineCents'),
vip: (bool) $request->input('vip'),
);
return new JsonResponse(['total' => $total], 201);
}
}도메인을 드러내는 아키텍처
폴더 구조는 프레임워크가 아니라 도메인을 드러내야 합니다. 최상위에 Controllers/, Models/를 두지 마십시오. 역량을 기준으로 구성하여 처음 보는 사람도 애플리케이션이 무엇을 하는지 알 수 있게 하십시오.
src/Ordering/Domain/— 엔터티, 값 객체src/Ordering/Application/— 사용 사례, 포트 인터페이스src/Ordering/Infrastructure/— Eloquent 저장소, HTTP 컨트롤러
각 경계 컨텍스트는 최상위 폴더가 되고, 프레임워크는 가장자리에 위치합니다.
의존성 규칙 강제하기
도구가 없으면 규율은 약해집니다. CI에서 deptrac 또는 phparkitect를 사용하여 Domain이 Infrastructure를 가져오면 빌드가 실패하도록 하십시오. 그러면 이 규칙은 코드 리뷰의 기대가 아니라 컴파일 시점에 보장됩니다.
# deptrac.yaml
deptrac:
layers:
- name: Domain
collectors: [{ type: directory, value: src/.*/Domain/.* }]
- name: Application
collectors: [{ type: directory, value: src/.*/Application/.* }]
- name: Infrastructure
collectors: [{ type: directory, value: src/.*/Infrastructure/.* }]
ruleset:
Domain: [] # Domain may depend on nothing
Application: [Domain]
Infrastructure: [Application, Domain]DTO로 경계 넘기
엔터티가 바깥으로 새어 나가지 않게 하려면 경계를 넘는 데이터는 엔터티나 ORM 모델이 아니라 단순한 DTO로 전달해야 합니다. 사용 사례는 어댑터가 직렬화할 수 있는 평면 구조를 반환하므로 도메인 객체가 코어 밖으로 나가지 않고, 바깥 계층도 내부 상태에 접근할 수 없습니다.
<?php
final class OrderSummary // boundary DTO, no behavior, no domain types
{
public function __construct(
public readonly string $orderId,
public readonly int $totalCents,
) {}
}
final class PlaceOrderV2 {
public function __construct(private OrderRepository $orders) {}
public function execute(array $lineCents, bool $vip): OrderSummary {
$order = new Order($lineCents, $vip);
$this->orders->save($order);
return new OrderSummary('ord_1', $order->total()->cents);
}
}간단히 확인하기
의존성 규칙에서 허용되는 의존성 방향은 무엇입니까?
복습
결합된 계층 구조에서 클린 아키텍처로 이동했습니다.
- 의존성 규칙: 소스 의존성은 오직 안쪽을 향합니다.
- 엔터티는 프레임워크가 없는 PHP로 기업 규칙을 담습니다.
- 사용 사례는 포트 인터페이스를 통해 작업을 조정하고, HTTP가 아닌 DTO를 반환합니다.
- 컨트롤러와 ORM 저장소는 안쪽을 의존하는 바깥쪽 어댑터입니다(DIP).
- 구조는 도메인을 드러내야 하며, deptrac과 같은 도구가 CI에서 이 규칙을 강제합니다.
자주 묻는 질문
“계층형 아키텍처에서 클린 아키텍처로” 강의는 무료인가요?
네 — “계층형 아키텍처에서 클린 아키텍처로” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 PHP Academy 강의 전체를 잠금 해제할 수 있습니다. PHP Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“계층형 아키텍처에서 클린 아키텍처로”에서 뭘 배우나요?
의존성이 안쪽을 향해야 하는 이유를 이해합니다. 브라우저에서 직접 실행하는 실습 코드로 PHP Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
PHP Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 PHP Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.
“계층형 아키텍처에서 클린 아키텍처로” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 PHP Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 PHP Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 계층형 아키텍처에서 클린 아키텍처로
- 포트와 어댑터 설명
- 사용 사례와 애플리케이션 서비스
- 의존성 역전 실전