Sun's Blog

4장 리포지터리와 모델 구현 본문

Book/도메인 주도 개발 시작하기

4장 리포지터리와 모델 구현

버스는그만 2023. 11. 16. 23:30

4.1 JPA를 이용한 리포지터리 구현

4.1.1. 모듈 위치

리포지터리 인터페이스는 애그리거트와 같이 도메인 영역에 속하고, 리포지터리를 구현한 클래스는 인프라스트럭처 영역에 속한다. 각 타입의 패키지 구성은 아래와 같다.

팀 표준에 따라 리포지터리 구현 클래스를 domain.impl과 같은 패키지에 위치시킬 수도 있는데 이것은 리포지터리 인터페이스와 구현체를 분리하기 위한 타협안 같은 것이지 좋은 설계 원칙을 따르는 것은 아니다. 가능하면 리포지터리 구현 클래스를 인프라스트럭처 영역에 위치시켜서 인프라스트럭처에 대한 의존을 낮춰야 한다.

4.1.2 리포지터리 기본 기능 구현

리포지터리의 기본 기능은 다음 두 가지다.

  • ID로 애그리거트 조회(findById)
  • 애그리거트 저장(save)

인터페이스는 애그리거트 루트를 기분으로 작성한다. 주문에는 Order 루트 엔티티를 비롯해 OrderLine, Orderer, ShippingInfo 등 다양한 객체를 포함하는데, 이 구성요소 중에서 루트 엔티티인 Order를 기준으로 리포지터리 인터페이스를 작성한다.

ID 외에 다른 조건으로 애그리거트를 조회할 때에는 JPA의 Criteria나 JPQL을 사용할 수 있다. 

4.2 스프링 데이터 JPA를 이용한 리포지터리 구현

스프링과 JPA를 함께 적용할 때는 스프링 데이터 JPA를 사용한다. 스프링 데이터 JPA는 지정한 규칙에 맞게 리포지터리 인터피에스를 정의하면 리포지터리를 구현한 객체를 알아서 만들어 스프링 빈으로 등록해 준다. 리포지터리 인터페이스를 직접 구현하지 않아도 되기 떄문에 개발자는 리포지터리를 쉽게 정의할 수 있다.

4.3 매핑 구현

4.3.1 엔티티와 밸류 기본 매핑 구현

한 테이블에 엔티티와 밸류 데이터가 같이 있다면

  • 밸류는 @Embeddable로 매핑 설정한다.
  • 밸류 타입 프로퍼티는 @Embedded로 패밍 설정한다.

주문 애그리거트는 루트 엔티티는 Order이고 이 애그리거트에 속한 Orderer와 ShippingInfo는 밸류이다. 이 세 객체와 ShippingInfo에 포함된 Address 객체와 Receiver 객체는 한 테이블에 매핑할 수 있다. 루트 엔티티와 루트 엔티티에 속한 밸류는 한 테이블에 매핑할 때가 많다.

엔티티와 밸류가 한 테이블로 매핑

4.4 애그리거트 로딩 전략

JPA는 애그리거트 루트에서 연관 매핑의 조회 방식을 즉시 로딩(FetchType.EAGER) 혹은 지연 로딩(FetchType.LAZY)로 연관 매핑의 조회 방식을 설정할 수 있다.

  • 즉시 로딩: 애그리거트 루트를 로딩하는 시점에 모든 연관 객체를 로딩
  • 지연 로딩: 해당 연관 객체에 접근하는 시점에서 로딩

일반적으로 조회가 상태 변경보다 자주 있기 때문에 조회 성능을 위해 즉시 로딩을 사용하지만 이 경우 조회되는 데이터 개수가 많아지면 즉시 로딩 방식을 사용할 떄 성능(실행 빈도, 트래픽, 지연 로딩 시 실행 속도 등)을 검토해 봐야 한다.(애그리거트에 맞게 즉시로딩과 지연로딩을 선택해야 한다.)

4.5 애그리거트의 영속성 전파

애그리거트가 조회 뿐만 아니라 삭제, 변경 시 에도 하나로 처리해야 함을 의미한다. @Embeddable 매핑 타입은 함께 저장되고 삭제되므로 cascade 속성을 추가로 설정하지 않아도 된다. 반면에 @Entity 타입은 cascade 속성을 사용해서 저장과 삭제 시에 함께 처리되도록 설정해야 한다.

4.6 식별자 생성 기능

식별자는 크게 세 가지 방식 중 하나로 생성한다.

  • 사용자가 직접 생성
  • 도메인 로직으로 생성
  • DB를 이용한 일련번호 사용

이메일 주소처럼 사용자가 직접 식별자를 입력하는 경우 주체가 사용자이기 때문에 도메인 영역에 식별자 생성 기능을 구현할 필요가 없지만 식별자 생성 규칙이 있다면 엔티티를 생성할 때 식별자를 엔티티가 별도 서비스로 식별자 생성 기능을 분리해야 한다. 

    @Transactional
    public void createProduct(NewProductRequest req) {
    	// 식별자를 생성
        ProductId id = productService.nextId();
        Product product = new Product(id,  ...);
        productRepo.save(product);
    }

식별자 생성 규칙을 구현하기에는 리포지터리도 좋다.(오라클의 sequence로 생성 등) 또는 DB 자동 증가 칼럼을 식별자로 사용하면 식별자 매핑에서 @GeneratedValue를 사용한다.

4.7 도메인 구현과 DIP

현재 리포지터리는 DIP 원칙을 어기고 있다. 먼저 엔티티는 JPA에 특화된 @Entity, @Column 등의 애너테이션을 사용하고 있다. DIP에 따르면 @Entity, @Table 등은 구현 기술에 속하므로 도메인 모델은 구현 기술인 JPA에 의존하지 말아야 하는데 영속성 구현 기술인 JPA에 의존하고 있다. 즉 도메인이 인프라에 의존하는 것이다.

 구현 기술에 대한 의존 없이 도메인을 순수하게 유지하려면 JPA에 특화된 애너테이션을 모두 지우고 인프라에 JPA를 연동하기 위한 클래스를 추가해야 한다. DIP를 적용하는 주된 이유는 저수준 구현이 변경되더라도 고수준이 영향을 받지 않도록 하기 위함이다. 하지만 리포지터리와 도메인 모델의 구현 기술은 거의 바뀌지 않는다. DIP를 완벽하게 지키면 좋겠지만 개발 편의성과 실용성을 가져가면서 구조적인 유연함은 어느 정도 유지했다. 복잡도를 높이지 않으면서 기술에 따른 구현 제약이 낮다면 합리적인 선택이라고 생각한다.