61호. 그것은 리포지토리 패턴이 아닙니다 free

2019-09-26

59호 단 30줄의 코드로 리포지토리 패턴을 구현하는 방법를 읽은 지인이 "이름만 리포지토리를 달고 있을 뿐 리포지토리 패턴이 아니다."라는 말씀을 해주셨습니다. 저도 막연히 엘로퀀트 모델에서 벗어나지 못하고 있는데 괜찮은걸까?라는 생각을 하긴했었어요. 그래서 이번 참에 리포지토리 패턴을 다시 공부해봤어요.


컬렉션 처럼 사용한다


리포지토리 패턴에 대해 다시 공부하면서 가장 먼저 눈에 들어온 단어는 컬렉션입니다.



리포지토리는 특정 타입의 모든 객체를 (대게 모방된) 하나의 개념적 집합으로 나타낸다. 더욱 정교한 질의 기능이 있다는 점을 제외하면 리포지토리는 컬렉션처럼 동작한다. 리포지토리에는 적절한 타입의 객체가 추가되고 제거되며, 이러한 리포지토리 이면에 존재하는 장치가 그러한 객체를 데이터베이스에 삽입하거나 삭제한다.

도메인 주도 설계 156p




도메인과 데이터 매핑 레이어를 중재하고 컬렉션과 비슷한 인터페이스를 사용해서 도메인 객체에 접근한다.

엔터프라이즈 애플리케이션 아키텍처 패턴 - 리포지토리



도메인 주도 설계와 엔터프라이즈 애플리케이션 아키텍처 패턴 두 책 모두에서 '컬렉션'을 언급합니다. 리포지토리를 사용하는 클라이언트 입장에서는 마치 컬렉션인것 처럼 사용한다는 것이 가장 큰 특징이라고 할 수 있을 것 같습니다. 라라벨에서 리포지토리의 클라이언트는 주로 컨트롤러의 매서드이거나, 서비스 레이어를 사용하는 경우 서비스 레이어라고 생각하시면 될 것 같습니다. 물론 두 도서는 라라벨 서적이 아니기 때문에 컬렉션은 라라벨의 컬렉션이 아닌 일반적인 의미의 컬렉션을 말하며, 자바에서는 목록성 데이터를 처리하는 자료구조를 통칭한다고 합니다(참고: [Java | 자바] Collection이란?(1) - 개요). 컬렉션은 데이터를 보관할 수 있고, 데이터를 추가, 제거, 수정, 검색 등을 할 수 있습니다(참고: 컬렉션이란). 아래 이미지는 자바에서의 자료구조 유형입니다.



아래는 컨트롤러 매서드에서 리포지토리를 컬렉션처럼 사용하는 느낌을 표현하기 위해 아주 간략하게 적어본 코드입니다.


// ProductController

public function index(ProductRepositoryInterface $productRepository)
{
$products = $productRepository->all();

return view('products.index', compact($products));
}

public function store(Request $request, ProductRepositoryInterface $productRepository)
{
$productRepository->add($request->all());

return redirect('products.index');
}

public function show($id, ProductRepositoryInterface $productRepository)
{
$product = $productRepository->first($id);

return redirect('products.show', compact($product));
}


전역적인 접근이 필요한 각 객체 타입에 대해 메모리상에 해당 타입의 객체로 구성된 컬렉션이 있다는 착각을 불러 일으키는 객체를 만든다. 잘 알려진 전역 인터페이스를 토대로 한 접근 방법을 마련하라. 객체를 추가하고 제거하는 메서드를 제공하고, 이 메서드가 실제로 데이터 저장소에 데이터를 삽입하고 데이터 저장소에서 제거하는 연산을 캡슐화하게 하라. 특정한 기준으로 객체를 선택하고 속성값이 특정 기준을 만족하는 완전히 인스턴스화된 객체나 객체 컬렉션을 반환하는 메서드를 제공함으로써 실제 저장소와 질의 기술을 캡슐화하라. 실질적으로 직접 접근해야 하는 AGGREGATE의 루트에 대해서만 리포지토리를 제공하고, 모든 객체 저장과 접근은 리포지토리에 위임해서 클라이언트가 모델에 집중하게 하라.

도메인 주도 설계 157p



도메인과 데이터 매핑 레이어를 중재한다


앞서 살펴본 엔터프라이즈 애플리케이션 아키텍처 패턴의 리포지토리 설명에 "도메인과 데이터 매핑 레이어를 중재한다"라는 표현이 나오는데, 도메인 주도 설계의 설명에서는 "리포지토리에는 적절한 타입의 객체가 추가되고 제거되며, 이러한 리포지토리 이면에 존재하는 장치가 그러한 객체를 데이터베이스에 삽입하거나 삭제한다."가 이에 해당합니다.


객체지향 프로그래밍으로 다루던 객체를 데이터베이스에 저장할 때는 테이블 형태로 적절히 변환해서 저장하고, 반대로 데이터베이스에 저장된 데이터를 객체지향 프로그래밍으로 다루기 위해서는 테이블 형태의 데이터를 객체 형태로 변환해줘야 합니다. 이를 수동으로 하려면 불편하기 짝이 없겠죠. 이때 도움을 주는 것이 데이터 맵퍼입니다. 라라벨에서는 엘로퀀트 ORM이 데이터 맵퍼 역할을 해주죠. 앞서 예제 코드는 사실 굳이 리포지토리를 안쓰고 다음과 같이 엘로퀀트 모델을 쓸 수도 있었습니다.


public function index()
{
$products = Product::all();

return view('products.index', compact($products));
}

결과는 같습니다. 그렇다면 굳이 리포지토리를 쓰는 이유가 뭘까요? 도메인 주도 설계에서 이야기 하는 리포지토리의 이점은 다음과 같습니다.



리포지토리에는 다음과 같은 이점이 있다.



  • 영속화된 객체를 획득하고 해당 객체의 생명주기를 관리하기 위한 단순한 모델을 클라이언트에게 제시한다.

  • 영속화 기술과 다수의 데이터베이스 전략, 또는 심지어 다수의 데이터소스로부터 애플리케이션과 도메인 설계를 분리해준다.

  • 객체 접근에 관한 설계 결정을 전해준다.(Communicates design decisions about object access)

  • 리포지토리를 이용하면 테스트에서 사용할 가짜 구현을 손쉽게 대체할 수 있다(보통 메모리상의 컬렉션을 이용).

    도메인 주도 설계 157p



사실 두번째 항목을 제외하고는 엘로퀀트 ORM을 직접 사용해도 얻을 수 있는 이점입니다. 엘로퀀트 ORM도 영속화된 객체를 다루는 쉬운 방법을 제공하고, '우리는 도메인 객체에 엘로퀀트 ORM으로 직접 접근할거야'라고 이야기할 수 있으며, 라라벨이 테스트 하기 좋게 손쉽게 가짜 구현으로 대체할 수 있도록 해주고 있습니다.


엘로퀀트 ORM은 4가지 데이터베이스를 지원하고 있어서 다수의 데이터베이스 전략을 지원하긴 합니다. 하지만 4가지 밖에 지원을 하지 않기 때문에 지원폭이 너무 좁습니다. 그리고 다수의 데이터소스는 지원하지 못합니다. 엘로퀀트 ORM을 직접 사용하는 대신 리포지토리 패턴을 사용하면 어떤 데이터베이스를 사용하던, 혹은 데이터베이스가 아닌 어떤 데이터소스를 사용하던 클라이언트 코드를 동일하게 유지할 수 있습니다.



리포지토리 구현은 영속화에 사용되는 기술과 인프라스트럭처에 따라 매우 다양해질 것이다. 이상적인 모습은 클라이언트에서 모든 내부 기능을 감춰서(비록 클라이언트 개발자에게는 그렇지 못하더라도), 데이터가 객체 데이터베이스나 관계형 데이터베이스에 저장되든, 아니면 단순히 메모리상에 상주하느냐에 관계없이 클라이언트 코드를 동일하게 유지하는 것이다. 리포지토리는 적절한 인프라스트럭처 서비스에 작업을 위임해서 작업을 완수할 것이다. 저장, 조회, 질의 메커니즘을 캡슐화하는 것은 리포지토리 구현의 가장 기본적인 기능이다.

도메인 주도 설계 160p



Laravel Eloquent Repositories가 리포지포리 패턴이 아닌 이유



리포지토리와 같은 것을 구현하기 전에 먼저 현재 사용 중인 인프라스트럭처, 특히 모든 아키텍처 프레임워크에 관해 곰곰히 생각해봐야 한다. 즉, 프레임워크에서 리포지토리를 만드는데 손쉽게 사용할 수 있는 서비스가 제공된다는 사실을 알게 되거나...

도메인 주도 설계 162p



위의 문구를 읽으면서 리포지토리 패턴의 데이터 맵핑 레이어로 엘로퀀트 ORM을 사용해도 상관없지 않나라는 생각이 들었습니다. 그러니 59호에서 소개했던 엘로퀀트 모델을 상속받아 리포지토리를 만드는 Laravel Eloquent Repositories도 문제 없을 것 같았는데 곰곰히 생각해보니 리포지토리 패턴을 구현한게 아니라는 결론을 내렸습니다.


그 이유는 바로 Laravel Eloquent Repositories로 만든 리포지토리 구현체가 순수한 도메인 모델이 아닌 엘로퀀트 모델을 상속받은 객체를 반환하기 때문입니다. 라라벨에서 일반적으로 모델이라고 부르는 것은 엘로퀀트 모델을 상속받기 때문에 엄격하게 말하면 도메인 모델은 아닙니다. 도메인 모델은 아무것에도 의존하지 않아야 하죠. 앞서 살펴봤듯이 리포지토리는 도메인과 데이터 맵핑 레이어 사이에서 동작하면서 도메인 모델을 반환해야 하는데 데이터 맵퍼를 반환한 셈입니다. 데이터 맵핑 레이어로 엘로퀀트 모델을 사용하는 건 상관없지만, 리포지토리 패턴을 제대로 구현하려면 도메인 모델을 반환하도록 한 단계를 더 거쳐서 내보냈어야 했던 것이죠.


// ProductController

public function index(ProductRepositoryInterface $productRepository)
{
$products = $productRepository->all();

return view('products.index', compact($products));
}

이를테면, Laravel Eloquent Repositories로 만든 리포지토리 구현체는 위의 코드를 실행하면 엘로퀀트 모델의 all() 매서드를 사용하게 되고 결과적으로 엘로퀀트 모델을 상속받은 클래스 객체의 컬렉션을 반환하게 됩니다. 도메인 모델을 반환하도록 만들기 위해 모든 엘로퀀트 모델 매서드를 오버라이드 해야한다면 이 패키지가 내세우는 장점(필요시 엘로퀀트 모델을 직접 사용해서 귀찮음을 더는)이 사라집니다.


appkr님의 리포지토리 데모를 보면 엘로퀀트 모델로 데이터를 가져와서 도메인 모델 컬렉션을 구성해서 내보내는 코드를 보실 수 있습니다.


// EmployeeDto
<?php
namespace App;
class EmployeeDto implements \JsonSerializable
{
private $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
public function jsonSerialize()
{
return [
'name' => $this->name,
];
}
}

// EloquentEmployeeRepository
<?php
namespace App;
use Illuminate\Support\Collection;
class EloquentEmployeeRepository implements EmployeeRepository
{
private $employeeModel;
public function __construct(Employee $employeeModel)
{
$this->employeeModel = $employeeModel;
}
/**
* @return Collection|EmployeeDto[]
*/
public function listEmployees(): Collection
{
return $this->employeeModel->all()->map(function (Employee $employee) {
return new EmployeeDto($employee->name);
});
}
}

이 코드에서 보듯이 엘로퀀트 모델을 리포지토리에 주입해서 쓰는게 리파지토리에서 엘로퀀트 모델을 활용하는 더 깔끔한 해결책이 아니었나 싶습니다. Appkr님의 저장소 패턴을 도입하는 과정을 점진적으로 보여주는 식으로 구성해두었으니 참고하시면 도움이 되실 겁니다.


마치며


엘로퀀트 모델 대신 일일이 도메인 모델을 만들어서 사용하는 것은 여간 귀찮은 일이 아닙니다. 라라벨 제작자가 도메인 주도 설계 개념이 없어서 도메인 모델 레이어를 두지 않고 엘로퀀트 모델을 사용하게 했다고 생각하진 않습니다. 생산성이나 편리성을 높이기 위한 결정이었을 겁니다. 그러한 결정을 무릎쓰고 리포지토리 패턴을 쓰기로 마음 먹었다면 다소 귀찮더라도 제대로(도메인 모델도 만들고, 리포지토리가 도메인 모델을 반환하도록) 하는 편이 낫겠죠. 어설프게 흉내만 내는건 별로인것 같아요.


독자께서 피드백을 주신 덕분에 리포지토리에 대해 다시 한 번 공부하는 계기가 되었습니다. 이번 글도 그렇고 앞으로도 제가 잘못 이해하고 쓴 부분이 있다면 피드백 부탁드려요!


1일 1식 라라벨 61호

2019년 9월 26일

유료 구독자 전용 레터입니다.

한 달 1만원으로 매일 라라벨 관련 메일 받아보시고 과거 메일도 열람하세요. 일반 구독으로 공개글만 받아보실 수도 있습니다.

구독하기 버튼을 눌러주시면 구독과 동시에 xly에도 가입됩니다.

이현석

바쁜 팀장님 대신 알려주는 신입 PHP 개발자 안내서를 쓰고, 클린 아키텍처 인 PHP를 번역했습니다. 2020년에 출간될 Laravel Up & Running 2nd Edition을 번역하고 있습니다.