PHpullh
심층 가이드/PHP/객체지향

PHP · 심층 가이드

PHP 객체지향 완전 정리

생성자 프로퍼티 승격과 값 객체를 축으로 PHP의 클래스, 인터페이스, Trait, 매직 메서드를 정리해 프레임워크 코드가 왜 그런 모양인지 이해하게 만듭니다.

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

PHP의 객체 모델은 단일 상속에 인터페이스로 계약을 넓히고, 구현 공유는 trait으로 처리하는 구조입니다. 여기에 __get, __call, __invoke 같은 매직 메서드가 얹히면서 런타임에 동작을 바꿀 여지가 굉장히 넓어졌습니다. 초심자가 자주 흔들리는 지점이 이 자유도입니다. 상속으로 풀 문제, 인터페이스로 풀 문제, Trait으로 풀 문제, 매직 메서드로 풀 문제의 경계가 흐릿해 보이기 때문입니다. 실무 기준은 단순합니다. 타입이 필요하면 인터페이스, 코드 조각만 필요하면 Trait입니다.

생성자 프로퍼티 승격을 맨 앞에 둔 이유는 이 문법 하나로 클래스 골격이 절반으로 줄어들기 때문입니다. 이걸 익힌 상태에서 값 객체readonly 클래스를 보면 불변 객체를 만드는 비용이 거의 없다는 게 보입니다. 정적 팩토리 메서드는 생성자에 이름을 붙이는 기법으로 값 객체와 짝을 이룹니다. 뒤쪽의 IteratorAggregate 구현, JsonSerializable 구현, ArrayAccess 구현은 내 객체를 foreach, json_encode, 대괄호 접근이라는 기본 문법에 편입시키는 묶음입니다.

복제 동작은 반드시 확인하고 넘어가야 합니다. clone얕은 복사라 프로퍼티에 담긴 객체는 원본과 그대로 공유되고, 복제본의 내부 컬렉션을 수정하면 원본까지 바뀝니다. 깊은 복사가 필요하면 객체 복제와 __clone에서 직접 내부 객체를 다시 만들어 줘야 합니다. Trait도 마찬가지로, 같은 이름의 메서드를 가진 Trait 두 개를 함께 쓰면 치명적 오류가 나므로 insteadofas로 충돌을 명시해야 합니다. __get 과 __set 주의 깊게 쓰기는 매직 메서드가 정적 분석과 IDE 자동완성을 무력화한다는 대가를 함께 설명합니다.

01생성자 프로퍼티 승격

생성자 프로퍼티 승격으로 클래스 선언을 짧게 만드는 예제입니다.

PHP code

<?php
class Article {
    public function __construct(
        public string $title,
        public string $author
    ) {}
}

$article = new Article('PHP 8', 'mida');
echo $article->title;
알아두면 좋은 점

클래스 예제는 생성자, 가시성, 협력 객체의 책임을 함께 보면 실무 감각이 빨리 붙습니다.

자주 하는 실수

가시성, final, readonly 제약을 무시하면 객체 설계가 금방 흐트러집니다.

02상속 기본

기본 상속과 메서드 오버라이드를 가장 작은 예제로 보여줍니다.

PHP code

<?php
class Animal {
    public function speak(): string {
        return '...';
    }
}

class Dog extends Animal {
    public function speak(): string {
        return 'woof';
    }
}

echo (new Dog())->speak();
알아두면 좋은 점

클래스 예제는 생성자, 가시성, 협력 객체의 책임을 함께 보면 실무 감각이 빨리 붙습니다.

자주 하는 실수

가시성, final, readonly 제약을 무시하면 객체 설계가 금방 흐트러집니다.

03추상 클래스

공통 규약은 두고 구현은 하위 클래스에 위임하는 추상 클래스 예제입니다.

PHP code

<?php
abstract class Payment {
    abstract public function pay(int $amount): string;
}

class CardPayment extends Payment {
    public function pay(int $amount): string {
        return "paid {$amount}";
    }
}

echo (new CardPayment())->pay(5000);
알아두면 좋은 점

클래스 예제는 생성자, 가시성, 협력 객체의 책임을 함께 보면 실무 감각이 빨리 붙습니다.

자주 하는 실수

가시성, final, readonly 제약을 무시하면 객체 설계가 금방 흐트러집니다.

04인터페이스 구현

인터페이스를 기준으로 구현체를 교체할 수 있게 만드는 기본 예제입니다.

PHP code

<?php
interface Logger {
    public function info(string $message): void;
}

class EchoLogger implements Logger {
    public function info(string $message): void {
        echo $message;
    }
}

(new EchoLogger())->info('boot');
알아두면 좋은 점

클래스 예제는 생성자, 가시성, 협력 객체의 책임을 함께 보면 실무 감각이 빨리 붙습니다.

자주 하는 실수

가시성, final, readonly 제약을 무시하면 객체 설계가 금방 흐트러집니다.

05Trait 재사용

Trait를 통해 중복 메서드를 여러 클래스에 재사용하는 예제입니다.

PHP code

<?php
trait Timestampable {
    public function now(): string {
        return date('c');
    }
}

class Post {
    use Timestampable;
}

echo (new Post())->now();
알아두면 좋은 점

클래스 예제는 생성자, 가시성, 협력 객체의 책임을 함께 보면 실무 감각이 빨리 붙습니다.

자주 하는 실수

가시성, final, readonly 제약을 무시하면 객체 설계가 금방 흐트러집니다.

06enum 에 메서드 넣기

enum 안에 동작을 담아 값과 규칙을 함께 모델링하는 예제입니다.

PHP code

<?php
enum Plan: string {
    case Free = 'free';
    case Pro = 'pro';

    public function limit(): int {
        return $this === self::Free ? 3 : 100;
    }
}

echo Plan::Pro->limit();
알아두면 좋은 점

클래스 예제는 생성자, 가시성, 협력 객체의 책임을 함께 보면 실무 감각이 빨리 붙습니다.

자주 하는 실수

가시성, final, readonly 제약을 무시하면 객체 설계가 금방 흐트러집니다.

07readonly 클래스

값 객체 전체를 immutable하게 만들고 싶을 때 readonly class를 사용하는 예제입니다.

PHP code

<?php
readonly class Money {
    public function __construct(
        public int $amount,
        public string $currency
    ) {}
}

$money = new Money(1000, 'KRW');
echo $money->currency;
알아두면 좋은 점

클래스 예제는 생성자, 가시성, 협력 객체의 책임을 함께 보면 실무 감각이 빨리 붙습니다.

자주 하는 실수

가시성, final, readonly 제약을 무시하면 객체 설계가 금방 흐트러집니다.

08toString 매직 메서드

객체를 문자열 문맥에서 자연스럽게 표현하는 __toString 예제입니다.

PHP code

<?php
class Version {
    public function __construct(private int $major, private int $minor) {}

    public function __toString(): string {
        return $this->major . '.' . $this->minor;
    }
}

echo new Version(8, 3);
알아두면 좋은 점

클래스 예제는 생성자, 가시성, 협력 객체의 책임을 함께 보면 실무 감각이 빨리 붙습니다.

자주 하는 실수

가시성, final, readonly 제약을 무시하면 객체 설계가 금방 흐트러집니다.

09생성자 주입

의존 객체를 생성자에서 주입받는 가장 기본적인 DI 예제입니다.

PHP code

<?php
interface Clock {
    public function now(): string;
}

class SystemClock implements Clock {
    public function now(): string {
        return date('c');
    }
}

class ReportService {
    public function __construct(private Clock $clock) {}

    public function generate(): string {
        return 'generated at ' . $this->clock->now();
    }
}

echo (new ReportService(new SystemClock()))->generate();
알아두면 좋은 점

클래스 예제는 생성자, 가시성, 협력 객체의 책임을 함께 보면 실무 감각이 빨리 붙습니다.

자주 하는 실수

가시성, final, readonly 제약을 무시하면 객체 설계가 금방 흐트러집니다.

10값 객체

유효성 규칙을 객체 생성 시점에 묶어 두는 값 객체 예제입니다.

PHP code

<?php
readonly class Email {
    public function __construct(public string $value) {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('invalid email');
        }
    }
}

echo (new Email('team@example.com'))->value;
알아두면 좋은 점

클래스 예제는 생성자, 가시성, 협력 객체의 책임을 함께 보면 실무 감각이 빨리 붙습니다.

자주 하는 실수

가시성, final, readonly 제약을 무시하면 객체 설계가 금방 흐트러집니다.

11정적 팩토리 메서드

객체 생성을 명명된 팩토리 메서드로 감추는 예제입니다.

PHP code

<?php
class Token {
    private function __construct(public string $value) {}

    public static function issue(): self {
        return new self(bin2hex(random_bytes(8)));
    }
}

echo Token::issue()->value;
알아두면 좋은 점

클래스 예제는 생성자, 가시성, 협력 객체의 책임을 함께 보면 실무 감각이 빨리 붙습니다.

자주 하는 실수

가시성, final, readonly 제약을 무시하면 객체 설계가 금방 흐트러집니다.

12private 생성자 싱글톤 스타일

완전한 싱글톤을 권장한다기보다 private 생성자와 지연 초기화 패턴을 이해하기 위한 예제입니다.

PHP code

<?php
class Config {
    private static ?self $instance = null;
    private function __construct(public array $values) {}

    public static function instance(): self {
        return self::$instance ??= new self(['env' => 'local']);
    }
}

echo Config::instance()->values['env'];
알아두면 좋은 점

클래스 예제는 생성자, 가시성, 협력 객체의 책임을 함께 보면 실무 감각이 빨리 붙습니다.

자주 하는 실수

가시성, final, readonly 제약을 무시하면 객체 설계가 금방 흐트러집니다.

13익명 클래스 어댑터

짧은 어댑터나 테스트 더블을 익명 클래스로 만드는 예제입니다.

PHP code

<?php
interface Cache {
    public function get(string $key): mixed;
}

$cache = new class implements Cache {
    public function get(string $key): mixed {
        return 'mock:' . $key;
    }
};

echo $cache->get('user:1');
알아두면 좋은 점

클래스 예제는 생성자, 가시성, 협력 객체의 책임을 함께 보면 실무 감각이 빨리 붙습니다.

자주 하는 실수

가시성, final, readonly 제약을 무시하면 객체 설계가 금방 흐트러집니다.

14final 클래스와 메서드

상속 확장을 막아야 하는 핵심 객체에 final을 적용하는 예제입니다.

PHP code

<?php
final class AppKernel {
    final public function boot(): string {
        return 'booted';
    }
}

echo (new AppKernel())->boot();
알아두면 좋은 점

클래스 예제는 생성자, 가시성, 협력 객체의 책임을 함께 보면 실무 감각이 빨리 붙습니다.

자주 하는 실수

가시성, final, readonly 제약을 무시하면 객체 설계가 금방 흐트러집니다.

15IteratorAggregate 구현

커스텀 컬렉션 객체를 foreach에 자연스럽게 연결하는 예제입니다.

PHP code

<?php
class TagCollection implements IteratorAggregate {
    public function __construct(private array $tags) {}

    public function getIterator(): Traversable {
        yield from $this->tags;
    }
}

foreach (new TagCollection(['php', 'api']) as $tag) {
    echo $tag . PHP_EOL;
}
알아두면 좋은 점

클래스 예제는 생성자, 가시성, 협력 객체의 책임을 함께 보면 실무 감각이 빨리 붙습니다.

자주 하는 실수

가시성, final, readonly 제약을 무시하면 객체 설계가 금방 흐트러집니다.

16JsonSerializable 구현

객체가 JSON으로 직렬화될 모양을 스스로 제어하는 예제입니다.

PHP code

<?php
class UserResource implements JsonSerializable {
    public function __construct(private int $id, private string $name) {}

    public function jsonSerialize(): array {
        return ['id' => $this->id, 'name' => $this->name];
    }
}

echo json_encode(new UserResource(1, 'mida'));
알아두면 좋은 점

클래스 예제는 생성자, 가시성, 협력 객체의 책임을 함께 보면 실무 감각이 빨리 붙습니다.

자주 하는 실수

가시성, final, readonly 제약을 무시하면 객체 설계가 금방 흐트러집니다.

17ArrayAccess 구현

객체를 배열처럼 다루고 싶을 때 ArrayAccess를 구현하는 예제입니다.

PHP code

<?php
class Bag implements ArrayAccess {
    private array $items = [];

    public function offsetExists(mixed $offset): bool { return isset($this->items[$offset]); }
    public function offsetGet(mixed $offset): mixed { return $this->items[$offset] ?? null; }
    public function offsetSet(mixed $offset, mixed $value): void { $this->items[$offset] = $value; }
    public function offsetUnset(mixed $offset): void { unset($this->items[$offset]); }
}

$bag = new Bag();
$bag['lang'] = 'php';
echo $bag['lang'];
알아두면 좋은 점

클래스 예제는 생성자, 가시성, 협력 객체의 책임을 함께 보면 실무 감각이 빨리 붙습니다.

자주 하는 실수

가시성, final, readonly 제약을 무시하면 객체 설계가 금방 흐트러집니다.

18__invoke 호출 객체

객체를 함수처럼 호출할 수 있게 만드는 __invoke 예제입니다.

PHP code

<?php
class Slugify {
    public function __invoke(string $value): string {
        return strtolower(str_replace(' ', '-', trim($value)));
    }
}

$slugify = new Slugify();
echo $slugify('Hello PHP');
알아두면 좋은 점

클래스 예제는 생성자, 가시성, 협력 객체의 책임을 함께 보면 실무 감각이 빨리 붙습니다.

자주 하는 실수

가시성, final, readonly 제약을 무시하면 객체 설계가 금방 흐트러집니다.

19__get 과 __set 주의 깊게 쓰기

동적 프로퍼티를 흉내 내는 매직 메서드 예제이며, 남용 시 가독성이 크게 떨어진다는 점도 함께 보여줍니다.

PHP code

<?php
class MetaBag {
    private array $values = [];

    public function __set(string $name, mixed $value): void {
        $this->values[$name] = $value;
    }

    public function __get(string $name): mixed {
        return $this->values[$name] ?? null;
    }
}

$bag = new MetaBag();
$bag->framework = 'Laravel';
echo $bag->framework;
알아두면 좋은 점

클래스 예제는 생성자, 가시성, 협력 객체의 책임을 함께 보면 실무 감각이 빨리 붙습니다.

자주 하는 실수

__get과 __set은 편하지만 IDE 지원과 정적 분석이 약해지므로 정말 필요한 경우에만 쓰는 편이 좋습니다.

20Attribute 기반 컨트롤러 메타데이터

클래스에 붙인 Attribute를 라우팅이나 미들웨어 설정에 활용하는 예제입니다.

PHP code

<?php
#[Attribute]
class Middleware {
    public function __construct(public string $name) {}
}

#[Middleware('auth')]
class DashboardController {}

$ref = new ReflectionClass(DashboardController::class);
echo $ref->getAttributes(Middleware::class)[0]->newInstance()->name;
알아두면 좋은 점

클래스 예제는 생성자, 가시성, 협력 객체의 책임을 함께 보면 실무 감각이 빨리 붙습니다.

자주 하는 실수

가시성, final, readonly 제약을 무시하면 객체 설계가 금방 흐트러집니다.

21객체 복제와 __clone

기본 빌더를 복제해 일부 조건만 다르게 조합할 때 쓰는 clone 예제입니다.

PHP code

<?php
class QueryBuilder {
    public function __construct(public array $wheres = []) {}

    public function __clone(): void {
        $this->wheres = array_values($this->wheres);
    }
}

$base = new QueryBuilder(['active = 1']);
$copy = clone $base;
$copy->wheres[] = 'role = "admin"';

print_r([$base->wheres, $copy->wheres]);
알아두면 좋은 점

클래스 예제는 생성자, 가시성, 협력 객체의 책임을 함께 보면 실무 감각이 빨리 붙습니다.

자주 하는 실수

가시성, final, readonly 제약을 무시하면 객체 설계가 금방 흐트러집니다.

정리하며

  • 의존성은 생성자 프로퍼티 승격으로 선언해 필드 선언과 대입 중복을 없앱니다.
  • 타입 계약이 목적이면 인터페이스, 코드 재사용이 목적이면 Trait으로 나눕니다.
  • clone은 얕은 복사이므로 내부 객체가 있으면 __clone에서 직접 다시 만듭니다.
  • json_encode 결과를 통제하려면 배열 변환 대신 JsonSerializable을 구현합니다.

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