생성자와 소멸자
생성자와 소멸자 (Constructors and Destructors)
생성자 (Constructor)
function __construct(mixed ...$values = ""): void
PHP에서는 클래스에 생성자 메서드를 선언할 수 있어요. 생성자 메서드를 가진 클래스는 새로 만들어진 객체마다 이 메서드를 호출하죠. 그래서 객체를 사용하기 전에 필요한 초기화 작업을 하는 자리에 딱 맞아요.
참고:
자식 클래스가 생성자를 정의하면 부모 생성자는 자동으로 호출되지 않아요. 부모 생성자를 실행하려면 자식 생성자 안에서
parent::__construct()를 명시적으로 호출해야 해요. 자식이 생성자를 정의하지 않았다면, 평범한 클래스 메서드처럼 부모의 생성자를 상속받을 수 있어요(private으로 선언되지 않았다면요).
예제 #1 상속에서의 생성자
<?php
class BaseClass {
function __construct() {
print "In BaseClass constructor\n";
}
}
class SubClass extends BaseClass {
function __construct() {
parent::__construct();
print "In SubClass constructor\n";
}
}
class OtherSubClass extends BaseClass {
// inherits BaseClass's constructor
}
// In BaseClass constructor
$obj = new BaseClass();
// In BaseClass constructor
// In SubClass constructor
$obj = new SubClass();
// In BaseClass constructor
$obj = new OtherSubClass();
?>
평범한 메서드들과 달리 __construct()는 상속될 때 보통의 시그니처 호환 규칙에서 예외로 취급돼요.
생성자는 해당 객체를 인스턴스화하는 과정에서 호출되는 평범한 메서드예요. 그래서 필요한 만큼 얼마든지 인자를 받을 수 있는데, 인자는 필수일 수도, 타입이 지정될 수도, 기본값을 가질 수도 있어요. 생성자 인자는 클래스 이름 뒤 괄호 안에 인자를 넣는 방식으로 전달돼요.
예제 #2 생성자 인자 사용하기
<?php
class Point {
protected int $x;
protected int $y;
public function __construct(int $x, int $y = 0) {
$this->x = $x;
$this->y = $y;
}
}
// Pass both parameters.
$p1 = new Point(4, 5);
// Pass only the required parameter. $y will take its default value of 0.
$p2 = new Point(4);
// With named parameters (as of PHP 8.0):
$p3 = new Point(y: 5, x: 4);
?>
클래스에 생성자가 없거나, 생성자에 필수 인자가 없다면 괄호를 생략할 수도 있어요.
구식 생성자 (Old-style constructors)
PHP 8.0.0 이전에는 전역 네임스페이스의 클래스에서 클래스 이름과 같은 이름의 메서드를 구식 생성자로 해석했어요. 이 문법은 더 이상 권장되지 않는데, E_DEPRECATED 오류가 발생하면서도 그 함수를 생성자로 호출하곤 했죠. __construct()와 같은 이름의 메서드가 둘 다 정의되어 있다면 __construct()가 호출돼요.
네임스페이스가 있는 클래스에서는, 그리고 PHP 8.0.0부터는 모든 클래스에서 클래스 이름과 같은 이름의 메서드는 아무 특별한 의미도 갖지 않아요.
새 코드에서는 항상 __construct()를 쓰세요.
생성자 프로모션 (Constructor Promotion)
PHP 8.0.0부터 생성자 매개변수를 객체 프로퍼티에 대응하도록 승격시킬 수도 있어요. 생성자 매개변수를 프로퍼티에 할당하긴 하는데 그 외에는 별로 조작하지 않는 경우가 정말 흔하거든요. 생성자 프로모션은 바로 그런 상황을 위한 축약형이에요. 위의 예제는 다음과 같이 다시 쓸 수 있어요.
예제 #3 생성자 프로퍼티 프로모션 사용하기
<?php
class Point {
public function __construct(protected int $x, protected int $y = 0) {
}
}
생성자 인자에 수식어(modifier)가 붙으면 PHP는 그 인자를 객체 프로퍼티이자 생성자 인자로 동시에 해석하고, 인자 값을 해당 프로퍼티에 할당해요. 그러면 생성자 본문은 비워 둘 수도 있고 다른 문장들을 담을 수도 있어요. 추가 문장이 있다면 인자 값이 해당 프로퍼티에 할당된 뒤에 실행돼요.
모든 인자를 프로모션해야 하는 건 아니에요. 프로모션된 인자와 그렇지 않은 인자를 아무 순서로든 섞어 쓸 수 있고, 프로모션된 인자는 생성자를 호출하는 코드에는 아무 영향도 주지 않아요.
참고:
가시성 수식어(
public,protected,private)를 쓰는 것이 프로퍼티 프로모션을 적용하는 가장 흔한 방법이지만, 다른 단일 수식어(예:readonly)도 같은 효과를 내요.
참고:
객체 프로퍼티는 엔진의 모호성 때문에
callable타입으로 지정할 수 없어요. 그래서 프로모션된 인자도callable타입으로 지정할 수 없죠. 다른 타입 선언은 무엇이든 허용돼요.
참고:
프로모션된 프로퍼티는 프로퍼티와 함수 매개변수 양쪽으로 디슈가(desugar)되기 때문에, 프로퍼티와 매개변수 양쪽의 모든 이름 규칙이 그대로 적용돼요.
참고:
프로모션된 생성자 인자에 붙은 애트리뷰트는 프로퍼티와 인자 양쪽에 복제돼요. 반면 기본값은 인자에만 복제되고 프로퍼티에는 적용되지 않아요.
이니셜라이저에서의 new (New in initializers)
PHP 8.1.0부터 객체를 기본 매개변수 값, 정적 변수, 전역 상수로 쓸 수 있고, 애트리뷰트 인자에도 쓸 수 있어요. 객체를 define()에 넘기는 것도 이제 가능해요.
참고:
동적이거나 문자열이 아닌 클래스 이름, 익명 클래스는 허용되지 않아요. 인자 풀기(argument unpacking)도 허용되지 않고, 지원되지 않는 표현식을 인자로 쓰는 것도 허용되지 않아요.
예제 #4 이니셜라이저에서 new 사용하기
<?php
// All allowed:
static $x = new Foo;
const C = new Foo;
function test($param = new Foo) {}
#[AnAttribute(new Foo)]
class Test {
public function __construct(
public $prop = new Foo,
) {}
}
// All not allowed (compile-time error):
function test(
$a = new (CLASS_NAME_CONSTANT)(), // dynamic class name
$b = new class {}, // anonymous class
$c = new A(...[]), // argument unpacking
$d = new B($abc), // unsupported constant expression
) {}
?>
정적 생성 메서드 (Static creation methods)
PHP는 클래스 하나당 생성자를 하나만 지원해요. 그런데 어떤 경우에는 서로 다른 입력으로 객체를 여러 가지 방식으로 만들 수 있게 하고 싶을 때가 있어요. 그럴 때 권장하는 방법은 정적 메서드를 생성자 래퍼로 쓰는 거예요.
예제 #5 정적 생성 메서드 사용하기
<?php
$some_json_string = '{ "id": 1004, "name": "Elephpant" }';
$some_xml_string = "<animal><id>1005</id><name>Elephpant</name></animal>";
class Product {
private ?int $id;
private ?string $name;
private function __construct(?int $id = null, ?string $name = null) {
$this->id = $id;
$this->name = $name;
}
public static function fromBasicData(int $id, string $name): static {
$new = new static($id, $name);
return $new;
}
public static function fromJson(string $json): static {
$data = json_decode($json, true);
return new static($data['id'], $data['name']);
}
public static function fromXml(string $xml): static {
$data = simplexml_load_string($xml);
$new = new static();
$new->id = (int) $data->id;
$new->name = $data->name;
return $new;
}
}
$p1 = Product::fromBasicData(5, 'Widget');
$p2 = Product::fromJson($some_json_string);
$p3 = Product::fromXml($some_xml_string);
var_dump($p1, $p2, $p3);
생성자를 private나 protected로 만들면 외부에서 호출할 수 없어요. 그렇게 하면 오직 정적 메서드만 클래스를 인스턴스화할 수 있게 되죠. 같은 클래스 정의 안에 있기 때문에 정적 메서드들은 서로 다른 객체 인스턴스여도 private 메서드에 접근할 수 있어요. private 생성자는 반드시 필요한 건 아니고, 사용 상황에 따라 의미가 있을 수도 없을 수도 있어요.
그다음 세 개의 public 정적 메서드가 객체를 인스턴스화하는 서로 다른 방법을 보여줘요.
-
fromBasicData()는 필요한 인자들을 정확히 받아서, 생성자를 호출해 객체를 만든 뒤 그 결과를 반환해요. -
fromJson()은 JSON 문자열을 받아서 생성자가 원하는 형태로 변환하는 전처리를 스스로 해요. 그다음 새 객체를 반환하죠. -
fromXml()은 XML 문자열을 받아 전처리한 뒤 빈 객체를 만들어요. 생성자는 여전히 호출되지만 모든 인자가 선택이라 그 인자들을 건너뛰게 돼요. 그리고 객체 프로퍼티에 값을 직접 할당한 다음 결과를 반환해요.
세 경우 모두 static 키워드는 코드가 있는 클래스의 이름으로 해석돼요. 여기서는 Product가 되겠죠.
소멸자 (Destructor)
function __destruct(): void
PHP에도 C++ 같은 다른 객체지향 언어와 비슷한 소멸자 개념이 있어요. 소멸자 메서드는 특정 객체를 참조하는 다른 참조가 더 이상 없게 되는 바로 그 시점, 또는 종료 과정(shutdown sequence) 중 어느 시점에 호출돼요.
예제 #6 소멸자 예제
<?php
class MyDestructableClass
{
function __construct() {
print "In constructor\n";
}
function __destruct() {
print "Destroying " . __CLASS__ . "\n";
}
}
$obj = new MyDestructableClass();
생성자와 마찬가지로 부모 소멸자는 엔진이 자동으로 호출하지 않아요. 부모 소멸자를 실행하려면 소멸자 본문에서 parent::__destruct()를 명시적으로 호출해야 해요. 생성자와 마찬가지로 자식 클래스는 자신만의 소멸자를 구현하지 않으면 부모의 소멸자를 상속받을 수 있어요.
소멸자는 스크립트 실행이 exit()로 중단되어도 여전히 호출돼요. 다만 소멸자 안에서 exit()를 호출하면 나머지 종료 루틴들이 실행되지 않게 돼요.
소멸자가 자기 객체에 대한 새 참조를 만들면, 참조 횟수가 다시 0이 되거나 종료 과정에서도 소멸자가 두 번 호출되지는 않아요.
PHP 8.4.0부터, Fiber 실행 중에 사이클 수집(cycle collection)이 일어나면 수집 대상으로 예약된 객체들의 소멸자가 gc_destructor_fiber라는 별도의 Fiber에서 실행돼요. 이 Fiber가 일시 중단되면 남은 소멸자들을 실행할 새 Fiber가 만들어져요. 이전 gc_destructor_fiber는 가비지 컬렉터가 더 이상 참조하지 않으므로, 다른 곳에서 참조되지 않는다면 수집될 수 있어요. 소멸자가 일시 중단된 객체는 소멸자가 반환되거나 Fiber 자체가 수집될 때까지 수집되지 않아요.
참고:
스크립트 종료 중에 호출되는 소멸자에는 HTTP 헤더가 이미 전송된 상태예요. 종료 단계의 작업 디렉터리는 SAPI에 따라 다를 수 있어요(예: Apache).
참고:
소멸자(스크립트 종료 시점에 호출된)에서 예외를 던지려 하면 치명적인 오류가 발생해요.