개발블로그

[객체지향] PHP Trait 본문

STUDY/PHP

[객체지향] PHP Trait

devmel 2026. 9. 5. 20:51
Contents 접기
 

개념

 

필요성

서로 다른 클래스에서 같은 기능이 필요한 경우가 있음.

ex) 사용자 관리 클래스와 파일 처리 클래스가 모두 로그를 기록한다고 할 때, 

각 클래스에 같은 메서드를 작성하면 중복이 생기고, 기능이 바뀔 때 여러 곳을 수정해야 함

 

개념

여러 클래스에 포함하여 재사용할 수 있는 메서드와 속성의 묶음

- Trait을 포함한 클래스에서는 해당 메서드를 자신의 메서드처럼 사용할 수 있음.

 


 

문법

 

기본 문법

trait TraitName
{
    // 속성 정의
    private string $propertyName = "초기값";

    // 메서드 정의
    public function methodName(): string
    {
        return $this->propertyName;
    }
}

class ClassName
{
    use TraitName;
}

$object = new ClassName();
echo $object->methodName(); // 초기값

 

ex)

trait LoggerTrait
{
    private string $prefix = "[LOG]";

    public function log(string $message): void
    {
        echo "{$this->prefix} {$message}\n";
    }
}

class UserService
{
    use LoggerTrait;
}

class FileService
{
    use LoggerTrait;
}

$userService = new UserService();
$fileService = new FileService();

$userService->log("사용자 생성");
$fileService->log("파일 저장");
/*
[LOG] 사용자 생성
[LOG] 파일 저장
*/

여러 클래스에서 여러 Trait을 함께 사용할 수 있음

trait FirstTrait
{
    public function firstMethod(): void
    {
        // 첫 번째 기능
    }
}

trait SecondTrait
{
    public function secondMethod(): void
    {
        // 두 번째 기능
    }
}

class ClassName
{
    use FirstTrait, SecondTrait;
}

 

ex)

trait LoggerTrait
{
    public function log(string $message): void
    {
        echo "[LOG] {$message}\n";
    }
}

trait NotifierTrait
{
    public function notify(string $message): void
    {
        echo "[알림] {$message}\n";
    }
}

class UserService
{
    use LoggerTrait, NotifierTrait;

    public function register(): void
    {
        $this->log("사용자 등록");
        $this->notify("가입 완료");
    }
}

$service = new UserService();
$service->register();

 

 

메소드 충돌 해결

여러 Trait에 같은 이름의 메서드가 있으면, 어떤 구현을 사용할지 명시해야 함.

키워드 역할
insteadof 충돌하는 메서드 중 사용할 구현 선택
as 메서드에 별칭 추가

 

class ClassName
{
    use FirstTrait, SecondTrait {
        FirstTrait::methodName insteadof SecondTrait;
        	// methodName을 호출하면, SecondTrait대신 FirstTrait의 구현을 사용하도록 지정
        SecondTrait::methodName as anotherMethod;
        	// SecondTrait의 methodName를 anotherMethod의 이름으로 사용할 수 있도록 별칭 추가
    }
}

 

ex)

trait ConsoleLoggerTrait
{
    public function log(string $message): void
    {
        echo "[일반] {$message}\n";
    }
}

trait JsonLoggerTrait
{
    public function log(string $message): void
    {
        echo json_encode(["message" => $message]) . "\n";
    }
}

class UserService
{
    use ConsoleLoggerTrait, JsonLoggerTrait {
        ConsoleLoggerTrait::log insteadof JsonLoggerTrait;
        JsonLoggerTrait::log as logJson;
    }
}

$service = new UserService();

$service->log("created");
$service->logJson("created");
/*
[일반] created
{"message":"created"}
*/

 

 

접근 제어자와 별칭 지정

Trait을 클래스에 포함할 때 as를 사용하면 메서드의 접근 범위를 변경하거나, 호출할 이름을 추가할 수 있음

use TraitName {
    methodName as private;
    // 기존 메서드의 접근 범위 변경
}

use TraitName {
    methodName as aliasName;
    // 기존 메서드에 별칭 추가
}

use TraitName {
    methodName as private aliasName;
    // private 접근 범위를 가진 별칭 추가
}

 

ex)

trait LoggerTrait
{
    public function log(string $message): void
    {
        echo "[LOG] {$message}\n";
    }
}

/*
접근 범위만 변경
*/
class UserService
{
	// 기존 log()가 private가 됨. 이름은 그대로이며 외부 호출은 제한 됨
    use LoggerTrait {
        log as private;
    }
}

/*
별칭만 추가
*/
class UserService
{
	//기존 log()를 유지하면서 writeLog()라는 호출 이름을 추가한다. 둘 다 public이며 같은 구현을 실행한다.
    use LoggerTrait {
        log as writeLog;
    }
}

/*
별칭의 접근 범위까지 지정
*/
class UserService
{
    use LoggerTrait {
        log as private writeLog;
    }
}

 

 

Trait의 추상메서드

Trait에 추상 메서드를 선언하면, Trait을 사용하는 클래스가 제공해야 할 메서드를 지정할 수 있다.

=> Trait은 해당 메서드를 호출하는 공통 흐름을 구현하고, 구체적인 동작은 사용하는 클래스에 맡긴다

 

trait TraitName
{
	// abstract = 메서드 구현이 필요함을 선언하는 키워드
    abstract protected function requiredMethod(): string;

}

class ClassName
{
    use TraitName;

    protected function requiredMethod(): string
    {
        return "구현 결과";
    }
}

 

ex) 로그 출력 방식은 공통으로 사용하고, 로그에 표시할 서비스 이름은 각 클래스에서 정의하도록 함

trait LoggerTrait
{
    abstract protected function getLogName(): string;

    public function log(string $message): void
    {
        echo "[{$this->getLogName()}] {$message}\n";
    }
}

class UserService
{
    use LoggerTrait;

    protected function getLogName(): string
    {
        return "USER";
    }
}

class FileService
{
    use LoggerTrait;

    protected function getLogName(): string
    {
        return "FILE";
    }
}

$userService = new UserService();
$fileService = new FileService();

$userService->log("사용자 생성");
$fileService->log("파일 저장");
/*
[USER] 사용자 생성
[FILE] 파일 저장
*/

 

 


 

메서드 우선순위

클래스에 직접 정의한 메서드
            ↓ 우선
Trait에서 포함한 메서드
            ↓ 우선
부모 클래스에서 상속받은 메서드

 

※ 위 순서는 정상적으로 재정의할 수 있는 메서드를 기준으로 함. 

부모 메서드가 final이거나 선언이 호환되지 않는 경우에는 우선순위로 해결되지 않고 오류가 발생할 수 있음

 

 

 


 

주의점

 

이름충돌

여러 Trait을 포함하면 같은 이름의 메서드나 속성이 겹칠 수 있음

=> Trait을 추가할 때는 현재 클래스와 다른 Trait에 이미 같은 이름이 있는지 확인해야 함

 

숨겨진 의존 관계

Trait이 사용하는 클래스의 특정 속성이나 메서드를 당연히 존재한다고 가정하면, 

필요한 조건을 파악하기 어려워짐

=> 필요한 속성을 Trait에 선언하거나, 클래스별 값이 필요하다면 추상 메서드로 요구사항을 명시할 수 있음

 

과도한 기능 포함

클래스에 Trait을 많이 넣으면 실제로 어떤 속성과 메서드를 가지는지 확인하기 어려워질 수 있음

확인 항목 판단 기준
기능의 관련성 Trait 내부 기능들이 같은 역할에 속하는가?
사용 범위 포함한 클래스에서 실제로 필요한 기능인가?
코드 추적 메서드의 정의 위치를 쉽게 찾을 수 있는가?
변경 영향 Trait 수정이 사용하는 클래스들에 어떤 영향을 주는가?

 

객체 조합과의 구분

Trait은 클래스 정의에 구현을 포함하는 방식이다. 

실행 중에 Trait을 다른 Trait으로 교체하는 방식으로 사용하지 않음. 

=> 실행 시점에 구현을 선택하거나 교체해야 한다면, 별도의 객체를 전달받아 사용하는 구조가 더 합할 수 있음