IMG-LOGO
공지사항 :

PHP 상속(inheritance)

lmkfox - 2025-08-29 07:02:39 36 Views 0 Comment

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() 를 명시적으로 연결.

  • 공통 로직은 추상 클래스/트레이트, 계약은 인터페이스로 분리.

  • 가능하면 조합(컴포지션)으로 유연성 확보.


댓글