0Pricing
PHP Academy · 강의

계층형 아키텍처에서 클린 아키텍처로

의존성이 안쪽을 향해야 하는 이유를 이해합니다.

계층형 아키텍처에서 클린 아키텍처로은(는) 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 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 계층형 아키텍처에서 클린 아키텍처로
  2. 포트와 어댑터 설명
  3. 사용 사례와 애플리케이션 서비스
  4. 의존성 역전 실전
← PHP Academy(으)로 돌아가기