도메인 주도 개발 시작하기: 4. 리포지터리와 모델 구현

ParkIsComing·2023년 3월 20일
0

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

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

4.1~2를 아우르는 기본적인 리포지터리 활용 방식은 넘어가겠다.

4.3 매핑 구현

앞선 챕터에서 언급한 애그리거트, 엔티티, 그리고 밸류 데이터는 어떻게 코드에 녹여내야 할까?

  • 애그리거트 루트는 엔티티이므로 @Entity로 매핑
  • 밸류는 @Embeddable로 매핑
  • 밸류타입 프로퍼티는 @Embedded로 매핑
  • 루트 엔티티와 루트 엔티티에 속한 밸류는 한 테이블에 매핑할 때가 많다.

주문 애그리거트를 예로 들어 설명해보자.

  • 주문 애그리거트의 애그리거트 루트는 Order 엔티티가 된다.
  • 주문 애그리거트에 속한 Orderer와 ShippingInfo는 밸류이다.
  • ShippingInfo에는 Address 객체와 Receiver 객체가 포함한다.

코드로 녹여내면 다음과 같다.

import javax.persistence.*;

@Entity
@Table(name= "purchase_order")
public class Order {
    @Embedded
    priavate Orderer orderer;
    
    @Embedded
    private ShippingInfo shippingInfo;
}

@Embeddable
public class Orderer { 
    //MemberId에 정의된 칼럼명을 바꾸기 위해 @AttributeOverride를 사용한다.
    @Embedded
    @AttributeOverrides(
            @AttributeOverride(name="id", column = @Column(name = "ordered_id"))
    )
    private MemberId memberId;
    
    @Column(name="order_name")
    private String name;
}

@Embeddable
public class MemberId implements Serializable {
    @Column(name="member_id")
    private String id;
}

@Embeddable
public class ShippingInfo {
    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "zipCode", column = @Column(name = "shipping_zipcode")),
            @AttributeOverride(name = "address1", column = @Column(name = "shipping_addr1")),
            @AttributeOverride(name = "address2", column = @Column(name = "shipping_addr2"))
    })
    private Address address;
    
    @Column(name = "shipping_message")
    private String message;
    
    @Embedded
    private Receiver receiver;
}

기본생성자

객체가 불변 타입이면 생성 시점에 필요한 값을 모두 전달 받기 때문에 set메소드는 필요없다. 하지만 JPA에서 @Entity@Embeddable로 클래스를 매핑하려면 기본생성자를 제공해야 한다.

따라서 기본생성자가 필요 없어도 기본 생성자를 추가해야 하는 상황이 생긴다..

이때 다른 코드에서 기본 생성자를 생성할 수 없도록 protected로 선언하는 것도 방법이다.

필드 접근 방식

JPA는 두 가지 방식으로 매핑을 처리한다.
1. 메소드 : 프로퍼티를 위한 get/set 메소드를 구현해야 함.
2. 필드 : 객체가 제공할 기능 중심으로 엔티티를 구현하게 하기 위해서는 필드를 택하자.

AttributeConverter를 이용한 밸류 매핑 처리

두 개 이상의 프로퍼티를 가진 밸류 타입을 한 개 칼럼에 매핑할 때는 AttributeConverter를 사용하자.

예시로 알아보자.

//autoApply = true로 지정하면 모델에 사용되는 모든 Money 타입의 프로퍼티에 대해 MoneyConverter가 자동으로 적용한다.#
@Converter(autoApply = true) 
public class MoneyConverter implements AttributeConverter<Money, Integer> {
    @Override
    public Integer convertToDatabaseColumn(Money moeny) {
        return money == null? null : money.getValue();
    }

    @Override
    public Money convertToEntityAttribute(Integer dbData) {
        return value == null ? null : new Money(value);
    }
}

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

Order 엔티티는 한 개이상의 OrderLine을 가진다. OrderLine에 순서를 매기고 싶다면 List 타입을 이용해 컬렉션을 프로퍼티로 지정할 수 있다.

@Entity
@Table(name="purchase_order")
public class Order{
    @EmbeddedId
    private OrderNo number;
    
    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name="order_line", joinColumns = @JoinColumn(name="order_name")) //밸류를 저장할 테이블을 지정
    @OrderColumn(name="line_idx") //지정한 칼럼에 리스트의 인덱스 값을 저장
    private List<OrderLine> orderLines;
}



@Embeddable
public class OrderLine{
    @Embedded
    private ProductId productId;
    
    @Column(name = "price")
    private Money price;
    
    @Column(name= "quantity")
    private int quantity;
    
    @Column(name="amounts")
    private Money amounts;
}

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

AttributeConverter를 이용하면 밸류 컬렉션을 한 개 칼럼에 매핑할 수 있다. 단, 밸류 컬렉션을 포현하는 새로운 밸류 타입을 추가해야 한다.

public class EmailSet{ //밸류 컬렉션을 위한 밸류 타입 추가
    private Set<Email> emails = new HashSet<>();
    
    public EmailSet(Set<Email> emails){
        this.emails.addAll(emails);
    }
    
    public Set<Email> getEmails(){
        return Collections.unmodifiableSet(emails);
    }
}
//AttributeConverter 구현

public class EmailSetConverter implements AttributeConverter<EmailSet,String>{
    @Override
    public String convertToDatabaseColumn(EmailSet attribute) {
        if(attribute == null){
            return null;
                     
        }
        return attribute.getEmails().stream()
                .map(email -> email.getAddress())
                .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);
    }
@Entity
@Table(name = "member")
public class Member {
    @EmbeddedId
    private MemberId id;

	//EmailSet 타입 프로퍼티가 Converter로 EmailSetConverter를 사용하도록 지정
    @Column(name = "emails")
    @Convert(converter = EmailSetConverter.class)
    private EmailSet emails; 
    
	//생략
}

밸류를 이용한 ID 매핑

식별자 자체를 밸류 타입으로 만들 수도 있다.

JPA에서 식별자 타입은 Serializable 타입이어야 하기 때문에 식별자로 사용한 밸류 타입은 Serializable 인터페이스를 상속받아야 한다.

밸류 타입으로 식별자를 구현하면 얻는 장점은 식별자에 기능을 추가할 수 있다는 점이다.

@Embeddable
public class OrderNo implements Serializable {
    @Column(name="order_number")
    private String number;
    
    public boolean is2ndGeneration(){
        return number.startsWith("N");
    }
}

4.4 애그리거트 로딩 전략

JPA 매핑을 설정할 때 애그리거트에 속한 객체가 모두 모여야 완전한 하나가 된다는 것을 유의하여야 한다.

조회 시점에서 애그리거트가 완전한, 정상 상태이게 하려면 애그리거트 루트에서 연관 매핑의 조회 방식을 즉시로딩 방식인 FetchType.EAGER로 설정하면 된다.

하지만 즉시 로딩 방식을 사용하려면 성능 문제에 대해 생각해봐야 한다. 즉시 로딩을 사용하면 조회되는 데이터 개수가 많아져 속도가 느릴 수 있다.

애그리거트는 개념적으로 완전한 하나여야 하지만 루트 엔티를 로딩하는 시점에 애그리거트에 속한 모든 객체를 로딩해야 하는 것은 아니다. JPA는 트랜잭션 범위 내에서 지연 로딩을 허용한다. 따라서 상태를 변경하는 시점에 필요한 구성요소만 로딩해도 괜찮다.

@Transactional
public void removeOptions(ProductId id, int optIdxToBeDeleted){
    Product product = productRepository.findById(id); //지연로딩으로 설정하면 Option은 로딩하지 않는다.
     // 트랜잭션 범위이므로 지연 로딩으로 설정한 연관 로딩이 가능하다.
    product.removeOptionoptIdxToBeDeleted);
        
}
@Entity
public class Product{
    @ElementCollection(fetch = FetchType.LAZY)
    @CollectionTable(name = "product_option", joinColumns = @JoinColumn(name = "product_id"))
    @OrderColumn(name = "list_idx")
    private List<Option> options = new ArrayList<>();

    public void removeOption(int optIdx){
        this.options.remove(optIdx);
    }
}

0개의 댓글