기존 클래스의 필드/메소드를 물려받아 새 클래스를 만드는 기법입니다.
공통 로직을 재사용하고, 하위 클래스가 동작을 확장/변경(오버라이드) 하도록 합니다.
문법: class Child extends Parent { ... }
<?php
declare(strict_types=1);
class Animal {
public function speak(): string { return '...'; }
}
class Dog extends Animal {
public function speak(): string { return 'Woof'; } // 오버라이드
}
echo (new Dog())->speak(); // "Woof"
자식이 생성자를 정의하면 부모 생성자는 자동 호출되지 않습니다. 필요 시 명시 호출.
class DbConn {
public function __construct(protected PDO $pdo) {}
}
class UserRepo extends DbConn {
public function __construct(PDO $pdo, private string $table = 'users') {
parent::__construct($pdo); // 명시 호출
}
}
자식은 부모 메소드를 같은 이름으로 재정의할 수 있습니다.
가시성은 좁힐 수 없습니다.
부모 public → 자식도 public만 가능
부모 protected → 자식은 protected 또는 public
final 메소드는 오버라이드 불가, final class는 상속 자체가 불가.
class Base {
public function run(): void {}
final public function id(): string { return 'fixed'; }
}
class Child extends Base {
// public function id(): string {} // 오류: final 오버라이드 불가
public function run(): void { /* 확장 */ }
}
추상 클래스(abstract) 는 직접 인스턴스화 불가, 추상 메소드를 자식이 반드시 구현.
abstract class Notifier {
abstract public function send(string $msg): void;
}
class EmailNotifier extends Notifier {
public function send(string $msg): void { /* 이메일 전송 */ }
}
PHP는 반환 타입 공변(covariant), 매개변수 타입 반공변(contravariant) 을 지원합니다.
자식 메소드의 반환 타입을 더 구체적으로 할 수 있음.
자식 메소드의 매개변수 타입을 더 넓게 받을 수 있음.
class Animal {}
class Dog extends Animal {}
class Shelter {
public function adopt(): Animal { return new Animal(); }
public function register(Animal $a): void {}
}
class DogShelter extends Shelter {
// 반환 타입 공변: Animal → Dog (더 구체적)
public function adopt(): Dog { return new Dog(); }
// 매개변수 반공변: Animal보다 넓힐 수 있지만 이미 최상위라 동일 유지
public function register(Animal $a): void {}
}
abstract class Shape {
abstract public function area(): float;
}
class Rectangle extends Shape {
public function __construct(private float $w, private float $h) {}
public function area(): float { return $this->w * $this->h; }
}
class Circle extends Shape {
public function __construct(private float $r) {}
public function area(): float { return M_PI * $this->r * $this->r; }
}
/** @param list<Shape> $shapes */
function totalArea(array $shapes): float {
$sum = 0.0;
foreach ($shapes as $s) $sum += $s->area(); // 같은 메시지, 다른 동작
return $sum;
}
echo totalArea([new Rectangle(3,4), new Circle(2)]); // 다형성
self:: 는 정의된 클래스 기준 고정.
static:: 은 호출한 실제 클래스 기준(늦은 정적 바인딩). 상속 구조에서 팩토리 메소드에 유용.
parent:: 는 부모 구현을 호출.
class Base {
public static function make(): static { // 8.0+: static 반환 타입
return new static();
}
public function who(): string { return self::class; }
}
class Sub extends Base {
public function who(): string { return static::class; }
}
echo get_class(Base::make()); // Base
echo get_class(Sub::make()); // Sub
자식은 부모와 동일 이름의 상수를 재정의할 수 있습니다.
PHP 8.1+에서는 final 클래스 상수를 선언해 재정의를 막을 수 있습니다.
class A {
public const T = 'base';
final public const VERSION = '1.0'; // 8.1+
}
class B extends A {
public const T = 'child'; // 허용: 재정의
// public const VERSION = 'X'; // 오류: final 상수
}
부모의 프로퍼티는 그대로 상속됩니다.
같은 이름의 프로퍼티를 자식에서 다시 선언하는 것은 혼동을 부르고 타입/가시성 호환성 문제가 날 수 있으니 피하세요.
공통 상태는 부모에 두고, 동작만 자식에서 확장하는 편이 안전합니다.
public 어디서나 접근
protected 자신/자식에서만
private 해당 클래스 내부만
오버라이드 시 가시성 축소 금지 규칙을 꼭 지키세요.
트레이트: 코드 조각 재사용(다중 상속 대체 수단). 상속과는 별개. 충돌 시 insteadof, as로 조정 가능.
인터페이스: “이 메소드들을 제공한다”는 계약. 상속 대신 다형성의 표준화에 사용.
실제로는 “추상 클래스(공통 구현) + 인터페이스(계약)” 조합이 실무에서 가장 유연합니다.
상속이 어울리는 경우
“is-a” 관계가 명확하고, 상위 타입의 계약이 안정적일 때
공통 동작을 템플릿 메소드 패턴으로 공유하고 일부만 바꾸고 싶을 때
조합(컴포지션) 이 나은 경우
기능을 교체/확장해야 할 가능성이 클 때 (런타임에 전략 교체 등)
상위 타입 변경이 잦아 하위 타입들이 깨질 위험이 있을 때
단일 책임을 유지하고 싶을 때
// 상속 대신 전략(Strategy) 조합 예시
interface PriceRule { public function apply(int $price): int; }
final class TenPercentOff implements PriceRule {
public function apply(int $price): int { return (int)round($price * 0.9); }
}
final class Cart {
public function __construct(private PriceRule $rule) {}
public function checkout(int $price): int { return $this->rule->apply($price); }
}
생성자에서 오버라이드 가능한 메소드 호출
자식 초기화 이전에 자식 메소드가 실행돼 버그 유발.
너무 깊은 상속 트리
변경 비용 급증. 2~3단계 이내 권장.
부모의 private 멤버를 자식에서 “덮어쓴다”는 오해
부모·자식 각각 별개 멤버입니다. 혼동 방지를 위해 동일 이름 재선언 지양.
공유 상태를 static으로 남용
테스트·동시성 문제. 필요한 곳에서만 신중하게 사용.
상속은 공통 계약 + 확장 지점을 명확히 설계했을 때만 사용.
오버라이드 시 가시성 축소 금지, 타입 호환성(반환 공변/매개변수 반공변) 준수.
생성자 체인은 parent::__construct() 를 명시적으로 연결.
공통 로직은 추상 클래스/트레이트, 계약은 인터페이스로 분리.
가능하면 조합(컴포지션)으로 유연성 확보.