제네릭 없는 PHP 인터페이스

544 words

PHP를 사용하면서 가장 아쉬운 부분은 인터페이스다. PHP는 인터페이스를 지원하고 있고 이 인터페이스를 활용한 타입 힌트, 의존성 주입 등 다양한 방식으로 적용 가능하다. 하지만 제네릭 타입이 존재하지 않아서 타입 컬렉션 같이 재사용하기 좋은 인터페이스를 만들 수 없다.

물론 이 문제를 해결하기 위한 패키지를 찾아보면 존재하긴 한다. 하지만 인터페이스가 아닌 클래스 구현에 의존하고 있어서 타입 검사가 로직 내부에 포함되어 있다. 간략한 구현을 보면 대략 다음과 같다. 1

<?php
class Collection
{
    protected $typeName = null;
    protected $collection = [];
    public function __construct($typeName)
    {
        $this->typeName = $typeName;
    }
    public function add($item) {
        if (! in_array($this->typeName, class_implements($item))) {
            throw new \TypeMismatchException();
        }
        $this->collection[] = $item;
    }
}

로직 내에 위치한 타입 검사는 런타임에서만 구동되어 실제로 코드가 실행되기 전까지는 문제가 있어도 찾기가 힘들다. 이런 방식의 구현에서 내부적으로는 인터페이스를 사용해서 입력을 검증하고 있지만 결국 메서드의 유무를 확인하는 덕타이핑과 큰 차이가 없어진다. 결과적으로 인터페이스가 반 쪽짜리 명세로 남아있게 된다. 주석을 잘 달아서 다소 모호한 함수 시그니처를 이해하도록 설득해야 한다.

조금 다른 부분이긴 하지만 PHP에서는 Type을 위한 타입이 존재하지 않는 대신 string으로 처리하기 때문에 위 방식조차도 깔끔하게 느껴지지 않는다. 즉, 타입::class로 반환되는 값도 타입이 아닌 문자열이며 메서드 시그니처에 적용할 수도 없다.

물론 매번 인터페이스와 클래스를 작성해서 사용하는 방법도 있겠다.

<?php
interface CollectionVehicleInterface implements CollectionInterface
{
  public function add(VehicleInterface $item);
}
class CollectionVehicle implements CollectionVehicleInterface
{
  public function add(VehicleInterface $item) {
    $this->collection[] = $item;
  }
}

의도대로 인터페이스를 통해 함수의 입력을 명확하게 정할 수 있게 되었다. 인터페이스는 명세를 명확하게 나타낸다. 다만 이런 방식으로는 모든 경우의 수에 대해 직접 작성해야 하는 수고스러움이 있다. 내부 로직은 동일한데 결국 함수 시그니처가 달라지므로 비슷한 코드를 반복해서 작성해야 한다. 이런 문제를 해결하기 위해 제네릭을 활용할 수 있다.

<?hh
namespace Haruair;
interface CollectionInterface<T>
{
  public function add(T $item) : void;
}
class Collection<T> implements CollectionInterface<T>
{
  protected array<T> $collection = [];
  public function add(T $item) : void
  {
    $this->collection[] = $item;
  }
}
?>

hack에서의 제네릭은 항상 함수 시그니처를 통해서만 사용 가능하며 명시적 선언으로 바로 사용할 수 없어 다른 언어의 제네릭과는 조금 다르다. 물론 hack은 다양한 컬랙션을 이미 제공하고 있으며 array에서도 타입을 적용할 수 있다.

요즘 제대로 된 타입 시스템이 존재하는 프로그래밍 언어를 사용하고 싶다는 생각을 정말 많이 한다. 최근 유지보수하는 프로젝트는 제대로 된 클래스 하나 없이 여러 단계에 걸친 다중 배열로 데이터를 처리하고 있다. 배열에서 사용하는 키는 전부 문자열로 관리되고 있어서 키가 존재하지 않거나 잘못된 연산을 수행하는지 판단하기 어렵다. 어느 하나 타입을 통해 자료를 확인하는 법이 없어 일일이 값을 열어보고 확인하고 있다. 물론 지금 프로젝트의 문제가 엉성한 타입에서 기인한다고 보기에는 다른 문제도 엄청 많다. 그래도 PHP에 타입이 존재하는 이상 조금 더 단단하게 사용할 수 있도록 만들었으면 이런 상황에 더 좋은 대안을 제시할 수 있지 않았을까 생각이 든다.

PHP RFC를 보면 기대되는 변경이 꽤 많이 있는데 빈번히 통과되지 않는 기능이 많아 참 아쉽다. 이 제네릭의 경우도 그 중 하나다. 기왕 인터페이스도 있는데 이런 구현도 함께 있으면 좋지 않을까. 정적 타입 언어도 아닌데 너무 많은 것을 바라는건가 싶으면서도 인터페이스도 만들었으면서 왜 이건 안만들어주나 생각도 동시에 든다. 이렇게 딱히 대안 없는 불평글은 별로 쓰고싶지 않다 😕


  1. 이 코드는 실무에서 사용하기 어렵다. 가령 class_implements는 문자열로 전달한 경우에는 해당 문자열을 사용해 클래스 또는 인터페이스를 찾으므로 객체임을 확인하는 코드가 필요하다. 
haruair

사소한 이야기를 많이 나누고 싶어하는 해커. 티끌 같은 기술들이 세상을 바꾼다고 믿습니다.

Category : 개발 이야기
Tags: ,

4 responses to “제네릭 없는 PHP 인터페이스

  1. 실수에 의한 로직예외는 만드는게 라이브러리 같이 컨텍스트가 존재해서 컨텍스트 외부로 전달해줘야하는 상황이 아니라면 그냥 강제하는 처리 없애고 주석과 ide 도움을 받는게 더 여러모로 좋은것 같아요.

    1. 주제랑은 벗아난 소리였네여..지성. go에서 제네릭을 사용하지 않는데 안하는 이유가 걍 코드 더 써라 그게 더 단순하다 제네릭은 복잡하다 요지같거든요. 모나드도 그렇고 제네릭도 그렇고 타입과 타입을 활용하는 일은 단순함을 잃게 하는것 같습니다. 최근엔 실수를 줄이는 것만큼 단순함을 잃는 것도 지불해야할 비용이 아닌가 생각이 드네요.

      1. 저도 제네릭이 단순한 코드를 읽기 어렵게 만든다는 점에는 수긍하는 편입니다. 하지만 Swift나 C#의 타입 추론을 보면 타입을 암묵적으로 처리하는 것으로 양쪽의 장점을 흡수하려는 노력으로 생각 되네요. go의 경우는 제가 학습해본 바가 없어서 어떤 방식으로 이런 문제를 해결하는지 궁금합니다.

  2. 공감합니다. T 타입리턴만 유추해줘도 라라벨에서 콜렉션 쓸때 더 편할것 같습니다. 리턴타입 유추가 안되니 이건뭐다 외워서 코딩하는 수 밖에 없더라구요.

Leave a Reply