객체지향

[DDD, 도메인 주도 개발] 4.리포지터리와 모델 구현

마디니 2023. 3. 28. 22:51
반응형

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

레포지토리 인터페이스는 도메인 영역에, 레포지토리 구현 클래스는 인프라스트럭처 영역에 속한다.

레포지토리는 기본적으로  ID로 애그리거트 조회 하기애그리거트 저장하기 두개의 기능을 제공한다.

그외에도 다음과 가능 기능을 제공할 수 있다.

  • ID외에 다른 조건으로 조회
    • JPA의 Criteria나 JPQL(Java Persistence Query Language) 사용
  • 애그리거트 삭제
    • 애그리거트 객체를 파라미터로 받음:  delete(Order order);
    • 요구사항이 삭제이더라도, 데이터를 바로 삭제하기보다는 삭제 플래그로 처리하여 일정기간 보관하는 것도 좋음

[SpringDataJPA 구현]

스프링 데이터 JPA는 다음 규칙에 따라 작성한 인터페이스를 찾고, 그 인터페이스의 구현체를 스프링 빈으로 등록해준다.

  • org.springframework.data.repository.Repository<T,ID> 인터페이스 상속 
  • T는 엔티티 타입, ID 는 식별자 타입
  • 지정한 규칙에 맞게 메서드를 작성하면 된다.

매핑 구현

엔티티와 밸류 매핑

  • 애그리거트 루트는 엔티티이므로 @Entity
  • 밸류는 @Embeddable 매핑, 밸류 타입 프로퍼티는 @Embedded 로 매핑
  • 엔티티와 밸류의 생성자는 객체를 생성할 때 필요한 것을 전달 받는다. -> set메소드 제공하지 않기
  • JPA는 기본생성자를 사용해 DB데이터와 매핑해주기 때문에 이때는 기본 생성자가 필요하다. -> protected접근자로 외부 에서 기본생성자를 사용지 못하게 한다.
  • JPA의 매핑 방식은 두가지가 있음
    • 필드 방식: @Access(AccessType.PROPERTY), get/set 메소드가 필요
    • 프로터티 방식 :  @Access(AccessType.PROPERTY)
    • 명시적으로 지정하지 않으면 @Id나 @EmbeddedID가 어디에 위치했느냐에 따라 접근방식을 결정함
  • AttributeConverter
    • 밸류 타입과 DB의 타입이 다를 경우 변환을 처리하기 위한 기능을 정의한다.
    • autoApply = true 로 지정하면 지정된 밸류타입 <-> DB 타입관 변환이 자동적용 된다. 
    • autoApply = false(디폴트) 로 지정하면 프로퍼티를 변환하고 싶을 때 @Converter을 이용해 변환에 쓸 컨버터를 지정해주면 된다.

밸류 컬렉션 매핑

  • 밸류 컬렉션을 별도의 테이블로 매핑할 때는 (예) Order, OrderLine), @ElementCollection 과 @CollectionTable을 함께 사용한다.
  • 밸류 컬렉션: 한개 컬럼을 매핑 할 때
    • 도메인 모델에서는 이메일 주소를 목록으로 보관하지만 디비에 저장할 때는 콤마(,) 로 구분해 한개 컬럼으로 저장할때
    • AttributeConverter을 사용하면 된다.  단, 새로운 밸류 타입이 필요하다.
  • 밸류를 이용한 ID 매핑
    • 식별자를 밸류타입으로 만들어 사용한다면  @Id 대신 @EmbeddedId 를 사용한다.
    • 식별자를 밸류타입으로 지정해주면 식별자에 기능을 추가 할 수 있다. 
    • 식별자 밸류타입에서는 엔티티를 비교할 때 사용하는 equals(), hashcode()  를 적절하게 구현해줘야 한다.
  • 별도 테이블에 저장하는 밸류 매핑
    • 테이블에 저장한다고 해서 그것이 엔티티가 되는 것은 아님(테이블과 엔티티/밸류의 개념은 다름)
    • 엔티티와 밸류를 구분하는 방법은 고유 식별자를 갖는지 갖지 않는지 보는 것(PK와 는 다른 것)
    • @SecondaryTable 사용
      • 엔티티 조회시 조인을 이용해 밸류 데이터도 함께 조회해온다. 
      • 밸류를 @Entity로 지정하고 지연로딩 시키기 ? ->  밸류모델을 엔티티로 만드는 것이라 좋은 방법은 아님 
    • 밸류 컬렉션을 @Entity로 매핑하기
      • 구현 기술의 한계나 팀 표준 때문에 개념은 밸류이지만 @Entity를 사용할 때도 있다.
      • 예) 상속이 필요한 경우 - @Embeddable 타입의 클래스 상속 매핑은 불가
      • @Inheritance(starategy - InheritanceType.SINGLE_TABLE) , @DiscriminatorColumn를 이용해 매핑 설정 -> 샘플코드1 참조
      • @Entity 지만 상태변경 메소드 제공X
    • @Entity로 매핑한 밸류를 컬렉션으로 매핑하기 -> @OneToMany사용
      • @OneToMany 매핑에서 컬렉션의 clear()를 호출 하면 N+1번 delete 쿼리가 나간다 -> 변경 빈도가 높다면 성능상 비효율적
      • @Embeddable 타입의 컬렉션의 clear()를 호출하면 한번의 delete 쿼리로 삭제처리를 수행한다. -> 성능이 좋아지기 때문에 상속을 포기하고 단일 클래스로 구현 -> 클래스 내부 if분기
  • 애그리거트간 집합연관 (M-N)
    • 성능상의 이유로 피하면 좋지만 불가피한 경우 ID참조를 이용한 단방향 집합 연관을 적용한다.
    • @ElementCollection 사용 - 삭제할 때 매핑에 사용된 조인 데이터도 함께 삭제된다.

(샘플코드1)

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) //단일테이블 전략
@DiscriminatorColumn(name = "image_type")
@Table(name = "image")
public abstract class Image {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "image_id")
    private Long id;

    @Column(name = "image_path")
    private String path;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "upload_time")
    private Date uploadTime;

    protected Image() {}
    public Image(String path) {
        this.path = path;
        this.uploadTime = new Date();
    }

    protected String getPath() {
        return path;
    }

    public Date getUploadTime() {
        return uploadTime;
    }
...(중략)..
}

//image_type == II 일때
@Entity
@DiscriminatorValue("II")
public class InternalImage extends Image {
.....
}

//image_type == EI 일때
@Entity
@DiscriminatorValue("EI")
public class ExternalImage extends Image {
.....
}

 


애그리거트 로딩전략과 영속성 전파

"애그리거트는 완전한 상태여야 한다"

-> 저장시 애그리거트에 속한 모든 객체를 저장해야 한다.

-> 삭제시 애그리거트에 속한 모든 객체를 삭제해야 한다.

 

@Embeddable 매핑 타입은 기본적으로 함께 저장되고 삭제 된다.

@Entity를 이용한 매핑은 cascade 속성을 사용해 저장과 삭제를 함께 처리하게 해야한다.

@Entity
@Table(name = "product")
public class Product {
  ...
 
  // Product를 저장/삭제 할 때 Image도 함께 저장/삭제된다.
  // 리스트에서 Image 객체를 제거하면 DB에서 함께 삭제되도록 orphanRemoval도 true로 설정한다.
  @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true)
  @JoinColumn(name = "product_id")
  @OrderColumn(name = "list_idx")
  private List<Image> images = new ArrayList<>();
  ...
  
  public void changeImages(List<Image> newImages) {
    images.clear();
    images.addAll(newImages);
  }
}
✅ orphanRemoval = true (고아객체 제거)
부모 엔티티와 자식 엔티티의 관계가 제거되면  자식엔티티를 고아로 취급해 삭제한다.

식별자 생성 기능

식별자는 세가지 방식중 하나로 생성 할 수 있다.

  • 사용자가 직접 생성
    • 이메일이나 아이디처럼 사용자가 직접 입력하는 경우 
  • 도메인 로직으로 생성
    • 식별자를 생성하는 기능을 따로 분리해 도메인 레이어의 서비스나, 레포지토리에 만든다.
  • DB를 이용한 일련번호 생성
    • 식별자 매핑에서 @GeneratedValue 를 사용
    • 자동증가 컬럼은 insert 후 식별자가 생성되므로 객체 생성시점에는 식별자를 알 수 없다. 
    • 엔티티를 저장한 후 에 생성된 식별자는 @Id에 매핑된 필드에 할당된다.
💡[도메인 구현과 DIP] JPA는 구현인데 도메인 레이어에 있어도 될까?
DIP를 적용하는 이유는 정책에 의해 코드가 변경될 때 그 변경점을 최소화 하기 위해서, 의존성을 줄여 테스트를 용이하게 함 이었다. 
JPA(구현)을 도메인 레이어에서 썼을 때 이 두가지 사항이 문제가 되지 않기 때문에 개발 편의성과 실용성을 위해 JPA애노테이션을 도메인 모델에 사용하는 것은  타협해볼만 하다.

 

Reference

도메인 주도개발 시작하기, 최범균 저

반응형