라라벨 Facade 사용..

회사에서 Facade에 대해서 이야기 하면서 나온 사항들을 정리해봤습니다.

라라벨 Facade

라라벨이 어느 정도 인기를 얻자 여러 포럼에서 아래 사항들에 대해서 논쟁이 일어났다.

라라벨의 Facade는 Facade 패턴이 맞나?

라라벨의 Facade는 일반적으로 패턴 책에서 소개되는 Facade 패턴이 아니다. Taylor Otwell도 이 부분에 대해서 언급을 한 적이 있다. (https://twitter.com/taylorotwell/status/441393069531742208).

안티 패턴이다?

보통 포럼의 논쟁을 보면 안티 패턴이라고 단정 짓는 경우를 많이 보는데, Facade 그 자체로는 안티 패턴은 아니라고 생각한다.

패턴은 사용 방법에 따라서 안티 패턴의 여부가 결정되기 때문에 무조건 안티패턴 이라고 단정할 수는 없다.

Facade는 테스트가 불가능 하다?

라라벨의 Facade를 보면 모든 함수를 Static으로 호출한다. 보통 Static 함수는 테스트가 불가능하거나 어렵다.

Cache::get('something');

아래처럼 어디에도 의존하지 않는 pure function을 제외하고는 Static 함수는 해당 함수 안의 의존성을 제어하기가 힘들어서 독립된 유닛 테스트가 불가능하거나 까다롭다.

function sum($a, $b)
{
    return $a + $b;
}

모듈 또는 클라스로 프로그래밍하는 OOP에서는 여러 클라스가 서로 메세지를 주고받으며 의존을 하므로 각각의 클라스에서 기능을 분리해서 작성한다.

회원을 가입하는 간단한 서비스 클라스가 있다고 가정하자. 예제로 나오는 코드는 실행이 안 되는 가상 코드이다.

class UserService
{
    private $users;

    public function __construct(UserRepositoryContract $users)
    {
        $this->users = $users;
    }

    public function create($username, $password)
    {
        $password = new UserPassword($password);
        $user = new User($username, $password);
        $this->users->persist($user);

        return true;
    }
}

이 클라스에서 UserRepositoryContract의 persist() 함수는 그 구현체에 따라서 mysql로 회원 정보를 저장할 수도, 아니면 Mongo DB로 저장할 수도 있다.

UserService의 create()를 유닛 테스트 할 때는 create() 함수에서 UserRepositoryContract의 persist를 호출을 제대로 하는지에 대해서만 테스트 하면 된다. UserRepositoryCcontract이 DB에 접속 여부는, UserRepositoryContract 테스트하거나 Integration 테스트에서 하면 된다.

간단한 가상 테스트:

class UserServiceTest
{
    public function it_should_call_persist_on_user_repository()
    {
        $userRepository = m::mock(UserRepositoryContract::class);
        $userRepository->shouldReceive('persist')
                ->once()
                ->withInstanceOf(User::class);

        $userService = new UserService($userRepository);
        $userService->create('test@test.com', 'a123456');
    }
}

UserService를 Static으로 변경하면 어떨까?

class UserService
{
    public static function create($username, $password)
    {
        $users = new MysqlUserRepository();
        $password = new UserPassword($password);
        $user = new User($username, $password);
        $this->users->persist($user);

        return true;
    }
}

이 경우는 MysqlUserRepository를 외부에서 제어가 불가능 하므로 유닛 테스트가 사실상 어렵다.

이런 static 함수 안에서의 의존성 문제 때문에 많은 사람이 라라벨의 Facade는 테스트가 어렵다고 하지만 사실 라라벨의 Facade는 테스트가 굉장히 쉽다.

공식 문서 (https://laravel.com/docs/5.2/testing#mocking-facades) 에도 나와 있지만 라라벨의 Facade는 mocking을 기본적으로 지원한다.

한 마디로 라라벨 Facade가 테스트가 불가능하다는 말은 사실이 아니다.

테스트가 가능하지만..

테스트가 가능하지만 개인적으로 두 가지 이유 때문에 Facade는 최대한 사용을 줄여야 한다고 생각한다.

1. 모든 Facade의 기능은 Facade 없이도 사용가능하다

https://laravel.com/docs/5.1/facades#facade-class-reference 테이블을 보면 각각의 Facade가 연결된 클라스가 나와있다.

class aController extends Controller
{
    public function getSomething()
    {
        Cache::get('something');
    }
}

위의 코드는 아래처럼 변경할 수 있다.

class aController extends Controller
{
    private $cache;
    public function __construct(\Illuminate\Cache\Repository $cache)
    {
        $this->cache = $cache;
    }

    public function getSomething()
    {
        $this->cache->get('something');
    }
}
2. 접근성이 너무 쉽다.

좀 황당한 이유지만 Facade의 쉬운 접근성은 자칫 코드의 혼란을 일으킬 수 있다.
MVC에서 가장 기본이지만 많은 사람이 가장 안 지키는 것 중에 하나가 레이어의 구분이다.

MVC에서 M은 프레임워크에 따라오는 ORM을 확장한 클라스들이 아니라 하나의 레이어다.

가장 기본적으로 어플리케이션은 도메인 (혹은 모델)과 어플리케이션 레이어의 구분이 확실해야 한다. 바꾸어서 말하면 각각의 레이어가 서로의 의존도가 낮으면 낮을수록 좋다.

프레임워크를 포함해서 프레임워크에 종속되어 따로 패키지로 사용될 수 없는 기능들은 모두 어플리케이션 레이어로 취급된다.

도메인 레이어를 작성하면서 프래임워크에 종속된 기능들은 최대한 자제해야 하지만 라라벨의 Facade 같은 경우는 사용성이 쉽다 보니 아무런 제약없이 도메인 코드에서 사용되는 경우를 많이 본다.

프레임워크가 무엇이 됐든 프레임워크는 도구이다. 프래임워크는 이용 하는 것이지 프레임워크가 현제 작성 중인 코드의 방향을 결정해서는 안 된다.

이런 이유에서 Facade의 사용은 도메인 레이어랑 상관없는 어플리케이션 코드인 컨트롤러에서만 사용하고 있다.

comments powered by Disqus