Page tree

트레이트는 PHP 5.4 에 추가된 기능으로 루비 언어의 mixin 과 동일한 용도를 제공하며 공통적으로 사용하는 메소드나 프로퍼티를 재사용할 수 있게 해주는 기능으로 인터페이스와 비슷하게 보이지만 큰 차이점은 트레이트는 구현부를 제공한다는 점입니다. 

trait Validation { 
  public function validation($item)
  {
    return 'validation this item';
  } 
}

class 대신 trait이라는 키워드를 사용한다는 것을 빼면 클래스와 큰 차이가 없어 보이지만 트레이트는 큰 잇점이 있습니다.


PHP의 상속 모델은 단일 상속으로 이 의미는 자식 클래스는 하나의 부모 클래스만 상속할 수 있다는 의미입니다.

그러므로 여러 클래스에서 상속받는 것과 비슷한 효과를 내려면 하나는 클래스가 아닌 인터페이스로 선언하여 이를 구현하고 부모 클래스를 상속받아야 합니다.

단일 상속 모델에서는 Person 을 인터페이스로 선언하고 Employee 는 Person 을 상속받고 getAge() 를 구현해야 하며 Teacher 클래스는 Employee 를 상속받아서 사용하면 됩니다.

하지만 Person 을 상속받지만 Employee 는 아닌 Student 클래스를 만들경우 또 getAge() 를 구현해야 하므로 같은 레벨의 클래스마다 중복된 코드가 발생하게 됩니다.


하지만 트레이트가 있으므로  여러 클래스에 공통적으로 필요한 특성이 있다면 트레이트로 구현하고 이를 가져다가 쓰면 되므로 소스의 재사용성이 높아지고 소스가 간단해 집니다. 


좋은 예제는 라라벨의 ORM Model 의 제약 조건을 검증하는 패키지인 validation 입니다. 서비스나 앱을 개발할 때 데이타 모델링을 하며 데이타의 조건에 따라 여러 가지 제약 조건을 설정하게 됩니다.

모델 값의 검증은 모델마다 공통적으로 필요하지만 실제 검증 함수는 모델의 특성마다 다르게 됩니다. 또 라라벨의 경우 eloquent 라는 ORM 을 사용할 경우 모든 모델은 eloquent ORM 을 상속받아야 하므로 검증 기능을 모델 클래스마다 공통적으로 사용하려면 복잡한 상속 단계를 거쳐야 합니다.


예로 게시판을 만들고 있는데 개별 글을 모델링하는 Post 라는 모델이 있다고 가정하겠습니다. 포스트 테이블은 id, 제목, 내용 이렇게 세 가지 컬럼을 갖고 있습니다.

CREATE TABLE Post (
	id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
	title VARCHAR(100) not null,
	content TEXT not null
);


이제 사용자가 글을 올릴 때 이를 받아서 데이타의 유효성 여부를 검증하는 서버 코드를 작성한다고 생각해 봅시다.


Post 클래스는 ORM(Object Relation Model) 을 사용하므로 데이타베이스의 테이블과 매핑되는 클래스는 반드시 Model 클래스를 상속해야 하나 단일 상속 모델이므로 데이타를 검증하는 클래스를 또 상속할 수는 없습니다.


이를 위해서 만약 인터페이스를 사용했을 경우 다음과 같이 사용해야 합니다.

interface ValidationInterface {
	public function validate();
}


이럴 경우 모든 자식 클래스들은 실제 데이타 검증을 하지 않더라도 ValidationInterface 인터페이스를 구현하여야 합니다.

class Post extends Model implements ValidationInterface {
	protected $id;
	protected $title;
	protected $content;
 
	public function validate() {
		// 데이타 검증 코드 구현
	}
}

위 코드는 잘 작동하겠지만 모델마다 Validation 클래스를 상속받아서 validate 메소드를 구현하는 것은 코드의 중복이 발생하며 수정이 발생해야 할 경우 모든 자식 클래스를 수정해야 합니다.

라라벨은 Interface 대신 계약이라는 의미의 Contract 라는 용어를 사용하고 있으며 인터페이스 역할을 하는 클래스들은 Illuminate\Contracts 네임스페이스 아래에 위치하고 있습니다.

개인적인 의견으로는 인터페이스라는 어려운 용어보다는 계약이 더 명확한 의미를 전달한다고 생각합니다.


이럴 경우 공통 코드는 트레이트를 사용하면 소스의 재사용성을 높이고 복잡한 상속을 거치지 않아도 됩니다.

trait ValidationTrait {
	public function validate() {
		$rules = $this->getRules();

        return $this->performValidation($rules);
	}
}


트레이트는 코드 조각이므로 $rules 변수나 performValidation 이 없어도 컴파일 에러가 나지 않습니다. 이제 모델 클래스는 다음과 같이 트레이트를 사용합니다.

use MyVendor\Validating\ValidatingTrait;

class Post extends Model
{
	// 트레이트 사용
    use ValidatingTrait;

	// 트레이트에 넘길 제약 조건 규칙
    protected $rules = [
        'title'   => 'required',
        'slug'    => 'required|unique:posts,slug',
        'content' => 'required'
    ];
 
	protected performValidation($rules) {
		// 규칙 검증
	}
}

이제 모델 클래스들은 복잡한 상속을 거치지 않고 트레이트을 사용하고 트레이트에 넘길 규칙만 정해주면 됩니다.

라라벨 내에서도 트레이트을 많이 사용하고 있으며 특히 라라벨 패키지중 과금을 처리하는 패키지인 Cashier 은 트레이트를 활용하여 사용자가 상속을 거치지 않고 사용자마다 다른 개별 과금 프로세스를 구현할 수 있도록 하고 있습니다.


trait override

여러 가지 이유로 트레이트내 메서드나 함수를 재구현해야 할 일이 있을수 있습니다. 예로 라라벨의 AuthenticatesUsers 에는 아래와 같이 showLoginForm() 과 login() 메서드가 구현되어 있는데

이중에서 login() 만 재구현할 필요가 있을 수 있습니다.

trait AuthenticatesUsers
{
    use RedirectsUsers, ThrottlesLogins;

    /**
     * Show the application's login form.
     *
     * @return \Illuminate\Http\Response
     */
    public function showLoginForm()
    {
        return view('auth.login');
    }

    /**
     * Handle a login request to the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response
     */
    public function login(Request $request)
    {

트레이트는 일반 클래스가 아니므로 override 가 되지 않지만 다음과 같이 트레이트를 사용하는 클래스에서 재구현 대상 메서드 이름을 변경한 후에 재구현 해주면 됩니다.


class AuthController extends Controller
{
	use AuthenticatesUsers {
		// 재구현을 위해 이름 변경
    	login as protected defaultAuthLogin;
	}
	
	// 재구현
	public function login(Request $request)
	{
		doSomething();
	}
}



Ref