[DDD] 도메인 주도 개발 시작하기 - 4장

Y_Sevin·2023년 7월 29일
0

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

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

  • 데이터 보관소로 RDBMS를 사용할 때, 객체 기반의 도메인 모델과 관계형 데이터 모델간의 매핑을 처리하는 기술인 ORM을 사용

  • 리포지터리 인터페이스는 애그리거트와 같이 도메인 영역에 속하고
    리포지터리를 구현한 클래스는 인프라스트럭처 영역에 속함

  • 리포지터리 구현 클래스를 인프라스트럭처 영역에 위치시켜서 인프라스트럭처에 대한 의존을 낮춰야 함

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

  • JPA는 지정한 규칙에 맞게 리포지터리 인터페이스를 정의하면 구현체를 알아서 만들어 스프링 빈으로 등록함
  • findById() : 식별자를 이용하여 엔티티를 조회할 때 사용
  • findBy[프로퍼티명] : 특정 프로퍼티를 이용한 엔티티를 조회
//Repository -> JpaRepository에서는 findById, save 등의 메서드를 자동으로 제공해줌
public interface OrderRepository extends Repository<Order, OrderNo> {
    Order findById(OrderNo id);
    void save(Order order);
}

4.3 매핑 구현


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

  • 루트 엔티티는 @Entity로 매핑
  • 밸류는 @Embeddable 로 매핑
  • 밸류 타입 프로퍼티는 @Embedded 로 매핑
@Entity // 루트 엔티티 Order
@Tagble(name = "purchase_order")
public class Order {
	...
	@Embedded
	Orderer orderer;
}
@Embeddable //Order의 밸류
public class Orderer {
	@Embedded
	@AttributeOverrides( //MemberId에 정의되어있는 컬럼의 이름을 변경하기 위해 사용
		@AttributeOverride(name = "id", column = @Column(name = "orderer_id"))
	)
	private MemberId memberId;
	...
}
@Entity // Order의 밸류 타입 프로퍼티
@Table(name="purchase_order")
public class Order {
	@Embedded
	private Orderer orderer;
}

4.3.2 기본 생성자

  • 밸류 타입은 불변이므로 생성 시점에 필요한 값을 모두 전달받는다. 때문에 값을 변경하기위한 set 메서드는 제공하지 않음
  • 하지만 JPA에서는 @Entity 와 @Embeddable 클래스를 매핑하려면 기본 생성자를 제공해야함
    -> 때문에 다른 코드에서 기본 생성자를 사용하지 못하도록 protected로 선언

4.3.3 필드 접근 방식 사용

JPA 는 필드메서드의 두 가지 방식으로 매핑 처리 함.

  • 메서드 방식
    get/set 메서드가 추가되어야함
    • set 메서드는 내부 데이터를 외부에서 변경할 수 있는 수단이 되기에 캡슐화를 깨는 원인이 될 수 있음
      -> 기능 중심으로 엔티티를 구현할 수 있도록 set 메서드 대신 의도가 잘 드러나는 필드방식을 사용

4.3.4 AttributeConverter 를 이용한 밸류 맵핑 처리

  • 밸류 타입의 프로퍼티를 한 개 칼럼에 매핑해야 할 경우 AttributeConverter를 사용하여 벨류 타입과 칼럼 데이터 간의 변환 처리가 가능 함

AttributeConverter

public interface AttributeConverter<X, Y> {
	public Y convertToDatabaseColumn(X attribute); //DB 타입
	public X convertToEntityAttribute(Y dbData); // 밸류 타입
}
@Converter(autoApply = true)
public class MoneyConverter implements AttributeConverter<Money, Integer> {

	@Override
	public Integer convertToDatabaseColumn(Money money) {
		if(money == null) return null;
		else return money.getValue();
	}

	@Override
	public Money convertToEntityAttribute(Integer value) {
		if(value == null) return null;
		else return new Money(value);
	}
}
  • AttributeConverter 인터페이스를 구현한 클래스는 @Converter 적용
  • @Converter의 autoApply가 true일 경우 모든 Money 타입의 프로퍼티에 대해 MoneyConverter를 자동으로 적용
    false일 경우 직접 컨버터를 지정함 (@Convert(converter = MoneyConveter.class))

4.3.5 밸류 컬렉션: 별도 테이블 매핑

  • 밸류 컬렉션을 별도 테이블로 매핑 시 @ElementCollection과 @CollectionTable을 함께 사용함
@Entity
@Table(name = "purchase_order")
public class Order {
	...
	@ElementCollection
	@CollectionTable(name = "order_line",
					joinColumns = @JoinColumn(name = "order_number"))// 외부키로 사용할 컬럼 지정, 두 개 이상일 경우 @JoinColumn 의 배열을 이용해 외부키 목록을 지정
	@orderColumn(name = "line_idx")
	private list<OrderLine> orderLines;
}

@Embeddable
public class OrderLine {
	@Embedded
	private ProductId productId;
}

4.3.6 밸류 컬렉션: 한 개 컬럼 매핑

  • 밸류 컬렉션을 별도 테이블이 아닌, 한 개 컬럼에 저장해야 할 때 경우 AttributeConverter를 사용하여 한 개 컬럼에 매핑함

    • ex) 도메인 모델에는 이메일 주소 목록을 Set으로 보관하고 DB에는 한 개 컬럼에 콤마로 구분해서 저장해야 할 경우
  • AttributeConverter를 사용하면 밸류 컬렉션을 한 개 칼럼에 쉽게 매핑할 수 있음
    --> 단, AttributeConverter를 사용하려면 밸류 컬렉션을 표현하는 새로운 밸류 타입을 추가해야 한다.

public class EmailSet {

  private Set<Email> emails = new HashSet<>();

  private EmailSet() {
  }
  private EmailSet(Set<Email> emails) {
    this.emails.addAll(emails);
  }

  public Set<Email> getEmails() {
    return Collections.unmodifiableSet(emails);
  }

}
@Converter
public class EmailSetConverter implements AttributeConveter<EmailSet, String> {

  @Override
  public String convertToDatabaseColumn(EmailSet attribute) {
    if (attribute == null) {
      return null;
    }
    return attribute.getEmails().stream()
        .map(Email::toString)
        .collect(Collectors.joining(","));
  }

  @Override
  public EmailSet convertToEntityAttribute(String dbData) {
    if (dbData == null) {
      return null;
    }
    String[] emails = dbData.split(",");
    Set<Email> emailSet = Arrays.stream(emails)
        .map(value -> new Email(value))
        .collect(toSet());
    return new EmailSet(emailSet);
  }

}
@Column(name = "emails")
@Convert(converter = EmailSetConverter.class)
private EmailSet emailSet;

4.3.7 밸류를 이용한 ID 매핑

  • 식별자라는 의미를 강조시키기 위해 식별자 자체를 밸류 타입으로 만들 수 있음

  • @EmbeddedId 를 사용하여 매핑하며 JPA에서 식별자 타입은 Serializable 타입이여야 하므로 Serializable인터페이스를 상속하여 구현

  • 이렇게 식별자를 밸류 타입으로 매핑한다면 식별자에 기능을 추가할 수 있는 장점이 있음

4.3.8 별도 테이블에 저장하는 밸류 매핑

  • 애그리거트에서 루트 엔티티를 뺀 나머지 구성요소는 대부분 밸류이다. 만약 밸류가 아니라 엔티티가 확실하다면 다른 애그리거트는 아닌지 확인이 필요함
  • 애그리거트에 속한 객체가 밸류인지 엔티티인지 구분하는 방법은 고유 식별자를 가지는지 확인

4.3.9 밸류 컬렉션을 @Entity로 매핑

개념적으로 밸류이지만 구현 기술의 한계나 팀 표준 때문에 @Entity를 사용해야 할 때도 있음

  • ex) 상속구조일때 - 하나의 Product가 여러 Image를 갖고 Image는 업로드 방식에 따라 InternalImage와 ExternalImage 로 나뉘어 질 경우
@Entity
public class Product {
	@OneToMany(
			cascade = {CascadeType.PERSIST, CascadeType.REMOVE},
			orphanRemoval = true)
	@JoinColumn(name = "product_id")
	@OrderColumn(name = "list_idx")
	private List<Image> images = new ArrayList<>();
}


@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "image_type")
public abstract class Image {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "image_id")
	private Long id;
}

@Entity
@DiscriminatorValue("EI")
public class ExternalImage extends Image{
	...
}
@Entity
@DiscriminatorValue("II")
public class InternalImage extends Image{
	...
}
  • JPA는 @Embeddable 타입의 클래스 상속 매핑을 지원하지 않기 때문에 @Entity를 이용한 상속 매핑으로 처리해야 함
    • 엔티티로 관리되므로 식별자 필드가 필요하고 타입 식별 칼럼을 추가해야 함
  • 밸류는 독자적인 라이프 사이클을 가지지 않기때문에 cascade 사용
  • @OneToMany 매핑에서 컬렉션의 clear() 시에는 select 후 delete를 진행하므로 매우 비효율적
    -> @Entity에서 @Embeddable로 변경하여 단일 클래스로 밸류 타입을 매핑할 수 있음

    ex) Image 테이블에 10개의 컬럼이 존재할 경우 List인 images를 삭제하게 된다면 Image 테이블의 컬럼들을 조회하는 select 1번 + 삭제하기위한 Delete 4번 = 5번의 쿼리가 발생함
    하지만 @Embedded 컬렉션을 사용한다면 JPA에서는 @Embedded를 삭제할 때 컬렉션에 속한 객체들을 조회하지 않고 삭제함 때문에 delete 쿼리 1번만으로 데이터를 삭제할 수 있음
    --> 선택사항...

4.3.10 ID 참조와 조인 테이블을 이용한 단방향 M-N 매핑

  • 애그리거트간 집합 연관은 성능상의 이유로 피해야 함
  • 하지만 요구사항을 구현하는 데 집합 연관을 사용하는 것이 유리하다면, ID 참조를 이용한 단뱡향 집합 연관을 적용
@Entity
@Table(name = "product")
public class Product {
	@EmbeddedId
	private ProductId id;

	@ElementCollection
	@CollectionTable(name ="product_category",
		joinColumns = @JoinColumn(name = "product_id"))
	private Set<CategoryId> categoryIds;
	...
}
  • @ElementCollection을 이용하므로 Product를 삭제할 때 매핑에 사용한 조인 테이블의 데이터도 함께 삭제됨

4.4 애그리거트 로딩 전략

애그리거트의 속한 객체가 모두 모여야 완전한 하나가 되고 애그리거트는 개념적으로 하나여야 함

-> 즉시 로딩(FetchType.EAGER)으로 설정한다면 조회 시점에 완전한 상태될 수 있음

하지만, 실제로 상태를 변경하는 시점에 필요한 구성요소만 로딩할 수 있으며 N+1과 같은 다양한 문제를 발생시킬 수 있기에 루트 엔티티를 로딩하는 시점에 애그리거트에 속한 객체를 모두 로딩해야 하는 것은 아님

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

애그리거트는 완전한 상태여야 함
--> 이는 조회할 때뿐만 아니라 저장하고 삭제할 때도 마찬가지

@Embeddable 매핑 타입은 함께 저장되고 삭제되므로 cascade 속성을 추가로 설정할 필요가 없지만 @Entity 타입은 cascade = {CascadeType.PERSIST, CascadeType.REMOVE} 속성을 사용해야 함

4.6 식별자 생성 기능

식별자 생성 방식의 종류
식별자는 크게 세가지 방식으로 생성한다.

  1. 사용자가 직접 생성
  2. 도메인 로직으로 생성 - 식별자 생성 규칙이 존재한다면 도메인 영역으로 따로 분리
  3. DB 를 이용한 일련번호 사용 - @GeneratedValue insert 시점에 생성

4.7 도메인 구현과 DIP

JPA를 사용하면 의존성이 생기므로 DIP 위배 함

  • 엔티티 : @Entity, @Table 등
  • 리포지토리 : Repository 인터페이스
    --> 인프라영역에 의존성이 생김 ( 인프라에 위치한 JPA 연동하기 위한 클래스 추가하는 방법으로 의존성 제거)

저번 스터디의 결론으로는 DIP를 완벽하게 지키면서 의존성을 가져가지 않는 것도 중요하지만 개발의 편의성을 가져가는 것도 중요하기에 어느정도의 의존성이 생기는건 감수

profile
매일은 아니더라도 꾸준히 올리자는 마음으로 시작하는 개발블로그😎

0개의 댓글