개발을 진행하면서 Repository는 데이터베이스와의 연동을 담당하는 핵심 구성 요소로 사용됩니다.
일반적으로 Spring Data JPA를 사용하여 JpaRepository가 있죠. 이를 상속받아 필요한 DB CRUD 기능을 구현하게 됩니다.
그렇다면 Domain Driven Design(DDD)에서는 이러한 Repository 클래스들이 어느 계층에 위치해야 할까요? 그리고 어떤 구조로 구현하는 것이 이상적일까요?
이 글에서는 Repository에 대한 의존성 역전(DIP)을 구현하는 방법과 그 이유에 대해 자세히 다루고자 합니다.
Repository
Repository는 애플리케이션에서 데이터 접근 계층을 담당하는 핵심 구성 요소이다.
데이터베이스와의 상호 작용을 추상화하여 비즈니스 로직이 데이터 저장소의 세부 구현에 의존하지 않도록 한다.
예를 들어, Spring Data JPA를 사용하면 JpaRepository 인터페이스를 상속받아 기본적인 CRUD(Create, Read, Update, Delete) 기능을 손쉽게 구현할 수 있다.
Domain Driven Design에서 Repository
Domain Driven Design(DDD)은 복잡한 도메인 지식을 소프트웨어 모델로 표현하는 방법론이다.
DDD에서 Repository는 도메인 계층의 한 부분으로 간주되며, Aggregate Root를 저장하고 조회하는 역할을 한다.
위 그림과 같이, 도메인은 가장 상위 수준의 계층으로서 비즈니스 로직의 중심을 이루며, 계층이 멀어질수록 하위 수준의 계층(예: 인프라스트럭처)이 된다. 여기서 Repository가 도메인 계층의 일부로 간주되는 이유는 다음과 같다
- 도메인 순수성 유지: 도메인 계층은 데이터베이스나 특정 기술 스택에 의존하지 않고, 비즈니스 로직을 순수하게 유지해야 한다. Repository는 도메인 모델을 조작하는 인터페이스를 정의하여, 도메인 로직이 데이터 접근 세부 사항에 의존하지 않도록 한다. 이로써 도메인 모델의 변경이 데이터베이스나 기술 변화에 종속되지 않게 된다.
- Aggregate 관리: DDD에서 Aggregate Root는 관련된 도메인 엔티티를 일관되게 관리하는 집합 단위이다. Repository는 Aggregate Root를 관리하며, 데이터 저장소와의 상호작용을 통해 Aggregate의 상태를 안정적으로 유지하도록 돕는다. 이는 데이터의 일관성과 무결성을 보장하는 데 필수적이다.
- 비즈니스 로직의 응집력: Repository는 데이터베이스 접근을 추상화하여 비즈니스 로직이 본래의 책임에 집중할 수 있게 한다. 이를 통해 도메인 계층의 응집력이 높아지고, 비즈니스 규칙과 요구사항에 더 밀접하게 맞춘 모델을 구현할 수 있다.
의존성 역전 원칙 (Dependency Inversion Principle)
의존성 역전 원칙(DIP)은 상위 수준의 모듈이 하위 수준의 모듈에 의존해서는 안 되며, 추상화에 의존해야 한다는 원칙이다. 이는 시스템의 유연성과 확장성을 높여준다.
특히, Domain Driven Design(DDD) 에서는 각 계층이 서로의 역할에 집중하여 단일 책임 원칙을 지키며 도메인 로직이 데이터 접근이나 구현 기술에 종속되지 않도록 의존성 역전이 필요하다.
계층별로 의존성을 역전하여 구현하면, 시스템의 복잡도가 높아질수록 계층 간의 결합도를 낮출 수 있어 유지보수성이 개선되고 테스트가 용이해진다.
또한, 도메인 로직의 순수성을 유지하며, 시스템의 변경에 유연하게 대응할 수 있는 구조가 된다.
예를 들어, 아래 클래스는 Spring을 개발하면서 흔히 볼 수 있는 Service와 Repository이다.
public interface CustomerRepository extends JpaRepository<Customer, Long> {
void save(Customer customer);
Optional<Customer> findById(Long id);
}
@Service
public class CustomerService {
private final CustomerRepository customerRepository;
@Autowired
public CustomerService(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
public void createCustomer(Customer customer) {
customerRepository.save(customer);
}
Service에서 Repository의 의존성을 주입 받고 있다. 일반적인 상황에선 문제가 없지만, 위 예시는 Domain Driven Design을 위반하고 있다.
DDD에서 Service 클래스는 일반적으로 application 계층에 있으며, JpaRepository의 구현체는 infrastrucure에 위치해야 한다.
그런데 상위 계층인 application 계층이 하위 계층인 infrastructure에 의존하고 있다.
따라서 DDD를 준수하여 설계하기 위해선, 두 의존성 간의 역전이 필요하다.
Repository 의존성 역전 구현
아래의 구조를 따라서 구현을 한다.
최상위 계층인 domain에서 하위 계층에서 사용할 repository 메서드를 정의하고, infrastructure 계층의 interface에 구현을 한다.
domain/DeliveryRepository
이 인터페이스는 하위 계층에서 사용할 메서드들을 선언한다. 특히, Spring Data Repository에서 사용하는 메서드로 정의한다.
public interface DeliveryRepository {
<S extends Delivery> S save(S entity);
Optional<Delivery> findById(UUID deliveryId);
boolean existsById(@Nonnull UUID deliveryId);
void deleteById(UUID deliveryId);
}
infrastructure/DeliveryCassandraRepository
실제 JPA와 같은 구현체 인터페이스로, domain 계층의 Repository를 상속받는다.
참고로 상속 받은 Repository의 이름과 Spring Data Repository에서 지원하는 메서드와 동일한 이름으로 사용하면, 결과적으로 Spring Data Repository의 기능을 사용한다.
public interface DeliveryCassandraRepository extends DeliveryRepository, CassandraRepository<Delivery, UUID> {
@Override
<S extends Delivery> @Nonnull S save(@Nonnull S entity);
@Override
@Nonnull Optional<Delivery> findById(@Nonnull UUID deliveryId);
@Override
boolean existsById(@Nonnull UUID deliveryId);
@Override
void deleteById(@Nonnull UUID deliveryId);
}
예를 들어 위 코드에서 DeliveryRepository에서 선언한 save와 CassandraRepository의 save와 메서드 이름과 타입을 같게 설정하면, CassandraRepository의 save를 사용한다.
(물론 Spring Data JpaRepository도 마찬가지다.)
application/DeliveryApplicationService
application 계층에서 비즈니스 로직을 수행하는 Service 클래스로, infrastructure의 repository가 아닌, domain 계층의 repository를 선언한다.
@Slf4j
@Service
@RequiredArgsConstructor
@Validated
public class DeliveryApplicationService {
private final DeliveryRepository deliveryRepository;
...
위 설계를 통해, 애플리케이션 계층이 도메인 계층의 Repository 인터페이스에 의존하게 되어, 상위 계층(즉, 도메인 계층)의 추상화에만 의존하도록 한다.
이는 애플리케이션 계층이 데이터 접근 방식이나 infrastructure 구현에 종속되지 않고, 오로지 비즈니스 로직과 관련된 인터페이스에 의존하도록 하여 DDD의 원칙과 일치한다.
정리
- DDD 에서는 상위 계층이 하위 계층의 구체적인 구현에 의존하지 않도록 설계하는 것이 권장된다.
- 왜냐하면 상위 계층(예: 애플리케이션 계층이나 도메인 계층)이 하위 계층(예: 인프라스트럭처 계층)의 구체적 구현을 직접 참조하게 되면, 비즈니스 로직이 데이터베이스나 외부 시스템의 변경에 영향을 받을 가능성이 커지기 때문이다.
- 따라서 의존성 역전 원칙과 결합하여, 상위 수준의 모듈(비즈니스 로직)이 하위 수준의 모듈(데이터 접근 로직 등)에 의존하지 않고, 추상화에 의존하도록 설계해야 한다.
- 이 글에서는 애플리케이션 계층의 서비스 클래스에서 도메인 계층의 repository를 의존하고, 구현체 repository는 infrastructure에 위치시켜 직접적으로 하위 계층의 구현체에 의존하지 않도록 의존성 역전을 달성했다.