Menu
Coddy logo textTech

레포지토리 패턴

Coddy PHP 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 91개 중 81번째.

레포지토리 패턴(Repository Pattern)은 애플리케이션의 비즈니스 로직과 데이터 저장소 사이를 중재하는 디자인 패턴입니다. 이는 도메인 객체에 접근하기 위한 컬렉션과 유사한 인터페이스를 제공하며, 데이터가 실제로 저장되거나 검색되는 방식에 대한 세부 사항을 숨깁니다.

저장소(repository)를 객체를 유지하고 검색하는 방법을 알고 있는 특화된 컬렉션이라고 생각해보세요. 애플리케이션 코드는 데이터가 데이터베이스, 파일 또는 API 중 어디에 있는지 알 필요 없이, 저장소에 ID로 사용자를 요청하거나 새로운 제품을 저장합니다. 이러한 분리는 코드를 더 테스트 가능하고 유연하게 만듭니다.

<?php
interface UserRepositoryInterface {
    public function find(int $id): ?User;
    public function findAll(): array;
    public function save(User $user): void;
    public function delete(int $id): void;
}

class User {
    public function __construct(
        public int $id,
        public string $name,
        public string $email
    ) {}
}

리포지토리 구현은 실제 저장 메커니즘을 처리합니다:

<?php
class InMemoryUserRepository implements UserRepositoryInterface {
    private array $users = [];
    
    public function find(int $id): ?User {
        return $this->users[$id] ?? null;
    }
    
    public function findAll(): array {
        return array_values($this->users);
    }
    
    public function save(User $user): void {
        $this->users[$user->id] = $user;
    }
    
    public function delete(int $id): void {
        unset($this->users[$id]);
    }
}

$repo = new InMemoryUserRepository();
$repo->save(new User(1, "Alice", "alice@example.com"));
$repo->save(new User(2, "Bob", "bob@example.com"));

echo $repo->find(1)->name . "\n";
echo count($repo->findAll());

출력:

Alice
2

이 패턴의 장점은 비즈니스 로직을 변경하지 않고도 구현을 교체할 수 있다는 점입니다. 인메모리 저장소에서 데이터베이스로 전환해야 합니까? 동일한 인터페이스를 구현하는 새로운 리포지토리 클래스를 생성하기만 하면 됩니다. 애플리케이션 코드는 구체적인 구현이 아닌 인터페이스에 의존하기 때문에 수정할 필요가 없습니다.

challenge icon

챌린지

쉬움

리포지토리 패턴(Repository Pattern)을 사용하여 제품 인벤토리 시스템을 구축해 보겠습니다. 도메인 객체와 저장 방식 사이에 명확한 분리를 만들어, 애플리케이션 코드가 저장소의 세부 사항을 알지 못해도 제품을 다룰 수 있도록 할 것입니다.

코드는 다음 네 개의 파일로 구성됩니다:

  • Product.php — 인벤토리의 항목을 나타내는 Product 클래스를 생성합니다. 각 제품은 id (int), name (string), price (float)를 가집니다. 쉬운 접근을 위해 public 속성을 가진 생성자 프로모션(constructor promotion)을 사용하세요.
  • ProductRepositoryInterface.php — 모든 제품 저장소 구현체에 대한 계약을 설정하는 ProductRepositoryInterface를 정의합니다. 인터페이스는 다음 메서드들을 선언해야 합니다:
    • find(int $id): ?Product — ID로 제품을 조회하며, 찾지 못하면 null을 반환합니다.
    • findAll(): array — 모든 제품을 배열로 가져옵니다.
    • save(Product $product): void — 제품을 저장하거나 업데이트합니다.
    • delete(int $id): void — ID로 제품을 삭제합니다.
  • InMemoryProductRepository.php — Product 클래스와 인터페이스를 모두 포함합니다. 제품을 저장하기 위해 프라이빗 배열을 사용하여 인터페이스를 구현하는 InMemoryProductRepository 클래스를 생성합니다. 효율적인 조회를 위해 제품은 ID를 키로 인덱싱되어야 합니다. findAll() 메서드는 제품 객체들만 반환해야 합니다 (배열 키를 재설정하기 위해 array_values()를 사용하세요).
  • main.php — 리포지토리 파일을 포함합니다. 두 개의 입력을 받게 됩니다: 추가할 제품 배열이 포함된 JSON 문자열과 검색할 ID입니다.

    JSON 형식은 다음과 같습니다:

    [{"id": 1, "name": "Laptop", "price": 999.99}, {"id": 2, "name": "Mouse", "price": 29.99}]

    InMemoryProductRepository를 생성하고, JSON 입력의 모든 제품을 저장한 다음, 두 번째 입력(ID)을 사용하여 특정 제품을 찾으세요. 결과는 다음 형식으로 출력합니다:

    Total products: [count]
    Found: [name] - $[price]

    가격은 소수점 둘째 자리까지 형식을 맞추세요. 제품을 찾을 수 없는 경우, 대신 Found: Not found를 출력하세요.

리포지토리 패턴은 저장소 구현을 교체해야 할 때 진가를 발휘합니다. 메인 코드는 인터페이스에만 의존하므로, 인메모리 저장소에서 데이터베이스로 전환하더라도 애플리케이션 로직을 변경하지 않고 새로운 리포지토리 클래스를 생성하기만 하면 됩니다.

치트 시트

레포지토리 패턴(Repository Pattern)은 비즈니스 로직과 데이터 저장소 사이를 중재하며, 저장소 구현 세부 사항을 숨기면서 도메인 객체에 접근하기 위한 컬렉션과 유사한 인터페이스를 제공합니다.

데이터 접근을 위한 계약을 설정하는 레포지토리 인터페이스를 정의합니다:

<?php
interface UserRepositoryInterface {
    public function find(int $id): ?User;
    public function findAll(): array;
    public function save(User $user): void;
    public function delete(int $id): void;
}

생성자 프로모션(constructor promotion)을 사용하여 도메인 클래스를 생성합니다:

<?php
class User {
    public function __construct(
        public int $id,
        public string $name,
        public string $email
    ) {}
}

구체적인 저장 메커니즘을 사용하여 레포지토리 인터페이스를 구현합니다:

<?php
class InMemoryUserRepository implements UserRepositoryInterface {
    private array $users = [];
    
    public function find(int $id): ?User {
        return $this->users[$id] ?? null;
    }
    
    public function findAll(): array {
        return array_values($this->users);
    }
    
    public function save(User $user): void {
        $this->users[$user->id] = $user;
    }
    
    public function delete(int $id): void {
        unset($this->users[$id]);
    }
}

인터페이스를 통해 레포지토리를 사용합니다:

$repo = new InMemoryUserRepository();
$repo->save(new User(1, "Alice", "alice@example.com"));
$user = $repo->find(1);
$allUsers = $repo->findAll();

이 패턴을 사용하면 비즈니스 로직을 변경하지 않고도 구현을 교체할 수 있습니다. 즉, 코드가 구체적인 구현이 아닌 인터페이스에 의존하게 됩니다.

직접 해보기

<?php

require_once 'InMemoryProductRepository.php';

// 입력 읽기
$jsonInput = trim(fgets(STDIN));
$searchId = intval(trim(fgets(STDIN)));

// JSON 입력을 배열로 파싱
$productsData = (array)json_decode($jsonInput, true);

// TODO: InMemoryProductRepository 인스턴스 생성

// TODO: $productsData를 루프하며 각 제품을 저장
// 각 항목은 'id', 'name', 'price' 키를 가짐

// TODO: 모든 제품을 가져와서 총 개수를 출력
// 형식: "Total products: [count]"

// TODO: $searchId로 제품 찾기
// 찾은 경우 출력: "Found: [name] - $[price]" (가격은 소수점 2자리까지 표시)
// 찾지 못한 경우 출력: "Found: Not found"

?>
quiz icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

객체 지향 프로그래밍의 모든 레슨