객체 상속
객체 상속 (Object Inheritance)
객체 상속
클래스 하나가 다른 클래스를 물려받는 일, 우리는 그걸 상속(inheritance)이라고 불러요. 상속은 아주 오래된 프로그래밍 원칙인데, PHP의 객체 모델도 이 원칙을 그대로 쓰고 있죠. 이 원칙이 많은 클래스와 객체가 서로 맺는 관계에 영향을 줍니다.
예를 들어 클래스를 확장(extend)하면, 자식 클래스는 부모 클래스의 모든 public·protected 메서드, 프로퍼티, 그리고 상수까지 전부 물려받아요. 해당 메서드를 자식 클래스에서 오버라이드하지 않는 한, 원래 기능 그대로 동작합니다.
이런 특징은 기능을 정의하고 추상화하는 데 유용해요. 비슷한 객체들에 추가 기능을 붙일 때 굳이 공통 기능을 전부 다시 구현하지 않아도 되니까요.
부모 클래스의 private 메서드는 자식 클래스에서 접근할 수 없어요. 그래서 자식 클래스는 일반적인 상속 규칙과 상관없이 private 메서드를 스스로 다시 구현할 수 있죠. 다만 PHP 8.0.0 이전에는 private 메서드에도 final·static 제한이 적용됐었어요. PHP 8.0.0부터는 private 메서드에 강제되는 제한이 private final 생성자 하나뿐입니다. 생성자를 "끄는" 흔한 방법이 static 팩토리 메서드라서요.
메서드·프로퍼티·상수의 가시성(visibility)은 넓히는 쪽으로는 바꿀 수 있어요. 예를 들어 protected 메서드를 public으로 표시할 수 있죠. 하지만 반대로 좁히는 건 안 돼요. public 프로퍼티를 private으로 바꾸는 건 허용되지 않습니다. 예외가 하나 있는데, 바로 생성자예요. 생성자는 가시성을 좁힐 수 있습니다. 자식 클래스에서 public 생성자를 private으로 바꿔도 돼요.
주의:
오토로딩(autoloading)을 쓰지 않는다면, 클래스는 사용하기 전에 반드시 정의돼 있어야 해요. 어떤 클래스가 다른 클래스를 상속한다면, 자식 클래스 구조보다 부모 클래스가 먼저 선언돼야 합니다. 이 규칙은 다른 클래스나 인터페이스를 상속하는 클래스 전부에 적용돼요.
주의:
읽기-쓰기(read-write) 프로퍼티를 readonly 프로퍼티로 오버라이드하거나, 그 반대는 허용되지 않아요.
<?php
class A {
public int $prop;
}
class B extends A {
// Illegal: read-write -> readonly
public readonly int $prop;
}
?>
상속 예제 (Inheritance Example)
예제 #1로 상속이 실제로 어떻게 동작하는지 볼게요. Bar가 Foo를 상속하면, printItem()은 오버라이드한 자신의 버전을 쓰고, 오버라이드하지 않은 printPHP()는 부모의 것을 그대로 씁니다.
Example #1 상속 예제
<?php
class Foo
{
public function printItem($string)
{
echo 'Foo: ' . $string . PHP_EOL;
}
public function printPHP()
{
echo 'PHP is great.' . PHP_EOL;
}
}
class Bar extends Foo
{
public function printItem($string)
{
echo 'Bar: ' . $string . PHP_EOL;
}
}
$foo = new Foo();
$bar = new Bar();
$foo->printItem('baz'); // Output: 'Foo: baz'
$foo->printPHP(); // Output: 'PHP is great.'
$bar->printItem('baz'); // Output: 'Bar: baz'
$bar->printPHP(); // Output: 'PHP is great.'
?>
$bar->printItem('baz')가 'Bar: baz'를 출력하는 걸 보면 Bar가 printItem()을 자기 것으로 덮어썼고, printPHP()는 물려받은 그대로 쓰는 걸 알 수 있어요.
내장 클래스와의 반환 타입 호환성 (Return Type Compatibility with Internal Classes)
PHP 8.1 이전에는 대부분의 내장 클래스나 메서드가 반환 타입을 선언하지 않았어요. 그래서 그걸 상속할 때 어떤 반환 타입이든 허용됐습니다.
PHP 8.1.0부터는 대부분의 내장 메서드가 반환 타입을 "잠정적으로(tentatively)" 선언하기 시작했어요. 이 경우 오버라이드한 메서드의 반환 타입은 상속 대상인 부모와 호환돼야 합니다. 그렇지 않으면 deprecated 경고가 발생하죠. 명시적인 반환 선언이 없는 것도 시그니처 불일치로 간주돼서, 마찬가지로 deprecated 경고가 나온다는 점을 기억해 둘게요.
PHP 버전 간 호환성 문제 때문에 오버라이드 메서드에 반환 타입을 선언할 수 없다면, ReturnTypeWillChange 속성(attribute)을 추가해서 deprecated 경고를 조용히 만들 수 있어요.
Example #2 반환 타입을 선언하지 않은 오버라이드 메서드
<?php
class MyDateTime extends DateTime
{
public function modify(string $modifier) { return false; }
}
// "Deprecated: Return type of MyDateTime::modify(string $modifier) should either be compatible with DateTime::modify(string $modifier): DateTime|false, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice" as of PHP 8.1.0
?>
Example #3 잘못된 반환 타입을 선언한 오버라이드 메서드
<?php
class MyDateTime extends DateTime
{
public function modify(string $modifier): ?DateTime { return null; }
}
// "Deprecated: Return type of MyDateTime::modify(string $modifier): ?DateTime should either be compatible with DateTime::modify(string $modifier): DateTime|false, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice" as of PHP 8.1.0
?>
Example #4 deprecated 경고 없이 잘못된 반환 타입을 선언한 오버라이드 메서드
<?php
class MyDateTime extends DateTime
{
/**
* @return DateTime|false
*/
#[\ReturnTypeWillChange]
public function modify(string $modifier) { return false; }
}
// No notice is triggered
?>