해당 포스팅은 최범균 작가님의 도메인 주도 개발 시작하기 (P.170~177)를 읽고 정리한 글입니다.
도메인 구현과 DIP
아래 리포지토리는 DIP 원칙을 어기고 있다. 먼저 엔티티는 구현 기술인 JPA에 특화된 @Entity, @Table, @Id, Column 등의 애너테이션을 사용하고 있다. DIP에 따르면 @Entity, @Table은 구현 기술에 속하므로 Article과 같은 도메인 모델은 구현 기술인 JPA에 의존하면 안 된다.
@Entity
@Table(name = "article")
@SecondaryTable(
name = "article_content",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "id")
)
public class Article{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
리포지터리 인터페이스도 마찬가지다. 아래 코드에서 ArticleRepository 인터페이스는 도메인 패키지에 위치하는데 구현 기술인 스프링 데이터 JPA의 Repository 인터페이스를 상속하고 있다. 즉, 도메인 인프라에 의존하는 것이다.
public interface ArticleRepository extends Repository<Article, Long> {
void save(Article article);
Optional<Article> findById(Long id);
}
구현 기술에 대한 의존 없이 도메인을 순수하게 유지하려면 스프링 데이터 JPA의 Repository 인터페이스를 상속받지 않도록 수정하고 ArticleRepository 인터페이스를 구현한 클래스를 인프라에 위치시켜야 한다. 또한 Article 클래스에서 @Entity나 @Table과 같이 JPA에 특화된 애너테이션을 모두 지우고 인프라에 JPA를 연동하기 위한 클래스를 추가해야 한다.
특정 기술에 의존하지 않는 순수한 도메인 모델을 추구하는 개발자는 위 그림과 같은 구조로 구현한다. 이 구조를 가지면 구현 기술을 변경하더라도 도메인이 받는 영향을 최소화할 수 있다. DIP를 적용하는 주된 이유는 저수준 구현이 변경되더라도 고수준이 영향을 받지 않도록 하기 위함이다.
하지만 실제 개발에서 JPA로 구현한 리포지터리 구현 기술을 다른 기술로 또는 RDBMS를 NoSQL로 변경하는 경우는 드물다. 이렇게 변경이 거의 없는 상황에서 변경을 미리 대비하는 것은 과하다. 따라서 DIP를 완벽하게 지키면 좋겠지만 개발 편의성과 실용성을 가져가면서 구조적인 유연함은 어느 정도 유지해야 한다.
스프링 데이터 JPA를 이용한 조회 기능
CQRS는 명령(Command) 모델과 조회(Query) 모델을 분리하는 패턴이다. 명령 모델은 상태를 변경하는 기능을 구현할 때 사용하고 조회 모델은 데이터를 조회하는 기능을 구현할 때 사용한다.
엔티티, 애그리거트, 리포지터리 등 앞에서 살펴봤던 모델은 주문 취소, 배송지 변경과 같이 상태를 변경할 때 주로 사용된다. 즉 도메인 모델은 명령 모델로 주로 사용된다. 반면에 이 장에서 설명할 정렬, 페이징, 검색 조건 지정과 같은 기능은 주문 목록, 상품 상세와 같은 조회 기능에 사용된다. 즉, 이 장에서 살펴볼 구현 방법은 조회 모델을 구현할 때 주로 사용한다.
검색을 위한 스펙
검색 조건을 다양하게 조합해야 할 때 사용할 수 있는 것이 스펙(Specification)이다. 스펙은 애그리거트가 특정 조건을 충족하는지를 검사할 때 사용하는 인터페이스이다.
public interface Specification<T>{
public boolean isSatisfiedBy(T agg);
}
isSatisfiedBy() 메서드의 agg 파라미터는 검사 대상이 되는 객체다. 스펙을 리포지터리에 사용하면 agg는 애그리거트 루트가 되고, 스펙을 DAO에 사용하면 agg는 검색 결과로 리턴할 데이터 객체가 된다.
예를 들어 Order 애그리거트 객체가 특정 고객의 주문인지 확인하는 스펙은 다음과 같이 구현할 수 있다.
public class OrdererSpec implements Specification<Order> {
private String ordererId;
public OrdererSpec(String ordererId){
this.ordererId = ordererId;
}
public boolean isSatisfiedBy(Order agg){
return agg.getOrdererId().getMemberId().equals(ordererId);
}
}
리포지터리나 DAO는 검색 대상을 걸러내는 용도로 스펙을 사용한다. 만약 리포지터리가 메모리에 모든 애그리거트를 보관하고 있다면 다음과 같이 스펙을 사용할 수 있다.
public class MemoryOrderRepository implements OrderRepository {
public List<Order> findAll(Specification<Order> spec){
List<Order> allOrders = findAll();
return allOrders.stream()
.filter(order -> spec.isSatisfiedBy(order))
.toList();
}
}
// 검색 조건을 표현하는 스펙을 생성해서
Specification<Order> ordererSpec = new OrdererSpec("madvirus");
// 리포지터리에 전달
List<Order> orders = orderRepository.findAll(ordererSpec);
JPA의 Spec에 대해서 들어는 봤지만 실제 사용해 본 적은 없다. 이번 기회를 통해 적용해 보는 시간을 가지면 좋을 거 같다.
'📚 개발자의 서재 > 도메인 주도 개발 시작하기' 카테고리의 다른 글
[DDD] 스프링 데이터 JPA를 이용한 조회 기능 -3 (0) | 2025.05.09 |
---|---|
[DDD] 스프링 데이터 JPA를 이용한 조회 기능 -2 (0) | 2025.05.08 |
[DDD] 리포지터리와 모델 구현 -7 (0) | 2025.05.06 |
[DDD] 리포지터리와 모델 구현 -6 (0) | 2025.05.05 |
[DDD] 리포지터리와 모델 구현 -1 (0) | 2025.05.01 |