88호. PHP 7.4 타입이 지정된 프로퍼티 (Typed Properties) free

2019-12-05

오늘은 PHP 7.4에 추가된 타입이 지정된 프로퍼티(Typed Properties)에 대해 소개해드리고자 합니다.


간략 소개


클래스 프로퍼티에 타입을 선언할 수 있게 되었습니다.


<?php
class User {
public int $id;
public string $name;
}
?>

위와 같이 하면 $id에는 int만, $name에는 string만 넣도록 강제할 수 있습니다. 자세히 읽어 볼 시간이 없으신 분들은 일단, 이 정도만 알고 있어도 활용하시는 데는 충분할 것 같습니다.


어떤 기능이 추가된 동기를 알면 그 기능을 어디에 쓰면 좋을지 더 잘 알 수 있게 되는 것 같습니다. 도입 동기와 기타 사용상의 주의 사항 등을 아래에 정리해봤습니다.


도입 동기


PHP 7.3까지는 클래스 프로퍼티에 타입을 선언할 수 없었습니다. 그렇기 때문에 프로퍼티에 타입을 강제하기 위해서는 게터와 세터를 써야만 했는데요. 덕분에 불필요한 코드를 써야 했고 성능에도 악영향이 있었다고 합니다. 코드로 비교해보면 훨씬 와 닿으실 것 같습니다. 다음은 $id와 $name의 타입을 강제하기 위해 게터와 세터를 사용하는 코드입니다.


class User {
/** @var int $id */
private $id;
/** @var string $name */
private $name;

public function __construct(int $id, string $name) {
$this->id = $id;
$this->name = $name;
}

public function getId(): int {
return $this->id;
}
public function setId(int $id): void {
$this->id = $id;
}

public function getName(): string {
return $this->name;
}
public function setName(string $name): void {
$this->name = $name;
}
}

클래스 프로퍼티에 타입을 지정할 수 있으면 아래와 같이 훨씬 간단하게 타입을 강제할 수 있습니다.


class User {
public int $id;
public string $name;

public function __construct(int $id, string $name) {
$this->id = $id;
$this->name = $name;
}
}

기본 문법


class Example {
// void와 callable을 제외한 모든 타입을 사용할 수 있습니다.
public int $scalarType;
protected ClassName $classType;
private ?ClassName $nullableClassType;

// 정적 프로퍼티에도 사용할 수 있습니다.
public static iterable $staticProp;

// var도 사용할 수 있습니다.
var bool $flag;

// 타입이 지정된 프로퍼티는 기본값을 가질 수도 있습니다.
public string $str = "foo";
public ?string $nullableStr = null;

// 한 번에 여러 프로퍼티에 타입을 지정할 수 있습니다.
public float $x, $y;
// 위의 코드는 한 줄은 아래 두 줄과 같습니다.
public float $x;
public float $y;
}

엄격하게 타입 지정하기


좋아하는 사람도 있고 싫어하는 사람도 있지만, PHP는 타입을 강제하되 다른 타입이 들어와도 가능하면 타입을 자동으로 바꿔줍니다.


function coerce(int $i)
{ /* … */ }

coerce('1'); // 1

int가 들어올 자리에 문자열이 들어왔지만, PHP가 알아서 int로 변환해서 처리합니다.


클래스 프로퍼티에도 위의 원리가 그대로 적용됩니다.


class Bar
{
public int $i;
}

$bar = new Bar;

$bar->i = '1'; // 1

이렇게 PHP가 자동으로 타입을 변환하는 게 싫으면 strict_types를 1로 선언하면 됩니다. 그러면 다른 타입이 대입하려고 할 때 에러가 납니다.


declare(strict_types=1);

$bar = new Bar;

$bar->i = '1';

Fatal error: Uncaught TypeError:
Typed property Bar::$i must be int, string used

상속과 가변성


private이 아닌 프로퍼티는 상속 과정에서 타입을 바꿀(프로퍼티 타입을 추가하거나 제거하는 것 포함) 수 없습니다.


class A {
private bool $a;
public int $b;
public ?int $c;
}

class B extends A {
public string $a; // 가능. A::$a가 private이기 때문
public ?int $b; // 불가능
public int $c; // 불가능
}

selfparent는 선언된 위치에 따라 달리 해석하기 때문에 아래 코드는 쓸 수 없습니다.


class A {
public self $prop;
}

class B extends A {
public self $prop;
}

알리아스 된 두 클래스는 같은 것으로 보기 때문에 아래 코드는 쓸 수 있습니다.


// file1.php
class Foo {}
class_alias('Foo', 'Bar');

// file2.php
class A {
public Foo $prop;
}

// file3.php
class B extends A {
public Bar $prop;
}

두 개 이상의 트레이트를 불러올 때 서로 다른 트레이트에 같은 이름의 프로퍼티가 있다면 타입도 같아야 합니다. 따라서 다음의 코드는 사용할 수 없습니다.


trait T1 {
public int $prop;
}
trait T2 {
public string $prop;
}
class C {
use T1, T2;
}

Uninitialized


uninitialized 라는 변수 상태가 추가되었습니다. 타입이 없는 프로퍼티에 아무 값도 넣지 않으면 null 입니다. 반면, 타입이 있는 프로퍼티에는 아무 값도 입력하지 않으면 uninitialized라는 상태로 인식되는데, 이는 null과는 다릅니다.


프로퍼티에 접근하는 시점에 초기화 여부를 확인하기 때문에 일단 초기화하지 않은 상태로도 객체를 만들 수 있습니다.


class Foo
{
public int $bar;
}

$foo = new Foo;

하지만 이 상태로 프로퍼티를 읽으면 에러가 납니다.


var_dump($foo->bar);

Fatal error: Uncaught Error: Typed property Foo::$bar
must not be accessed before initialization

읽기 전에 값을 넣으면 에러가 나지 않습니다. 다음과 같이 객체를 만든 후 값을 넣어도 문제가 되지 않습니다.


class Foo
{
public int $a;
}

$foo = new Foo;

$foo->a = 1;

타입이 있는 프로퍼티를 unset 하면 uninitialized가 되고, 타입이 없는 프로퍼티는 null이 됩니다.


기본값


스칼라 타입은 기본값을 줄 수 있습니다. 당연한 얘기겠지만, 기본값은 프로퍼티의 타입과 타입이 일치해야 합니다.


실제로 null을 허용하는 타입만 기본값으로 null을 쓸 수 있습니다. 따라서 아래와 같이 사용할 수 없습니다.


function passNull(int $i = null)
{ /* … */ }

passNull(null);

객체나 클래스 타입은 기본값을 줄 수 없습니다. 이들은 생성자를 이용해야 합니다.


마치며


클래스에 타입이 있는 프로퍼티가 도입되면서 더 적은 코드로 더 엄격한 코드를 작성할 수 있게 된 것 같습니다. 물론 PHP가 항상 그래왔듯이 쓰고 싶은 사람만 쓰면 되고 필수는 아닙니다. 기존 코드에 영향을 주지도 않고요. 더 엄격하게 코드를 작성하고 싶었던 분들께는 좋은 소식이 아니었나 싶습니다.


그럼 오늘도 즐거운 코딩 생활하세요!


1일 1식 라라벨 88호

2019년 12월 5일


이현석

바쁜 팀장님 대신 알려주는 신입 PHP 개발자 안내서를 쓰고, 클린 아키텍처 인 PHP를 번역했습니다. 2020년에 출간될 Laravel Up & Running 2nd Edition을 번역하고 있습니다.