[DDD START] 4장 리포지터리와 모델구현(JPA 중심) - Entity와 Value 매핑 구현

Entity와 Value 기본 매핑 구현

주문 애그리거트의 루트 Entity인 Order는 JPA의 @Entity로 매핑한다
@Entity
@Table(name = "purchase_order")
public class Order {
    ...
}

한 테이블에 Entity와 Value데이터가 같이 있다면

Order에 속하는 Orderer는 Value이므로 @Embeddable로 매핑한다
@Embeddable
public class Orderer {
    
    // MemberId에 정의된 칼럼 이름을 변경하기 위해
    // @AttributeOverride 사용
    @Embedded
    @AttributeOverrides(
        @AttributeOverride(name = "id", column = @Column(name = "orderer_id"))
    )
    private MemberId memberId;

    @Column(name = "orderer_name")
    private String name;
}
@Embeddable
public class MemberId implements Serializable {
    @Column(name = "member_id")
    private String id;
}
Order 애그리거트 루트 Entity는 @Embedded를 이용해서 Value타입 프로퍼티를 설정한다
@Entity
public class Order {
    ...
    @Embedded
    private Orderer orderer;

    @Embedded
    private ShippingInfo shippingInfo;
    ...
}

Value 컬렉션을 별도 테이블에 매핑할 때

@Entity
@Table(name = "purchase_order")
public class Order {
    ...
    @ElementCollection
    @CollectionTable(name = "order_line", joinColumns = @JoinColumn(name = "order_number"))
    @OrderColumn(name = "line_idx")
    private List<OrderLine> orderLines;
}
@Embeddable
public class OrderLine {
    @Embedded
    private ProductId productId;
    
    private Monen price;

    private int quantity;

    private Money amounts;
}

Value 컬렉션을 한 개 칼럼에 매핑할 때

Value를 별도 테이블에 매핑할 때

@Entity
@Table(name = "article")
@SecondaryTable(
    name = "article_content",
    pkJoinColumns = @PrimaryKeyJoinColumn(name = "id")
)
public class Article {
    @Id
    private Long id;
    private String title;
    
    @AttributeOverrides({
        @AttributeOverride(name = "content", column = @Column(table = "article_content")),
        @AttributeOverride(name = "contentType", column = @Column(table = "article_content"))
    })
    private ArticleContent content;
}

@SecondaryTable을 이용할 때 조회 성능

// @SecondaryTable로 매핑된 article_content 테이블을 조인
Article article = entityManager.find(Article.class , 1L);

게시글 목록을 보여주는 화면에 article_content 데이터가 필요하지 않다면 조회 전용 기능을 구현한다

Value 컬렉션을 @Entity로 매핑하기

구현 기술의 한계나 팀 표준 때문에 @Entity를 사용해야 할 때도 있다

예)제품의 이미지 업로드 방식에 따라 이미지 경로와 썸네일 이미지 제공 여부가 달라질 때 Image 클래스 구현

@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;
    }

    public abstract String getUrl();
    public abstract boolean hasThumbnail();
    public abstract String getThumbnailUrl();
}

자식 클래스 구현

@Entity
@DiscriminatorValue("II")
public class InternalImage extends Image {

    protected InternalImage() {}
    public InternalImage(String path) {
        super(path);
    }

    @Override
    public String getUrl() {
        return "/images/original/" + getPath();
    }

    @Override
    public boolean hasThumbnail() {
        return true;
    }

    @Override
    public String getThumbnailUrl() {
        return "/images/thumbnail/"+getPath();
    }
}
@Entity
@DiscriminatorValue("EI")
public class ExternalImage extends Image {

    protected ExternalImage() {}
    public ExternalImage(String path) {
        super(path);
    }

    @Override
    public String getUrl() {
        return getPath();
    }

    @Override
    public boolean hasThumbnail() {
        return false;
    }

    @Override
    public String getThumbnailUrl() {
        return null;
    }
}

Image를 사용하는 Product Entity에서 매핑 처리

@Entity
@Table(name = "product")
public class Product {
    @EmbeddedId
    private ProductId id;
    
    ...

    @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE},
            orphanRemoval = true, fetch = FetchType.EAGER)
    @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);
    }

    ...
}

@OneToMany 컬렉션의 삭제 비효율성

Image 클래스 구현을 @Entity가 아닌 @Embeddable로 바꾸면

@Embeddable로 구현

@Embeddable
public class Image {
    
    private String imageType;

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

    public boolean hasThumbnail() {
        // 성능을 위해 다형을 포기하고 if-else로 구현
        if (imageType.equals("TI")) {
            return true;
        } else {
            return false;
        }
    }
}

images의 변경 빈도에 따라 성능과 다형성(코드 유지보수)을 고려해서 구현 방식을 선택해야 한다

Reference