PHpullh

PHP · 심층 가이드

PHP 패턴 완전 정리

Front Controller에서 시작해 미들웨어 파이프라인, 서비스 컨테이너, Repository와 데코레이터까지 PHP 프레임워크가 실제로 쓰는 구조를 직접 만들어 보며 이해합니다.

주제 18개 · 예제 코드 포함 · 최종 수정 2026-08-30 · 작성 pullh 편집팀

PHP의 설계 패턴은 교과서보다 PSR 표준과 프레임워크 관행에 더 강하게 묶여 있습니다. 클래스 하나가 파일 하나에 대응하는 PSR-4 자동 로딩, 요청과 응답을 값 객체로 다루는 HTTP 메시지 규약, 그 사이를 지나가는 미들웨어 개념이 사실상 공용어입니다. 여기에 요청마다 프로세스가 새로 시작하는 실행 모델이 겹치면서, 부팅 비용을 줄이려는 지연 생성과 캐싱이 거의 모든 구조에 스며들어 있습니다. 패턴의 이름보다 이 제약을 먼저 이해하는 편이 빠릅니다.

Front Controller 기본이 출발점입니다. 모든 요청을 한 진입점으로 모아 두면 그 지점에서 Middleware 파이프라인으로 인증과 로깅 같은 횡단 관심사를 겹겹이 감쌀 수 있고, 최종 처리기를 고르는 일은 match 기반 라우터가 맡습니다. 객체를 누가 만드느냐는 문제는 Service Container 간단 구현Factory Method가 나눠 다루고, 데이터 접근은 Repository 패턴으로 경계를 그은 뒤 캐시 데코레이터를 씌워 원본 코드를 건드리지 않고 성능을 얻는 흐름을 보여 줍니다.

배포에서 터지는 대표적 사고가 PSR-4 자동 로딩 규칙과 관련이 있습니다. macOS 기본 파일시스템은 대소문자를 구분하지 않아 src/Http/Kernel.phpsrc/http/Kernel.php로 적어도 로컬에서는 잘 돌아가지만, 대소문자를 구분하는 리눅스 서버에 올리는 순간 클래스를 찾지 못합니다. 네임스페이스와 디렉터리 이름을 글자 단위로 맞추십시오. 컨테이너 쪽에도 함정이 있습니다. 컨테이너 자체를 클래스 안으로 주입해 필요할 때마다 꺼내 쓰면 서비스 로케이터가 되어 의존성이 생성자에서 사라지고, 테스트에서 무엇을 대체해야 하는지 알 수 없게 됩니다.

01Front Controller 기본

모든 요청을 한 진입점으로 모으는 front controller 패턴의 핵심 아이디어 예제입니다.

PHP code

<?php
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);

echo match ($path) {
    '/' => 'home',
    '/health' => 'ok',
    default => '404',
};
알아두면 좋은 점

패턴 예제는 클래스 수보다 의존성 방향과 책임 분리를 중심으로 읽는 것이 더 중요합니다.

자주 하는 실수

패턴을 적용하더라도 구현체에 강하게 결합되면 오히려 변경 비용이 커집니다.

02Repository 패턴

저장소 접근 방식을 인터페이스 뒤로 숨기는 repository 패턴 예제입니다.

PHP code

<?php
interface UserRepository {
    public function findById(int $id): ?array;
}

class InMemoryUserRepository implements UserRepository {
    public function __construct(private array $rows) {}
    public function findById(int $id): ?array {
        foreach ($this->rows as $row) {
            if ($row['id'] === $id) return $row;
        }
        return null;
    }
}

var_dump((new InMemoryUserRepository([['id' => 1]]))->findById(1));
알아두면 좋은 점

패턴 예제는 클래스 수보다 의존성 방향과 책임 분리를 중심으로 읽는 것이 더 중요합니다.

자주 하는 실수

패턴을 적용하더라도 구현체에 강하게 결합되면 오히려 변경 비용이 커집니다.

03Service Container 간단 구현

의존 객체 생성 책임을 모으는 아주 작은 서비스 컨테이너 예제입니다.

PHP code

<?php
class Container {
    private array $factories = [];

    public function set(string $id, callable $factory): void { $this->factories[$id] = $factory; }
    public function get(string $id): mixed { return $this->factories[$id]($this); }
}

$container = new Container();
$container->set('config', fn () => ['env' => 'local']);
print_r($container->get('config'));
알아두면 좋은 점

패턴 예제는 클래스 수보다 의존성 방향과 책임 분리를 중심으로 읽는 것이 더 중요합니다.

자주 하는 실수

패턴을 적용하더라도 구현체에 강하게 결합되면 오히려 변경 비용이 커집니다.

04Middleware 파이프라인

HTTP 미들웨어가 서로 감싸며 요청을 흘려보내는 구조를 보여주는 예제입니다.

PHP code

<?php
$middlewares = [
    fn (array $request, callable $next) => $next($request + ['trace' => 'a']),
    fn (array $request, callable $next) => $next($request + ['auth' => true]),
];

$kernel = array_reduce(
    array_reverse($middlewares),
    fn (callable $next, callable $mw): callable => fn (array $request) => $mw($request, $next),
    fn (array $request): array => $request
);

print_r($kernel(['path' => '/']));
알아두면 좋은 점

패턴 예제는 클래스 수보다 의존성 방향과 책임 분리를 중심으로 읽는 것이 더 중요합니다.

자주 하는 실수

패턴을 적용하더라도 구현체에 강하게 결합되면 오히려 변경 비용이 커집니다.

05Strategy 패턴

알고리즘을 객체로 바꿔 런타임에 교체하는 strategy 패턴 예제입니다.

PHP code

<?php
interface DiscountStrategy {
    public function apply(int $price): int;
}

class TenPercent implements DiscountStrategy {
    public function apply(int $price): int { return (int) ($price * 0.9); }
}

echo (new TenPercent())->apply(10000);
알아두면 좋은 점

패턴 예제는 클래스 수보다 의존성 방향과 책임 분리를 중심으로 읽는 것이 더 중요합니다.

자주 하는 실수

패턴을 적용하더라도 구현체에 강하게 결합되면 오히려 변경 비용이 커집니다.

06Factory Method

타입 문자열에 따라 적절한 구현체를 생성하는 factory 패턴 예제입니다.

PHP code

<?php
interface Notifier {
    public function send(string $message): string;
}

class EmailNotifier implements Notifier {
    public function send(string $message): string { return 'mail:' . $message; }
}

class NotifierFactory {
    public static function make(string $type): Notifier {
        return match ($type) {
            'email' => new EmailNotifier(),
            default => throw new InvalidArgumentException('unsupported'),
        };
    }
}

echo NotifierFactory::make('email')->send('hi');
알아두면 좋은 점

패턴 예제는 클래스 수보다 의존성 방향과 책임 분리를 중심으로 읽는 것이 더 중요합니다.

자주 하는 실수

패턴을 적용하더라도 구현체에 강하게 결합되면 오히려 변경 비용이 커집니다.

07Builder 패턴

복잡한 객체를 단계적으로 조립하는 builder 패턴 예제입니다.

PHP code

<?php
class QueryBuilder {
    private array $parts = [];

    public function select(string $table): self { $this->parts[] = 'SELECT * FROM ' . $table; return $this; }
    public function where(string $condition): self { $this->parts[] = 'WHERE ' . $condition; return $this; }
    public function toSql(): string { return implode(' ', $this->parts); }
}

echo (new QueryBuilder())->select('users')->where('active = 1')->toSql();
알아두면 좋은 점

패턴 예제는 클래스 수보다 의존성 방향과 책임 분리를 중심으로 읽는 것이 더 중요합니다.

자주 하는 실수

패턴을 적용하더라도 구현체에 강하게 결합되면 오히려 변경 비용이 커집니다.

08Observer 패턴

이벤트 발행과 구독을 분리하는 observer 패턴 예제입니다.

PHP code

<?php
class EventBus {
    private array $listeners = [];

    public function on(string $event, callable $listener): void { $this->listeners[$event][] = $listener; }
    public function emit(string $event, mixed $payload): void {
        foreach ($this->listeners[$event] ?? [] as $listener) {
            $listener($payload);
        }
    }
}

$bus = new EventBus();
$bus->on('user.created', fn (array $user) => print($user['name']));
$bus->emit('user.created', ['name' => 'mida']);
알아두면 좋은 점

패턴 예제는 클래스 수보다 의존성 방향과 책임 분리를 중심으로 읽는 것이 더 중요합니다.

자주 하는 실수

패턴을 적용하더라도 구현체에 강하게 결합되면 오히려 변경 비용이 커집니다.

09Command 패턴

실행할 작업을 객체로 캡슐화하는 command 패턴의 가장 작은 예제입니다.

PHP code

<?php
interface Command {
    public function handle(): string;
}

class SendMailCommand implements Command {
    public function handle(): string { return 'sent'; }
}

echo (new SendMailCommand())->handle();
알아두면 좋은 점

패턴 예제는 클래스 수보다 의존성 방향과 책임 분리를 중심으로 읽는 것이 더 중요합니다.

자주 하는 실수

패턴을 적용하더라도 구현체에 강하게 결합되면 오히려 변경 비용이 커집니다.

10DTO 매퍼

외부 데이터 구조를 내부 DTO로 변환하는 매퍼 패턴 예제입니다.

PHP code

<?php
readonly class UserDto {
    public function __construct(public int $id, public string $name) {}
}

function mapUser(array $row): UserDto {
    return new UserDto($row['id'], $row['name']);
}

print_r(mapUser(['id' => 1, 'name' => 'Kim']));
알아두면 좋은 점

패턴 예제는 클래스 수보다 의존성 방향과 책임 분리를 중심으로 읽는 것이 더 중요합니다.

자주 하는 실수

패턴을 적용하더라도 구현체에 강하게 결합되면 오히려 변경 비용이 커집니다.

11CQRS 식 명령/조회 분리

쓰기와 읽기 요청 객체를 분리해 책임을 구분하는 CQRS 감각의 예제입니다.

PHP code

<?php
class CreatePost {
    public function __construct(public string $title) {}
}

class FindPostById {
    public function __construct(public int $id) {}
}

echo CreatePost::class . ' / ' . FindPostById::class;
알아두면 좋은 점

패턴 예제는 클래스 수보다 의존성 방향과 책임 분리를 중심으로 읽는 것이 더 중요합니다.

자주 하는 실수

패턴을 적용하더라도 구현체에 강하게 결합되면 오히려 변경 비용이 커집니다.

12캐시 데코레이터

기존 구현을 감싸 추가 기능을 붙이는 decorator 패턴 예제입니다.

PHP code

<?php
interface ArticleGateway {
    public function latest(): array;
}

class CachedArticleGateway implements ArticleGateway {
    private ?array $cache = null;
    public function __construct(private ArticleGateway $inner) {}
    public function latest(): array {
        return $this->cache ??= $this->inner->latest();
    }
}

echo CachedArticleGateway::class;
알아두면 좋은 점

패턴 예제는 클래스 수보다 의존성 방향과 책임 분리를 중심으로 읽는 것이 더 중요합니다.

자주 하는 실수

패턴을 적용하더라도 구현체에 강하게 결합되면 오히려 변경 비용이 커집니다.

13Adapter 패턴

기존 라이브러리 인터페이스를 새 애플리케이션 계약에 맞추는 adapter 예제입니다.

PHP code

<?php
interface SmsSender {
    public function send(string $message): string;
}

class LegacySmsClient {
    public function push(string $body): string { return 'legacy:' . $body; }
}

class SmsAdapter implements SmsSender {
    public function __construct(private LegacySmsClient $client) {}
    public function send(string $message): string { return $this->client->push($message); }
}

echo (new SmsAdapter(new LegacySmsClient()))->send('hello');
알아두면 좋은 점

패턴 예제는 클래스 수보다 의존성 방향과 책임 분리를 중심으로 읽는 것이 더 중요합니다.

자주 하는 실수

패턴을 적용하더라도 구현체에 강하게 결합되면 오히려 변경 비용이 커집니다.

14간단한 템플릿 렌더러

문자열 기반 템플릿 렌더링의 핵심 개념을 가장 작게 표현한 예제입니다.

PHP code

<?php
function render(string $template, array $data): string {
    foreach ($data as $key => $value) {
        $template = str_replace('{{' . $key . '}}', (string) $value, $template);
    }
    return $template;
}

echo render('hello {{name}}', ['name' => 'php']);
알아두면 좋은 점

패턴 예제는 클래스 수보다 의존성 방향과 책임 분리를 중심으로 읽는 것이 더 중요합니다.

자주 하는 실수

패턴을 적용하더라도 구현체에 강하게 결합되면 오히려 변경 비용이 커집니다.

15PSR-4 자동 로딩 규칙

PSR-4 오토로딩이 실제로 어떤 규칙으로 동작하는지 감을 잡는 예제입니다.

PHP code

<?php
spl_autoload_register(function (string $class): void {
    $prefix = 'App\\';
    if (!str_starts_with($class, $prefix)) {
        return;
    }

    $relative = substr($class, strlen($prefix));
    $path = __DIR__ . '/src/' . str_replace('\\', '/', $relative) . '.php';
    if (is_file($path)) {
        require $path;
    }
});
알아두면 좋은 점

패턴 예제는 클래스 수보다 의존성 방향과 책임 분리를 중심으로 읽는 것이 더 중요합니다.

자주 하는 실수

패턴을 적용하더라도 구현체에 강하게 결합되면 오히려 변경 비용이 커집니다.

16구성 객체로 설정 묶기

흩어진 설정 값을 하나의 명시적인 객체로 모으는 패턴 예제입니다.

PHP code

<?php
readonly class AppConfig {
    public function __construct(
        public string $env,
        public bool $debug
    ) {}
}

$config = new AppConfig('local', true);
echo $config->env;
알아두면 좋은 점

패턴 예제는 클래스 수보다 의존성 방향과 책임 분리를 중심으로 읽는 것이 더 중요합니다.

자주 하는 실수

패턴을 적용하더라도 구현체에 강하게 결합되면 오히려 변경 비용이 커집니다.

17match 기반 라우터

작은 애플리케이션에서 match 조합으로 라우팅을 표현하는 예제입니다.

PHP code

<?php
function route(string $method, string $path): string {
    return match ([$method, $path]) {
        ['GET', '/users'] => 'list users',
        ['POST', '/users'] => 'create user',
        default => '404',
    };
}

echo route('GET', '/users');
알아두면 좋은 점

패턴 예제는 클래스 수보다 의존성 방향과 책임 분리를 중심으로 읽는 것이 더 중요합니다.

자주 하는 실수

패턴을 적용하더라도 구현체에 강하게 결합되면 오히려 변경 비용이 커집니다.

18Hexagonal Port 인터페이스

외부 시스템 의존성을 포트 인터페이스 뒤로 숨기는 헥사고날 아키텍처 감각의 예제입니다.

PHP code

<?php
interface PaymentPort {
    public function charge(int $amount): string;
}

class CheckoutService {
    public function __construct(private PaymentPort $payment) {}
    public function checkout(int $amount): string { return $this->payment->charge($amount); }
}

echo CheckoutService::class;
알아두면 좋은 점

패턴 예제는 클래스 수보다 의존성 방향과 책임 분리를 중심으로 읽는 것이 더 중요합니다.

자주 하는 실수

패턴을 적용하더라도 구현체에 강하게 결합되면 오히려 변경 비용이 커집니다.

정리하며

  • 횡단 관심사는 컨트롤러에 넣지 말고 미들웨어 한 겹으로 분리해 순서를 통제합니다.
  • 컨테이너는 진입점에서만 해석하고 클래스에는 완성된 의존성만 주입합니다.
  • 네임스페이스와 디렉터리 대소문자를 정확히 맞춰 리눅스 배포 시 로딩 실패를 막습니다.
  • 캐싱이나 로깅은 기존 구현을 수정하는 대신 같은 인터페이스의 데코레이터로 덧씌웁니다.

더 깊이 들어가고 싶다면 PHP 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.