Laravel에서 Repository Pattern: 지저분한 코드를 정리하세요
Source: Dev.to

문제
이와 같은 컨트롤러를 본 적 있나요?
public class OrderController extends Controller
{
public function show($id)
{
$order = Order::with(['customer', 'items.product'])
->where('id', $id)
->first();
return response()->json($order);
}
public function getUserOrders($userId)
{
// Same query duplicated! 😱
$orders = Order::with(['customer', 'items.product'])
->where('customer_id', $userId)
->get();
return response()->json($orders);
}
}
문제점
- 🔴 모든 곳에서 중복된 쿼리
- 🔴 컨트롤러가 Eloquent에 강하게 결합됨
- 🔴 데이터베이스 없이는 테스트가 불가능함
- 🔴 비즈니스 로직이 데이터 접근과 뒤섞여 있음
솔루션: Repository 패턴
1단계 – 인터페이스 생성
interface OrderRepositoryInterface
{
public function find(int $id): ?Order;
public function findWithRelations(int $id): ?Order;
public function findByCustomer(int $customerId): Collection;
}
2단계 – Repository 구현
class OrderRepository implements OrderRepositoryInterface
{
protected $model;
public function __construct(Order $model)
{
$this->model = $model;
}
public function findWithRelations(int $id): ?Order
{
return $this->model
->with(['customer', 'items.product'])
->find($id);
}
public function findByCustomer(int $customerId): Collection
{
return $this->model
->with(['customer', 'items.product'])
->where('customer_id', $customerId)
->orderBy('created_at', 'desc')
->get();
}
}
3단계 – Service Provider에 등록
class RepositoryServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(
OrderRepositoryInterface::class,
OrderRepository::class
);
}
}
4단계 – 컨트롤러 정리
class OrderController extends Controller
{
protected $orderRepository;
public function __construct(OrderRepositoryInterface $orderRepository)
{
$this->orderRepository = $orderRepository;
}
public function show(int $id)
{
$order = $this->orderRepository->findWithRelations($id);
if (! $order) {
return response()->json(['message' => 'Not found'], 404);
}
return response()->json($order);
}
public function getUserOrders(int $userId)
{
$orders = $this->orderRepository->findByCustomer($userId);
return response()->json($orders);
}
}
Benefits
- ✅ 중복 없음 – 쿼리 로직이 한 곳에 존재
- ✅ 쉬운 테스트 – 데이터베이스 대신 리포지토리를 모킹
- ✅ 유연성 – 비즈니스 로직을 건드리지 않고 데이터 소스 전환
- ✅ 클린 코드 – 컨트롤러는 HTTP 관련만 담당
- ✅ 재사용성 – 동일 리포지토리를 컨트롤러, 잡, 커맨드 등에서 사용 가능
고급: 기본 저장소
abstract class BaseRepository
{
protected $model;
public function all(): Collection
{
return $this->model->all();
}
public function find(int $id): ?Model
{
return $this->model->find($id);
}
public function create(array $data): Model
{
return $this->model->create($data);
}
public function update(int $id, array $data): bool
{
return $this->model->find($id)?->update($data) ?? false;
}
public function delete(int $id): bool
{
return $this->model->find($id)?->delete() ?? false;
}
}
기본 저장소 확장
class ProductRepository extends BaseRepository
{
public function __construct(Product $model)
{
parent::__construct($model);
}
public function getFeatured(int $limit = 10): Collection
{
return $this->model
->where('is_featured', true)
->where('stock', '>', 0)
->limit($limit)
->get();
}
public function searchAndFilter(array $filters)
{
$query = $this->model->query();
if (!empty($filters['search'])) {
$query->where('name', 'like', "%{$filters['search']}%");
}
if (!empty($filters['min_price'])) {
$query->where('price', '>=', $filters['min_price']);
}
return $query->paginate(15);
}
}
테스트를 쉽게
리포지토리 없이
// Must set up entire database
$order = Order::factory()->hasItems(3)->create();
$response = $this->getJson("/api/orders/{$order->id}");
리포지토리와 함께
// Just mock the repository!
$orderRepo = Mockery::mock(OrderRepositoryInterface::class);
$orderRepo->shouldReceive('find')
->with(1)
->andReturn($mockOrder);
$this->app->instance(OrderRepositoryInterface::class, $orderRepo);
Common Pitfalls
❌ Don’t Return Query Builder
// Bad
public function getActive()
{
return $this->model->where('active', true); // Query builder!
}
✅ Do Return Concrete Results
// Good
public function getActive(): Collection
{
return $this->model->where('active', true)->get();
}
✅ Keep Repository for Data Access Only
// Good – Repository only handles data
public function create(array $data): Order
{
return $this->model->create($data);
}
// Business logic in Service
class OrderService
{
public function placeOrder(array $data): Order
{
$order = $this->orderRepository->create($data);
Mail::to($order->customer)->send(new OrderCreated($order));
return $order;
}
}
빠른 체크리스트
Before implementing the Repository Pattern, ask yourself:
- 내 컨트롤러가 데이터베이스 쿼리를 직접 수행하고 있나요?
- 같은 쿼리를 여러 곳에서 중복하고 있나요?
- 데이터베이스 없이 내 코드를 테스트하기가 어려운가요?
- Eloquent / Query Builder / Raw SQL 사이를 쉽게 전환하고 싶나요?
- 단순 CRUD 앱보다 더 복잡한 것을 만들고 있나요?
If you answered yes to 2+ questions, the Repository Pattern will help you!
결론
Repository 패턴은 단순한 CRUD 앱에 항상 필요한 것은 아니지만, 애플리케이션이 성장하면 매우 귀중해집니다. 이것은 다음을 제공합니다:
- 깨끗하고 테스트 가능한 코드
- 중앙 집중식 데이터 접근 로직
- 데이터 소스를 변경할 수 있는 유연성
- 더 나은 관심사의 분리
작게 시작하세요 – 가장 복잡한 모델부터 구현하고, 필요에 따라 확장하세요.
전체 심층 분석을 원하시나요?
이것은 요약본입니다! 전체 가이드를 보려면:
- ✨ 더 고급 예제 (캐싱, 서비스‑layer 통합)
- ✨ 실제 블로그 시스템 구현
- ✨ 완전한 테스트 전략
- ✨ 전자상거래 주문 관리 예제
Medium에서 전체 기사 읽기:
👉 Repository Pattern in Laravel: From Problem to Solution
더 많은 Laravel 팁을 원하시면 팔로우해주세요:
Repository Pattern에 대한 여러분의 경험은 어떠신가요? 좋아하시나요? 아니면 싫어하시나요? 댓글로 이야기 나눠요! 💬
