트레이트
트레이트 (Trait)
PHP에는 코드를 재사용할 수 있는 방법이 하나 있는데, 바로 Trait(트레이트)이에요.
Trait은 PHP처럼 **단일 상속(single inheritance)**을 쓰는 언어에서 코드 재사용을 위한 메커니즘이에요. 단일 상속이 가진 몇 가지 한계를 줄여주는 역할을 하죠. 개발자가 서로 다른 클래스 계층에 속한 여러 독립된 클래스에서 메서드 묶음을 자유롭게 재사용할 수 있게 만들어요. Trait과 클래스가 결합되는 규칙도 복잡도를 줄이고, 다중 상속이나 믹스인(Mixin)이 흔히 겪는 문제를 피하도록 설계되어 있어요.
Trait이 클래스와 비슷해 보이긴 하지만, 용도는 기능을 세밀하고 일관되게 묶는 데만 있어요. Trait 자체로는 인스턴스화할 수 없어요. 기존 상속에 더해지는 개념으로, 상속 없이도 클래스의 멤버를 적용할 수 있는 가로 합성(horizontal composition) 즉 동작의 조합을 가능하게 해줘요.
<?php
trait TraitA {
public function sayHello() {
echo 'Hello';
}
}
trait TraitB {
public function sayWorld() {
echo 'World';
}
}
class MyHelloWorld
{
use TraitA, TraitB; // A class can use multiple traits
public function sayHelloWorld() {
$this->sayHello();
echo ' ';
$this->sayWorld();
echo "!\n";
}
}
$myHelloWorld = new MyHelloWorld();
$myHelloWorld->sayHelloWorld();
?>
위 예제의 출력은 이렇게 나와요.
Hello World!
우선순위 (Precedence)
기본 클래스에서 상속받은 멤버는 Trait이 넣어준 멤버에 의해 덮어써져요. 우선순위를 정리하면, 현재 클래스의 멤버가 Trait 메서드를 덮어쓰고, Trait 메서드는 다시 상속받은 메서드를 덮어써요.
참고: PHP 8.5.0부터 Trait은 부모 클래스보다 먼저 클래스에 결합돼요. 이전에는 그 반대였는데, 이게 Trait의 복사-붙여넣기 의미에 더 가까워졌어요. 특히 Trait과 부모 클래스가 같은 이름의 프로퍼티나 상수를 모두 선언하면, 이제는 Trait의 정의가 상속받은 것보다 우선해요. 이전 버전에서는 호환되지 않는 조합이라며 치명적 오류를 냈었죠. 메서드 해석에는 영향이 없어요. 자세한 내용은 마이그레이션 가이드를 확인해 보세요.
<?php
class Base {
public function sayHello() {
echo 'Hello ';
}
}
trait SayWorld {
public function sayHello() {
parent::sayHello();
echo 'World!';
}
}
class MyHelloWorld extends Base {
use SayWorld;
}
$o = new MyHelloWorld();
$o->sayHello();
?>
위 예제의 출력은 이렇게 나와요.
Hello World!
<?php
trait HelloWorld {
public function sayHello() {
echo 'Hello World!';
}
}
class TheWorldIsNotEnough {
use HelloWorld;
public function sayHello() {
echo 'Hello Universe!';
}
}
$o = new TheWorldIsNotEnough();
$o->sayHello();
?>
위 예제의 출력은 이렇게 나와요.
Hello Universe!
여러 Trait 사용하기 (Multiple Traits)
use 문에 콤마로 구분해 나열하면 클래스에 여러 Trait을 넣을 수 있어요.
<?php
trait Hello {
public function sayHello() {
echo 'Hello ';
}
}
trait World {
public function sayWorld() {
echo 'World';
}
}
class MyHelloWorld {
use Hello, World;
public function sayExclamationMark() {
echo '!';
}
}
$o = new MyHelloWorld();
$o->sayHello();
$o->sayWorld();
$o->sayExclamationMark();
?>
위 예제의 출력은 이렇게 나와요.
Hello World!
충돌 해결 (Conflict Resolution)
두 Trait이 같은 이름의 메서드를 넣는다면, 그 충돌을 명시적으로 해결하지 않는 한 치명적 오류가 발생해요.
같은 클래스에서 사용한 Trait 사이의 이름 충돌을 해결하려면 insteadof 연산자로 충돌하는 메서드 중 정확히 하나를 골라야 해요.
이 방식은 메서드를 하나로 제외할 수 있을 뿐이니까, as 연산자로 그 메서드 중 하나에 별칭(alias)을 붙일 수 있어요. as 연산자는 메서드 이름을 바꾸지도 않고 다른 메서드에 영향을 주지도 않는다는 점을 기억해 두세요.
<?php
trait A {
public function smallTalk() {
echo 'a';
}
public function bigTalk() {
echo 'A';
}
}
trait B {
public function smallTalk() {
echo 'b';
}
public function bigTalk() {
echo 'B';
}
}
class Talker {
use A, B {
B::smallTalk insteadof A;
A::bigTalk insteadof B;
}
}
class Aliased_Talker {
use A, B {
B::smallTalk insteadof A;
A::bigTalk insteadof B;
B::bigTalk as talk;
}
}
?>
메서드 가시성 바꾸기 (Changing Method Visibility)
as 문법을 쓰면 메서드를 사용하는 클래스에서 메서드의 가시성(visibility)도 바꿀 수 있어요.
<?php
trait HelloWorld {
public function sayHello() {
echo 'Hello World!';
}
}
// Change visibility of sayHello
class MyClass1 {
use HelloWorld { sayHello as protected; }
}
// Alias method with changed visibility
// sayHello visibility not changed
class MyClass2 {
use HelloWorld { sayHello as private myPrivateHello; }
}
?>
Trait으로 구성된 Trait (Traits Composed from Traits)
클래스가 Trait을 쓸 수 있듯이 Trait도 다른 Trait을 쓸 수 있어요. Trait 정의 안에서 하나 이상의 Trait을 사용하면, 그 Trait은 다른 Trait들이 정의한 멤버로 부분적으로 또는 전부 구성될 수 있어요.
<?php
trait Hello {
public function sayHello() {
echo 'Hello ';
}
}
trait World {
public function sayWorld() {
echo 'World!';
}
}
trait HelloWorld {
use Hello, World;
}
class MyHelloWorld {
use HelloWorld;
}
$o = new MyHelloWorld();
$o->sayHello();
$o->sayWorld();
?>
위 예제의 출력은 이렇게 나와요.
Hello World!
추상 Trait 멤버 (Abstract Trait Members)
Trait은 추상 메서드를 지원해서, 이를 쓰는 클래스에 요구 사항을 부과할 수 있어요. public, protected, private 메서드가 모두 지원돼요. PHP 8.0.0 이전에는 public과 protected 추상 메서드만 지원됐어요.
주의: PHP 8.0.0부터 구체적인(concrete) 메서드의 시그니처는 시그니처 호환성 규칙을 따라야 해요. 이전에는 시그니처가 달라도 괜찮았어요.
<?php
trait Hello {
public function sayHelloWorld() {
echo 'Hello'.$this->getWorld();
}
abstract public function getWorld();
}
class MyHelloWorld {
private $world;
use Hello;
public function getWorld() {
return $this->world;
}
public function setWorld($val) {
$this->world = $val;
}
}
?>
정적 Trait 멤버 (Static Trait Members)
Trait은 정적 변수, 정적 메서드, 정적 프로퍼티를 정의할 수 있어요.
참고: PHP 8.1.0부터 Trait에서 직접 정적 메서드를 호출하거나 정적 프로퍼티에 접근하는 것은 deprecated(권장되지 않음) 처리됐어요. 정적 메서드와 프로퍼티는 Trait을 사용하는 클래스에서만 접근해야 해요.
<?php
trait Counter
{
public function inc()
{
static $c = 0;
$c = $c + 1;
echo "$c\n";
}
}
class C1
{
use Counter;
}
class C2
{
use Counter;
}
$o = new C1();
$o->inc();
$p = new C2();
$p->inc();
?>
위 예제의 출력은 이렇게 나와요.
1
1
<?php
trait StaticExample
{
public static function doSomething()
{
return 'Doing something';
}
}
class Example
{
use StaticExample;
}
echo Example::doSomething();
?>
위 예제의 출력은 이렇게 나와요.
Doing something
<?php
trait T
{
public static $counter = 1;
}
class A
{
use T;
public static function incrementCounter()
{
static::$counter++;
}
}
class B extends A
{
use T;
}
A::incrementCounter();
echo A::$counter, "\n";
echo B::$counter, "\n";
?>
주의: PHP 8.3.0 이전에는 Trait에 정의된 정적 프로퍼티가 같은 상속 계층에서 그 Trait을 사용하는 모든 클래스에 공유됐어요. PHP 8.3.0부터는 자식 클래스가 정적 프로퍼티를 가진 Trait을 쓰면, 부모 클래스에 정의된 것과 별개로 취급돼요.
위 예제의 PHP 8.3에서의 출력은 이렇게 나와요.
2
1
프로퍼티 (Properties)
Trait은 프로퍼티도 정의할 수 있어요.
<?php
trait PropertiesTrait
{
public $x = 1;
}
class PropertiesExample
{
use PropertiesTrait;
}
$example = new PropertiesExample();
$example->x;
?>
Trait이 프로퍼티를 정의했다면, 클래스는 호환되지 않는(같은 가시성·타입·readonly 한정자·초기값) 같은 이름의 프로퍼티를 정의할 수 없어요. 호환되지 않으면 치명적 오류가 발생해요.
<?php
trait PropertiesTrait {
public $same = true;
public $different1 = false;
public bool $different2;
public bool $different3;
}
class PropertiesExample {
use PropertiesTrait;
public $same = true;
public $different1 = true; // Fatal error
public string $different2; // Fatal error
readonly protected bool $different3; // Fatal error
}
?>
상수 (Constants)
PHP 8.2.0부터 Trait은 상수도 정의할 수 있어요.
<?php
trait ConstantsTrait {
public const FLAG_MUTABLE = 1;
final public const FLAG_IMMUTABLE = 5;
}
class ConstantsExample {
use ConstantsTrait;
}
$example = new ConstantsExample;
echo $example::FLAG_MUTABLE;
?>
위 예제의 출력은 이렇게 나와요.
1
Trait이 상수를 정의했다면, 클래스는 호환되지 않는(같은 가시성·초기값·final 여부) 같은 이름의 상수를 정의할 수 없어요. 호환되지 않으면 치명적 오류가 발생해요.
<?php
trait ConstantsTrait {
public const FLAG_MUTABLE = 1;
final public const FLAG_IMMUTABLE = 5;
}
class ConstantsExample {
use ConstantsTrait;
public const FLAG_IMMUTABLE = 5; // Fatal error
}
?>
final 메서드 (Final methods)
PHP 8.3.0부터 Trait에서 가져온 메서드에 as 연산자로 final 한정자를 적용할 수 있어요. 이렇게 하면 자식 클래스가 그 메서드를 오버라이드하지 못하게 막을 수 있어요. 다만 Trait을 사용하는 클래스 자체는 그 메서드를 여전히 오버라이드할 수 있어요.
<?php
trait CommonTrait
{
public function method()
{
echo 'Hello';
}
}
class FinalExampleA
{
use CommonTrait {
CommonTrait::method as final; // The 'final' prevents child classes from overriding the method
}
}
class FinalExampleB extends FinalExampleA
{
public function method() {}
}
?>
위 예제의 출력은 대략 이렇게 나와요.
Fatal error: Cannot override final method FinalExampleA::method() in ...