레포지토리

레포지토리 패턴(Repository Pattern)

레포지토리는 도메인 주도 설계(Domain Driven Design, DDD)의 일부 개념으로 소개되었다. 레포지토리는 데이터를 저장하고 불러오는 로직을 담당하는 객체이다. 도메인 레이어는 레포지토리를 통해서 데이터베이스에 저장된 데이터와 별개로 단순하게 추상화된 객체를 반환 받을 수 있다. 이 덕분에 레포지토리를 사용하는 측에서는 데이터베이스 연결 상태나 SQL 구문 등에 대한 걱정 없이 아이템을 추가, 수정, 삭제하는 작업을 할 수 있다.

팩토리(Factory)와의 차이점

팩토리는 메모리 내에 객체 생성을 담당한다. 반면 레포지토리는 기존 객체의 재구성에 가깝다. 팩토리에서 생성 로직을 통해 생성된 객체가 레포지토리를 통해 저장되고, 이후 필요한 시점에 재구성 된다고 볼 수 있다.

레포지토리 선언과 정의

레포지토리 인터페이스는 도메인 계층(Domain Layer)에 선언하고 레포지토리 구현은 데이터 계층(Data Layer)에 정의한다. 이를 통해 도메인 계층의 도메인 로직 테스트를 쉽게 수행할 수 있고, 각 계층 내의 변화가 다른 계층에 미치는 영향을 줄일 수 있다.

레포지토리 구성 및 사용

이 부분에서는 Design the infrastructure persistence layer 문서에서 사용한 Order Aggregate를 예시로 사용한다.

Order Aggregate

프로젝트 레이아웃(Project Layout)

프로젝트 레이아웃는 다음과 같이 구성한다. 다만 해당 내용은 가이드를 위한 예시일 뿐이며, 기능 중심 패키징(Package by Layer)와 클린 아키텍처(Clean Architecture) 의존성 규칙(Dependency Rule)을 유지하는 선에서 자유로이 변경할 수 있다.

.
└── order
    ├── entity.go
    ├── repo
    │   └── repo.go     # Repository Implementation
    ├── repo.go         # Repository Interface
    ├── usecase.go
    └── valueobject.go

레포지토리 역할

데이터베이스는 레포지토리를 통해서만 업데이트 한다. 어그리게이트 루트(Aggregate Root) 별로 한 개의 레포지토리를 가지며, 레포지토리의 각 동작은 단위 작업(Unit of Work) 기준으로 구성한다. 이 때 일반적으로 단위 작업은 하나의 트랜잭션(Transaction)을 기준으로 생각할 수 있다. 엔티티, 밸류 오브젝트의 변경 사항은 도메인 레이어에서 수행되며, 레포지토리를 포함하는 데이터 레이어에서는 해당 변경 사항을 적용하는 것에 집중한다.

order/entity.go 안에 정의된 Order 엔티티는 어그리게이트 루트로서 존재하며, Order를 위한 레포지토리는 한 개만 존재한다. Order의 자식 엔티티인 OrderItem이나 밸류 오브젝트(Value Object)인 Address의 경우, 데이터베이스 테이블이 다를지라도 Order 레포지토리를 통해서 접근한다.

다중 데이터 소스(Multiple Data Source)

하나의 어그리게이트 루트를 사용하기 위해 여러 데이터 소스를 사용해야 할 경우가 존재한다. 레포지토리는 모델 주도 설계의 일부이며, 데이터베이스의 성격에 따라서 정의되지 않는다. 따라서 데이터베이스가 다중이라고 하더라도 그 때문에 레포지토리를 여러 개 정의하지는 않는다.

하나의 레포지토리에서 다중 데이터 소스를 사용하기 위해서 데이터 계층에 데이터 소스(Data Source)를 정의하여 사용할 수 있다. 레포지토리는 여러 데이터 소스로부터 받은 데이터를 조합하여 원하는 객체를 생성해야 한다. 예를 들어 Order 객체를 재생성 하기 위해 레포지토리 내부에서 MySQL 데이터와 Redis 데이터를 조합하는 로직이 존재할 수 있다.

객체 관계 매핑(Object Relational Mapping, ORM) 도구

관계형 데이터베이스를 사용할 때 객체 관계 매핑 도구를 사용할 수 있다. ORM 사용 시 테이블 매핑을 객체에 설정하는데, 종종 이 객체를 여러 모델에서 공통으로 접근해야 하기도 한다. 이 때 해당 매핑에 관련한 코드는 패키지 외부에서 독립적인 패키지에 정의하고, 레포지토리에서 해당 패키지를 사용하는 방식으로 구현 가능하다.

.
├── rdb
│   └── schema.go
└── order
    ├── repo
    │   └── repo.go
    ...

rdb/schema.go 파일 안에 ORM 관련 객체를 정의한다. order/repo/repo.go 파일 안에서 ORM 과 rdb 패키지를 사용해서 데이터베이스에 접근한 뒤 해당 데이터를 활용해서 엔티티를 재구성 할 수 있다.

schema.go

type Profile struct {
    ID        int64 `gorm:"primary_key"`
    // ...
}

repo.go

var p rdb.Profile
if err := r.db.First(&p).Error; err != nil {
    return nil, err
}

참고 자료