PHP5.4부터 추가된 Traits 라는 기능에 대해 알아보자.

Trait 란?

PHP5.4부터 추가된 내용으로써 클래스에 적용이 가능한 메소드 및 프로퍼티의 집합이다.

내가 자신있게 작성할 수 있는 부분은 여기까지이다.

Trait에 관한 예제와 자세한 설명은 아래 내용을 통해 확인해 본다. 출처

PHP5.4의 trait를 이용한 싱글톤 패턴 구현 (trait입문)

PHP5.4 alpha1가 릴리스 되었다. PHP의 OOP의 새로운 기능으로서 trait라 불리는 기능이 추가되었다.

PHP5.4의 trait는 형에 영향을 주지 않고 클래스에 적용이 가능한 메소드, 프로퍼티의 집합이다.

조속히 PHP5.4 alpha1를 인스톨해 trait를 사용해 싱글톤 패턴을 구현해 보았다.

이 코드는 클래스의 계승관계에 영향을 주지않고 싱글톤 패턴을 모듈화 하고 있다.


trait Singleton
{
    protected function __construct() {}
    static function getInstance()
    {
        static $obj = null;
        return $obj ?: $obj = new static;
    }

    function __clone()
    {
        throw new RuntimeException("You can't clone this instance.");
    }
}

클래스의 계승관계에 영향을 주지않는다는 것은 무얼 의미하는 걸까?

trait는 trait를 사용하는 클래스가 어떤 클래스를 계승해도 그리고 어떤 인터페이스를 구현해도 관계없이 사용할수 있다.

예를 든다면 위에 적은 trait는 아래와 같이 사용할수 있다.

class MyBaseClass
{
    function doSomething()
    {
        /* ... */
    }
}

class MyClass extends MyBaseClass
{
    use Singleton; // Singleton trait를 사용

    function doMySomething()
    {
        /* ... */
    }
}

$pro = MyClass::getInstance();
$pro->doSomething();
$pro->doMySomething();

new MyClass; // 컨스트럭터를 직접 콜하면 에러가 발생

getInstance라는 메소드명이 마음에 들지 않으면 아래처럼 as를 사용하여 별칭을 설정하는것도 가능하다.

class MyHoge
{
    use Singleton
    {
        getInstance as singleton;
    }
}
$hoge = MyHoge::singleton();

그리고 싱글톤으로 구현된 오브젝트를 구현, 컨스트럭트로 부터 인터페이스화 하고 싶은 경우 아래처럼 as를 사용하여 메소드의 속성을 변경한다. 게다가 clone도 가능하도록 하고 싶은경우 단순하게 trait의 메소드를 오버라이드 한다.

class MyHoge
{
    use Singleton
    {
        __construct as public; // 컨스트럭트를 public으로 한다.
    }

    function __clone() {} // clone의 오버라이드
}

$hoge = new MyHoge;    // 에러 없음
$hoge2 = clone($hoge); // clone도 가능

위에서 trait는 < 형에 영향을 주지 않고 클래스가 사용가능하도록 하는 메소드,프로퍼티의 집합이다. > 라고 적었는데 이것은 무엇을 의미 하는 것일까?

이번에 추가된 trait의 용도를 말하기 전 먼저 PHP에 이전부터 구현되어 있는 객체지향언어의 기능을 정리해보자.

class Hoge
{
    function doHoge()
    {
        echo "hoge";
    }
}

class Fuga extends Hoge
{
    function doFuga()
    {
        echo "fuga";
    }
}

$fuga = new Fuga;
$fuga->doHoge(); // 부모Hoge클래스의 메소드가 이용가능
$fuga->doFuga();

var_dump($fuga instanceof Hoge); // Hoge오브젝트로서 인정

call_user_func(function(Hoge $hoge) { // 타이프 힌트에도 에러 없음
    /* ... */
}, $fuga);

객체지향언어중에는 다중상속이 가능한 언어가 있지만 PHP는 단일 상속만 가능하다.

따라서 클래스를 사용해서 Singleton패턴을 모듈화하려고 해도 상속 받은 자식클래스에서는 사용할수 없다.

상속은 여러 경우에서 충분히 강력하지만 한편 불편할때도 있다.

abstract class Singleton
{
    protected function __construct() { }
    static function getInstance()
    {
        static $obj = null;
        return $obj ?: $obj = new static;
    }
}

class Hoge extends Fuga
{
    // 이 클래스는 이미 Fuga 클래스를 계승해 위의 Singleton클래스를 계승할수 없다.
}

인터페이스에서는 그 인터페이스에 정의되어 있는 메소드를 클래스가 그 역할에 맞게 구현 하면 된다.

클래스와 복수의 인터페이스를 사용 할수 있다.

interface Hoge
{
    function hoge();
}

interface Fuga
{
    function fuga();
}

class Piyo implements Hoge, Fuga
{
    function hoge()
    {
        echo "hoge";
    }

    function fuga()
    {
        echo "fuga";
    }
}

$piyo = new Piyo;
var_dump($piyo instanceof Hoge); // => true
var_dump($piyo instanceof Fuga); // => true

< 계승 >에서는 부모 클래스의 메소드, 프로퍼티등의 멤버를 재이용 가능하고, 부모클래스랑 같은 형태으로 인정된다.

이에 관해「인터페이스」에서는 구현한 인터페이스랑 클래스가 동일한 형태로 인정된다.

또 한개의 클래스와 복수의 인터페이스를 사용가능하다.

< 계승 > 과 < 인터페이스 > 의 틀린점은 인터페이스가 가지고 있는 메소드는 시그네이쳐만 정의 되어 있으므로 코드를 재사용은 없다는 점이다.

그럼, trait는?

trait Hoge
{
    function doHoge()
    {
        echo "hoge";
    }
}

trait Fuga
{
    function doFuga()
    {
        echo "fuga";
    }
}

class Piyo
{
    use Hoge, Fuga;
}

$piyo = new Piyo;
$piyo->doHoge(); // "hoge"라고 표신된다.
var_dump($piyo instanceof Hoge); // => false

trait는 인터페이스터럼 클래스가 얼마든지 사용가능하나 trait는 형태를 가지지 않는다.

다시말하면 클래스의 틀에 영향을 주지 않는다.

지금까지의 내용을 간단히 표로 정리하면 아래와 같다.

*계승
- 재이용(O)
- 형에 영향(X)
- 복수적용가능여부(O)
*인터페이스
- 재이용(O)
- 형에 영향(X)
- 복수적용가능여부(O)
*trait
- 재이용(O)
- 형에 영향(X)
- 복수적용가능여부(O)

계승은 부모 클래스의 코드를 재이용하고 형태에 관해서도 부모 클래스의 형으로 인정된다.

클래스가 계승가능한것은 한개의 클래스만 가능하다.

인터페이스가 계승가능한것은 메소드의 내부 처리와 정의 되지 않으므로 재이용할 수 없으나 클래스에서는 복수의 인터페이스를 사용할수 있다.

trait는 클래스의 형태에 아무런 영향을 주지 않지만 메소드, 프로퍼티의 집합을 클래스 안에 여러개 집어넣을수 있다.

이러한 내용으로 PHP5.4 alpha1 부터 새롭게 등장한 trait의 기본적인 걔념에 관해 설명했다.

다음은 trait의 메소드의 충돌과 그것의 해결에 대해서 쓴다.

아래와같이, 이용하는 복수의 trait에서 메소드명이 중복했을 경우, 클래스의 선언시에 에러를 낸다. 이것을 메소드의 충돌이라고 부른다.

trait A
{
    function doSomething()
    {
        echo "hoge";
    }
}

trait B
{
    function doSomething()
    {
        echo "fuga";
    }
}

class C
{
    use A, B; // fatal error!
}

fatal error가 발생하지만 아래처럼 override 한 경우는 문제가 없이 실행된다.

trait A
{
    function doSomething()
    {
        echo "hoge";
    }
}

trait B
{
    function doSomething()
    {
        echo "fuga";
    }
}

class C
{
    use A, B;

    function doSomething()
    {
        echo "hogefuga";
    }
}

메소드의 충돌을 해결하기 위해서는 새롭게 되입된 “insteadof”를 사용하여 우선도를 설정한다.

class D
{
    use A, B
    {
        A::doSomething insteadof B; // A::doSomething 메소드를 우선
    }
}
$d = new D;
$d->doSomething(); // "hoge"라고 표시된다.

충돌하는 메소드 양쪽 모두를 사용하고 있는경우는 “as”를 사용해서 별칭을 설정하서 메소드의 충돌을 해결한다.

class E
{
    use A, B
    {
        A::doSomething as doSomethingOnA;
        B::doSomething insteadof A;
    }
}
$e = new E;
$e->doSomething();    // "fuga"라고 표시된다.
$e->doSomethingOnA(); // "hoge"라고 표시된다.

잘못생각하기 쉬운데 “as”를 사용한 메소드의 별칭 설정은 메소드명의 변경이 아니므로 주의하기 바란다.

아래의 예를 통해 알아보자.

trait A
{
    function doSomething()
    {
        echo "hoge";
    }
}

class B
{
    use A
    {
        doSomething as hoge;
    }
}

$b = new B;
$b->doSomething(); // "hoge"라고 표시된다.
$b->hoge();        // "hoge"라고 표시된다.
trait A
{
    function doSomethingTwice()
    {
        $this->doSomething();
        $this->doSomething();
    }

    function doSomething()
    {
        echo "hoge";
    }
}

trait B
{
    function doSomething()
    {
        echo "foobar";
    }
}

class C
{
    use A, B
    {
        A::doSomething as doSomethingOnA; // 별칭을 설정
        B::doSomething insteadof A;       // 우선순위를 설정
    }
}

$c = new C;
$c->doSomethingOnA();   // "hoge"라고 표시된다.
$c->doSomething();      // "foobar"라고 표시된다.
$c->doSomethingTwice(); // "hogehoge"라고 표시된다.
정리
- trait는 클래스에 적용가능한 메소드, 프로퍼티의 집합을 의미한다.
- trait의 적용은 그 클래스의 형태에는 영향을 주지 않는다.
- 클래스의 계승, 인터페이스의 구현만으로는 한계가 있다.(php는 다중상속이 불가능)
- trait메소드의 충돌을 해결하기 위해 우선순위, 별칭을 설정할수 있다.

간단하게 말하면 trait는 클래스에 사용될 메소드, 프로퍼티의 묶음을 의미하며 trait는 class의 구성부품으로 서로 조합될때 계승구조와 관계없이 동일한 선상에서 조합된다.

하지만 trait안에서 trait를 조합(use)할 경우에는 계승구조에 따라 조합된다.

구성 부품으로 사용되므로 trait단독으로는 인스턴스화 할수 없다.

또한 trait에 정의된 static 멤버 변수가 복수의 클래스에 사용될 경우 직접 static변수가 정의된 클래스를 또 다른 클래스가 상속해 사용하는 경우처럼 공유되지 않고 별개 동작한다. 이는 trait가 구성부품으로 사용된다는 것에 대한 반증이기도 하다.

trait에서 정의된 private나 protected 메소드 또한 클래스의 그것가 달리 그냥 메소드에 지나지 않으며 비로서 클래스 안에 포함되고 나서야 본래의 특성이 활성화 된다.

아래의 예를 보면 좀더 이해하기 쉬워질것이다.

trait A

{
    public function getVar() { return 'A';  }
    private function _getVar() { return '_A'; }
}

trait B
{
    public function getVar() { return 'B'; }
    public function getVar1() { return $this->_getVar(); }
}

class AA
{
    use A, B;
    public function getVar() { return $this->_getVar(); }
}

$aa = new AA;
echo $aa->getVar1().PHP_EOL;
echo $aa->getVar().PHP_EOL;

trait C
{
    use A;
    public function getVar() { return 'C'; }
}

class BB
{
    use C;
//    public function getVar() { return $this->_getVar(); }

}
$bb = new BB;
echo $bb->getVar().PHP_EOL;

일단 위의 실행 결과는 아래와 같다.

_A
_A
C

첫번째와 두번째의 결과를 보면 특이한 점을 알수 있다. trait B 에는 정의되어 있지않은 _getVar()를 콜하고 있으며 이는 trait A에 있다는걸 알수 있다.

또한 이 메소드는 private이다. 이는 위에서 언급한 “class의 구성부품으로 서로 조합될때 계승구조와 관계없이 동일한 선상에서 조합된다.” 처럼 동작하는것을 알수있다.

여기서 별도로 눈여겨 볼것이 trait A와 B에 있는 getVar()라는 메소드인데 좀전에 언급한것터럼 동일한 선상에서 조합된다면 메소드명이 중복되게 되는데 괜찮은것인가라고 생각할수도 있다. 역시 괜찮지 않다. 그럼 위의 코드는 왜 문제없이 실행 되는걸까?

그 이유는 class AA에 있는 getVar() 메소드 때문이다.

AA클래스에 getVar()라는 동명의 메소드가 존재하기때문에 각 trait의 getVar()는 무시되었기 때문이다.

만약 AA클래스에 동명의 메소드가 존재하지 않는다면 PHP Fatal Error로 종료될것이다.

하지만 위의 예제는 보이기 위해 만든것으로 trait B의 getVar1()처럼 코드는 만드는것은 가독성을 떨어뜨리며 구조파악에 혼란만 가할뿐이므로 비추천한다.

세번째의 처리를 보면 trait C 안에서 trait A 를 use하는것을 볼수 있다.

이 경우는 위에서 언급한것처럼 계승법에 의해 trait C의 getVar()가 trait A의 getVar()를 override했다라고 생각하면 된다.(결과또한 C)

다음은 static관련예제로 php.net에서 발췌한것이다.

Example using parent class:

class TestClass {
    public static $_bar;
}
class Foo1 extends TestClass { }
class Foo2 extends TestClass { }
Foo1::$_bar = 'Hello';
Foo2::$_bar = 'World';
echo Foo1::$_bar . ' ' . Foo2::$_bar; // Prints: World World
// Example using trait:
trait TestTrait {
    public static $_bar;
}
class Foo1 {
    use TestTrait;
}
class Foo2 {
    use TestTrait;
}
Foo1::$_bar = 'Hello';
Foo2::$_bar = 'World';
echo Foo1::$_bar . ' ' . Foo2::$_bar; // Prints: Hello World

마치며

정광섭님이 작성하신 Trait 내용 중 인상깊은 문구가 있다. “인터페이스라는 어려운 용어보다는 계약이 더 명확한 의미를 전달한다고 생각합니다.” 이제 주변에서 인터페이스를 묻는다면 이처럼 말해 줄 것이다.

Tags: laravel