왜 나는 PHP Traits를 피하는가 (그리고 대신 사용하는 것은)
I’m ready to translate the article for you, but I need the full text you’d like translated. Could you please paste the content (excluding the source line you already provided) here? Once I have the text, I’ll translate it into Korean while preserving all formatting, markdown, and code blocks.
트레이트가 실제로 무엇인가
트레이트는 단일 상속을 우회하기 위한 타협안으로 등장했습니다. 트레이트는 상속도 아니고, 컴포지션도 아니며, 인터페이스도 아닙니다. 요컨대, 트레이트는 명시적인 의존성 없이 코드를 클래스에 “붙이는” 방법입니다. 낮은 수준에서는 단순히 복사‑붙여넣기와 같은 방식으로 동작합니다.
왜 문제가 되는가
- 가독성 저하 –
use SomeTrait가 있는 클래스를 보면 그 클래스가 실제로 무엇을 하는지 알 수 없습니다. 트레이트를 열어 어떤 protected 메서드와 속성을 기대하는지 확인하고, 무언가를 오버라이드하는지 봐야 합니다. 동작이 명확하지 않게 됩니다. - 숨겨진 결합 – 트레이트는 종종 클래스의 protected 속성을 사용하거나 클래스가 직접 구현하지 않은 protected 메서드를 호출합니다. 이는 양방향의 암묵적인 결합을 만들며, 트레이트는 클래스 내부를 알고, 클래스는 트레이트 내부에 의존합니다. 이러한 관계는 생성자나 메서드 시그니처만으로는 알 수 없습니다.
- 캡슐화 파괴 – 트레이트는 클래스의 내부 상태에 접근하도록 장려합니다. 명확한 계약과 명시적인 의존성 대신 “트레이트는 어딘가에
$service가 존재하기를 기대한다”는 상황이 생깁니다. 내부 구조를 변경하면 트레이트가 깨집니다. - 테스트 어려움 – 트레이트는 인스턴스화할 수 없습니다. 테스트하려면 가짜 클래스를 만들고 모든 protected 의존성을 설정한 뒤 누락된 것이 없는지 확인해야 합니다. 이는 단위 테스트가 아니라 우회책에 불과합니다.
- 아키텍처 혼란 – PHP는 트레이트 안에 트레이트를 포함하고 하나의 클래스에 여러 트레이트를 사용할 수 있습니다. 이로 인해 다이아몬드 문제, 얽힌 의존성 체인, “동작은 하지만 어떻게 동작하는지 모르는” 코드가 쉽게 생깁니다.
PHP 8.x 개선 사항은 어떨까요?
PHP 8은 추상 메서드, 상수, 그리고 트레이트에서 정적 속성에 대한 변경을 추가했습니다. 안타깝게도 SOLID와 클린 아키텍처 관점에서는 이것이 상황을 악화시켰습니다—트레이트가 이제 상속 사고와 정적 상태에 더 깊이 관여하게 되었습니다.
전형적인 냄새
특성이 보호된 메서드를 가지고 클래스의 보호된 서비스를 사용한다면, 이는 거의 항상 별도의 객체가 누락되어 있어 해당 객체를 자체 클래스으로 추출해야 함을 의미합니다.
대신 사용하는 방법
대부분의 경우 해결책은 간단합니다:
- Dependency Injection – 의존성이 명시적이며, 생성자를 통해 코드를 읽을 수 있습니다.
- Composition – “uses” 대신 “has‑a”.
- Strategy / Factory – 특히 보호된 헬퍼 메서드 대신 사용합니다.
- 간단한 클래스나 함수 – 트레이트에 상태가 없으면 필요 없을 가능성이 높습니다.
이 모든 방법은 의존성을 드러내고, 테스트 용이성을 높이며, 아키텍처를 깔끔하게 유지합니다.
빠른 리팩터 예시
이전 (trait 사용)
trait NotifiableTrait
{
protected function notifyUser(string $message)
{
$this->notificationService->send($this->user, $message);
}
}
final class OrderCreateAction
{
use NotifiableTrait;
public function handle(OrderCreateDTO $dto)
{
// ...order logic...
$this->notifyUser('Order created!'); // where is this from?
}
}
이후 (DI 사용)
final class OrderCreateAction
{
public function __construct(private readonly UserNotifier $notifier) {}
public function handle(OrderCreateDTO $dto)
{
// ...order logic...
$this->notifier->send($user, 'Order created!'); // explicit and clear
}
}
이제 의존성이 보이고, 테스트 가능하며, 명시적입니다.
결론
Traits는 나중에 큰 리팩터를 초래하는 지름길인 경우가 많습니다. 동작을 별도의 객체로 추출할 수 있다면 추출해야 합니다. Trait에 상태가 없으면 존재할 필요가 없을 가능성이 높습니다. 저는 Trait을 금지하지는 않지만, 나중에 비용을 지불하지 않으려고 합니다.
References
- Doğan Uçar – PHP 트레이트 8.3: 새로운 기능들, 하지만 여전히 나쁜 개념
- Barry O’Sullivan – 내가 트레이트를 싫어하는 이유