IT
PHP 상속(inheritance)
1) 상속이란 무엇인가
-
기존 클래스의 필드/메소드를 물려받아 새 클래스를 만드는 기법입니다.
-
공통 로직을 재사용하고, 하위 클래스가 동작을 확장/변경(오버라이드) 하도록 합니다.
-
문법: 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"
2) 생성자와
parent::__construct()
-
자식이 생성자를 정의하면 부모 생성자는 자동 호출되지 않습니다. 필요 시 명시 호출.
class DbConn {
public function __construct(protected PDO $pdo) {}
}
class UserRepo extends DbConn {
public function __construct(PDO $pdo, private string $table = 'users') {
parent::__construct($pdo); // 명시 호출
}
}
3) 메소드 오버라이딩과 가시성 규칙
-
자식은 부모 메소드를 같은 이름으로 재정의할 수 있습니다.
-
가시성은 좁힐 수 없습니다.
-
부모 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 { /* 확장 */ }
}
4) 추상 클래스와 추상 메소드
-
추상 클래스(abstract) 는 직접 인스턴스화 불가, 추상 메소드를 자식이 반드시 구현.
abstract class Notifier {
abstract public function send(string $msg): void;
}
class EmailNotifier extends Notifier {
public function send(string $msg): void { /* 이메일 전송 */ }
}
5) 타입 호환성(공변/반공변)
-
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 {}
}
6) 다형성(Polymorphism) 예제
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)]); // 다형성
7) 정적 멤버와 늦은 정적 바인딩(
self
vs
static
vs
parent
)
-
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
8) 클래스 상수 상속과
final
상수
-
자식은 부모와 동일 이름의 상수를 재정의할 수 있습니다.
-
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 상수
}
9) 프로퍼티 상속 주의
-
부모의 프로퍼티는 그대로 상속됩니다.
-
같은 이름의 프로퍼티를 자식에서 다시 선언하는 것은 혼동을 부르고 타입/가시성 호환성 문제가 날 수 있으니 피하세요.
-
공통 상태는 부모에 두고, 동작만 자식에서 확장하는 편이 안전합니다.
10) 접근 제어자 요약
-
public 어디서나 접근
-
protected 자신/자식에서만
-
private 해당 클래스 내부만
-
오버라이드 시 가시성 축소 금지 규칙을 꼭 지키세요.
11) 트레이트/인터페이스와의 관계
-
트레이트: 코드 조각 재사용(다중 상속 대체 수단). 상속과는 별개. 충돌 시 insteadof, as로 조정 가능.
-
인터페이스: “이 메소드들을 제공한다”는 계약. 상속 대신 다형성의 표준화에 사용.
실제로는 “추상 클래스(공통 구현) + 인터페이스(계약)” 조합이 실무에서 가장 유연합니다.
12) 언제 상속을 쓰고, 언제 조합(composition)을 쓸까
-
상속이 어울리는 경우
-
“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); }
}
13) 자주 하는 실수와 피해야 할 패턴
-
생성자에서 오버라이드 가능한 메소드 호출
자식 초기화 이전에 자식 메소드가 실행돼 버그 유발.
-
너무 깊은 상속 트리
변경 비용 급증. 2~3단계 이내 권장.
-
부모의 private 멤버를 자식에서 “덮어쓴다”는 오해
부모·자식 각각 별개 멤버입니다. 혼동 방지를 위해 동일 이름 재선언 지양.
-
공유 상태를 static으로 남용
테스트·동시성 문제. 필요한 곳에서만 신중하게 사용.
14) 체크리스트(요약)
-
상속은 공통 계약 + 확장 지점을 명확히 설계했을 때만 사용.
-
오버라이드 시 가시성 축소 금지, 타입 호환성(반환 공변/매개변수 반공변) 준수.
-
생성자 체인은 parent::__construct() 를 명시적으로 연결.
-
공통 로직은 추상 클래스/트레이트, 계약은 인터페이스로 분리.
-
가능하면 조합(컴포지션)으로 유연성 확보.
댓글
아직 댓글이 없습니다.
로그인 후 댓글을 남길 수 있습니다.