IT recording...
[DDD START!] 4장 - 리포지터리와 모델 구현 (JPA중심) 본문
https://adorable-aspen-d23.notion.site/DDD-START-4-JPA-ed21abe14e4147ed90c19161b9d5d7af
1. JPA를 이용한 리포지터리 구현
- 애그리거트를 어떤 저장소에 저장하느냐에 따라 리포지터리를 구현하는 방법이 다르다.
- 도메인 모델과 리포지터리를 구현할 때 선호하는 기술은 JPA이다.
- 데이터 보관소로 RDBMS를 사용할 때 객체 기반의 도메인 모델과 관계형 데이터 모델 간의 매핑을 처리하는 기술로 ORM이 최고다.
- 다양한 ORM기술 중 ORM 표준인 JPA를 사용해서 리포지터리와 애그리거트를 구현하는 방법을 살펴보자
2. 리포지터리 기본 기능 구현
- 리포지터리의 기본 기능
- 아이디로 애그리거트 조회하기
- 애그리거트 생성하기
- 인터페이스 형식
public interface OrderRepository {
public Order findById(OrderNo no);
public void save(Order order);
}
→ 인터페이스는 애그리거트 루트를 기준으로 작성한다.
- 리포지터리 구현 클래스
@Repository
public class JpaOrderRepository implements OrderRepository {
@PersistenceContext
private EntityManaver entityManager;
@Override
public Order findById(OrderNo id){
return entityManager.find(Order.class,id);
}
@Override
public void save(Order order) {
entityManager.persist(order);
}
}
- 애그리거트를 수정한 결과를 저장소에 반영하는 메서드를 추가할 필요는 없다.
- JPA를 사용하면 transaction 범위에서 변경한 데이터를 자동으로 DB에 반영한다.
public class ChangeOrderService {
@Transactional
public void changeShippingInfo(OrderNo no, ShippingInfo newShippingInfo) {
Order order = orderRepository.findById(no);
if (order == null) throw new OrderNotFoundException();
order.chagneShippingInfo(newShippingInfo);
}
...
}
→ changeShippingInfo() 메서드는 스프링 프레임워크의 트랜잭션 관리 기능을 통해 트랜잭션 범위에서 실행된다.
→ 메서드 실행이 끝나면 트랜잭션을 커밋하는데, 이때 JPA는 트랜잭션 범위에서 변경된 객체의 데이터를 DB에 반영하기 위해 자동으로 UPDATE 쿼리를 실행한다.
- 아이디가 아닌 다른 조건으로 애그리거트를 조회할 경우
public interface OrderRepository {
...
public List<Order> findByOrdererId(String ordererId, int startRow, int size);
}
@Override
public List<Order> findByOrdererId(String ordererId, int startRow, int fetchSize){
TypedQuery<Order> query = entityManager.createQuery(
"select o from Order o where o.orderer.memberId.id = :ordererId" +
"order by o.number.number desc",
Order.class);
query.setParameter("ordererId", ordererId);
query.setFirstResult(startRow);
query.setMaxResults(fetchSize);
return query.getResultList();
}
- 애그리거트 삭제 기능
- 삭제 요구사항이 있다고 하더라도 바로 삭제하는 경우는 거의 없다.
- 관리자 기능에서 삭제 데이터를 조회해야 하는 경우도 있고 데이터 원복을 위해 일정 기간 동안 보관해야 할 때도 있기 때문이다.
- 따라서 삭제 플래그를 사용해서 데이터를 화면에 보여줄지 여부를 결정하는 방식으로 구현한다.
public interface OrderRepository {
...
public void delete(Order order);
}
public class JpaOrderRepository implements OrderRepository {
@PersistenceContext
private EntityManager entityManager;
...
@Override
public void remove(Order order){
entityManager.remove(order);
}
}
3. 매핑 구현
- 앤티티와 밸류 기본 매핑 구현
애그리거트와 JPA 매핑을 위한 기본 규칙
- 애그리거트 루트는 엔티티이므로 @Entity로 매핑 설정한다.
- 한 테이블에 엔티티와 밸류 데이터가 같이 있다면,
- 밸류는 @Embeddable로 매핑 설정한다.
- 밸류 타입 프로퍼티는 @Embedded로 매핑 설정한다.
- Order 애그리거트
- 루트 엔티티 : Order
- 밸류 : Orderer, ShippingInfo
- 그 외 객체 : Address, Receiver
@Entity
@Table(name = "purchase_order")
public class Order {
...
@Embedded //밸류 타입 프로퍼티
private Orderer orderer;
@Embedded
private ShippingInfo shippingInfo;
}
**@Embeddable //밸류**
public class Orderer {
**@Embedded** //밸류 타입 프로퍼티
@AttributeOverrides(
@AttributeOverride(name = "id", column = @Column(name="orderer_id"))
) **//MemberId안에 존재하는 id 값을 table에 저장할 때 orderer_id로 저장한다.**
private MemeberId memberId;
@Column(name = "orderer_name")
private String name;
**//name을 table에 저장할 때 orderer_name으로 저장한다.**
}
**@Embeddable**
public class MemberId implements Serializable {
@Column(name = "member_id")
private String id;
}
- AttributeOverride 사용 방법
JPA - Entity의 가독성을 높이자(@Embedded, @Embeddable, @AttributeOverride 사용법)
@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;
)
}
기본 생성자
- JPA의 @Entity와 @Embeddable로 클래스를 매핑하려면 기본 생성자를 제공해야 한다.
- 하이버네이트와 같은 JPA 프로바이더는 DB에서 데이터를 읽어와 매핑된 객체를 생성할 때 기본 생성자를 이용해서 객체를 생성하기 때문이다.
- 따라서 필요 없더라도 기본 생성자를 추가해야 한다.
- 하지만 다른 곳에서 사용하면 값이 없는 온전하지 못한 객체를 만들게 되므로 이를 방지하기 위해 protected로 선언한다.
@Embeddable
public class Receiver {
@Column(name = "receiver_name")
private String name;
@Column(name = "receiver_phone")
private String phone;
**protected Receiver() {}** //JPA적용 위한 기본 생성자
public Receiver(String name, String phone) {
this.name = name;
this.phone = phone;
}
...//get생략
}
필드 접근 방식 사용
→ JPA는 메서드(PROPERTY) 방식, 필드(FIELD) 방식 두 가지 방식으로 매핑을 처리할 수 있다.
- @Access(AccessType.PROPERTY)
- get/set 메서드를 구현해야 한다.
- 도메인의 의도가 사라지고 객체가 아닌 데이터 기반으로 엔티티를 구현할 가능성이 높아진다.
- set메서드를 통해 외부에서 내부 데이터를 변경할 수 있기 때문에 캡슐화를 깨는 원인이 될 수 있다.(ex. setState → cancel() , setShippingInfo() → changeShippingInfo() )
- → set메서드 대신 기능을 잘 표현하는 의미를 갖는 메서드를 사용해야 한다.
- @Access(AccessType.FIELD)
- get/set 메서드를 구현하지 않는다.
@Entity
**@Access(AccessType.FIELD)**
public class Order {
@EmbeddedId
private OrderNo number;
@Column(name = "state")
@Enumerated(EnumType.STRING)
private OrderState state;
...//cancel(), chagneShippingInfo() 등 도메인 기능 구현
...//필요한 get 메서드 제공
}
- JPA의 구현체인 하이버네이트는 @Access 를 이용하여 명시적으로 접근 방식을 지정하지 않으면 @Id 나 @EmbeddedId가 필드에 위치해 있을 때는 필드 방식, 위치해 있지 않다면 메서드 접근 방식을 택한다.
AttributeConverter를 이용한 밸류 매핑 처리
- 밸류 타입의 프로퍼티(두 개 이상)을 한 개 칼럼에 매핑해야 할 때 @Embeddable로는 처리할 수 없다.
- 이에 AttributeConverter를 사용하여 밸류타입과, 칼럼 데이터 간의 변환 처리를 가능하게 한다.
package javax.persistence;
public interface AttributeConverter<X,Y> { //X:밸류타입, Y:DB타입
public Y convertToDatabaseColumn (X attribute);
public X convertToEntityAttribute (Y dbData);
}
**@Converter(autoApply = True)**
public class MoneyConverter implements AttributeConverter<Money, Integer> {
@Override
public Integer convertToDatabaseColumn(Money money){ //밸류->DB
if(money == null) return null;
else return money.getValue();
}
@Override
public Money convertToEntityAttribute(Integer value) { //DB->밸류
if(value == null) return null;
else return new Money(value);
}
}
- autoApply = True는 모델에 출현하는 모든 Money타입의 프로퍼티에 대해 MoneyConverter를 자동으로 적용한다는 것이다.
- autoApply = false인 경우 컨버터를 명시적으로 지정할 수 있다.
public class Order {
@Column(name = "total_amounts")
@Convert(converter = MoneyConverter.class)
private Money totalAmounts;
}
밸류 컬렉션 : 별도 테이블 매핑
- 한 개의 엔티티가 한 개 이상의 밸류를 가질 수 있다.
- Order 엔티티는 한 개 이상의 OrderLine을 가질 수 있다.
- 밸류 타입의 컬렉션(여러 개를 담아 놓은 것)은 별도 테이블에 보관한다.
- 밸류 컬렉션을 저장하는 ORDER_LINE 테이블은 외부키를 사용해서 엔티티에 해당하는 PURCHASE_ORDER 테이블을 참조한다.
- 이 외부 키는 컬렉션이 속할 엔티티를 의미한다.
- List타입의 컬렉션은 인덱스 값이 필요하므로 ORDER_LINE 테이블에는 인덱스 값을 저장하기 위한 칼럼(line_idx)도 존재한다.
- 밸류 컬렉션을 저장하는 ORDER_LINE 테이블은 외부키를 사용해서 엔티티에 해당하는 PURCHASE_ORDER 테이블을 참조한다.
public class Order {
private List<OrderLine> orderLines;
...
}
- 밸류 컬렉션을 별도 테이블에 매핑할 때는 @ElementCollection과 @CollectionTable을 함께 사용한다.
@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;
...
}
- List 자체가 인덱스를 갖고 있기 때문에 OrderColumn 어노테이션을 사용해서 리스트의 인덱스를 저장한다.
- @CollectionTable은 밸류를 저장할 테이블을 지정할 때 사용한다.
- name 속성으로 table 이름을 지정한다.
- joinColumns 속성으로 외부키로 사용하는 컬럼을 지정한다.
@Embeddable
public class OrderLine{
@Embedded
private ProductId productId;
@Column(name = "price")
private Money price;
@Column(name = "quantity")
private int quantity;
@Column(name = "amounts")
priavate Money amounts;
...
}
밸류 컬렉션 : 한 개 칼럼 매핑
- 밸류 컬렉션을 별도의 테이블이 아닌 한 개의 칼럼에 매핑해야 할 때가 있다.
- AttributeConverter를 사용한다.
ex) 도메인 모델에서는 이메일 주소 목록을 Set으로 보관하고 DB에는 한 개 칼럼에 콤마로 구분해서 저장한다.
- 밸류 컬렉션을 위한 새로운 밸류 타입을 추가해야 한다.
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);
}
}
- AttributeConverter 구현
@Converter
public class EmailSetConverter implements AttributeConverter<EmailSet,String> {
@Override
public String convertToDatabaseColumn(EmailSet attribute) {
if(attribute == null) return null;
return attribute.getEmails().stream() //콤마로 이어서 저장
.map(Email::String)
.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;
밸류를 이용한 아이디 매핑
- 식별자를 기본 타입이 아닌 밸류 타입을 이용하게 되면 @Id 어노테이션 대신 @EmbeddedId 를 사용한다.
@Entity
public class Order{
@Id
private String number;
...
}
@Entity
@Table(name = "purchase_order")
public class Order{
**@EmbeddedId**
private OrderNo number;
...
}
**@Embeddable**
public class OrderNo **implements Serialiazable**{ //JPA에서 식별자는 Serializable타입이어야 한다.
@Column(name = "order_number")
private String number;
public boolean is2ndGeneration(){ //식별자에 기능 추가가 가능하다.
return number.startsWith("N");
}
...
}
별도 테이블에 저장하는 밸류 매핑
- 밸류임에도 불구하고 별도 테이블에 저장하는 경우가 있다.
- 애그리거트에서 루트 엔티티를 제외한 나머지 구성요소는 대부분 밸류이다.
- 엔티티로 판단된 것들이 자신만의 독자적인 라이프사이클을 갖는다면 다른 애그리거트일 가능성이 높다. (Product, Review)
- Product와 Review는 함께 생성되지 않고, 함께 변경되지도 않는다.
- 애그리거트에 속한 객체가 밸류인지 엔티티인지 구분하는 방법은 고유 식별자를 갖는지 여부를 확인하는 것이다.
- 단, 별도 테이블로 저장되고 테이블에 PK가 존재한다고 해서 테이블과 매핑되는 애그리거트 구성요소가 고유 식별자를 갖는 것은 아니다.
- 게시글 데이터를 ARTICLE 테이블과 ARTICLE_CONTENT 테이블로 나누어 저장한다고 하자
- ARTICLE_CONTENT의 ID 칼럼이 식별자이므로 ArticleContent를 엔티티로 생각할 수 있는데, 이것 때문에 Article과 ArticleContent를 두 엔티티 간의 일대일 연관으로 매핑하는 실수를 할 수 있다.
- 하지만 ArticleContent는 엔티티가 아닌 밸류이므로 다음과 같이 접근해야 한다.
- ArticleContent는 밸류이므로 @Embeddable로 매핑한다.
- 밸류를 매핑한 테이블을 지정하기 위해 @SecondaryTable과 @AttributeOverride를 사용한다.
@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_contentType")) })** private ArticleContent content; ... }
- @SecondaryTable을 이용하면 아래 코드 실행 시 두 테이블을 조인해서 데이터를 조회한다.
//@SecondaryTable로 매핑된 article_content 테이블을 조인 Article article = entityManager.find(Article.class, 1L);
- 밸류를 매핑한 테이블을 지정하기 위해 @SecondaryTable과 @AttributeOverride를 사용한다.
밸류 컬렉션을 @Entity로 매핑하기
* 개념적으로 밸류인데 구현 기술의 한계 혹은 팀 표준 때문에 @Entity를 사용해야 할 때가 있다.
ex) 제품의 이미지 업로드 방식에 따라 이미지 경로와 썸네일 이미지 제공 여부가 달라진다고 해보자.
- JPA는 @Embeddable 타입의 클래스 상속 매핑을 지원하지 않는다.
- 따라서 상속 구조를 갖는 밸류 타입을 사용하려면 @Embeddabl대신 @Entity 를 이용한 상속 매핑으로 처리해야 한다.
- 밸류 타입을 @Entity로 매핑하므로 식별자 매핑을 위한 필드도 추가해야 한다. (엔티티는 식별자 필요)
- 구현 클래스 구분을 위한 타입 식별 (discriminator) 칼럼을 추가해야 한다.
@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() {} //JPA에서 @Entity,@Embeddable을 사용하면 기본 생성자 필요 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(); }
- 한 테이블에 Image 및 하위 클래스를 매핑하므로 @Inheritance를 적용하고 strategy 값으로 SINGLE_TABLE을 사용한다.
- @DiscriminatorColumn을 사용하여 타입을 구분하는 용도로 사용할 칼럼을 지정한다.
//Image를 상속받은 클래스 **@Entity @DiscriminatorValue("II")** public class InternalImage extends Image { ... } **@Entity @DiscriminatorValue("EI")** public class ExternalImage extends Image { ... }
@Entity @Table(name = "product") public class Product { @EmbeddedId private ProductId id; private String name; @Convert(converter = MoneyConverter.class) private Money price; private String detail; **@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); } }
- Image가 @Entity이므로 목록을 담고 있는 Product는 @OneToMany를 이용해서 매핑을 처리한다.
- Image는 밸류이므로 독자적인 라이프사이클을 가지지 않고 Product에 완전히 의존한다.
- cascade 속성을 사용하여 Product를 저장할 때 함께 저장되고, 삭제될 때 함께 삭제되도록 설정한다.
- 리스트에서 Image 객체를 제거하면 DB에서 함께 삭제되도록 orphanRemoval = true로 설정한다.
- changeImages 메소드를 보면 이미지 교체를 위해 clear() 메서드를 사용하고 있는데, Image 목록을 가져오기 위한 한 번의 select 쿼리, 각 image를 삭제하기 위한 여러 번의 delete 쿼리가 실행된다.
- 전체 서비스 성능에 문제가 될 수 있다.
- 하이버네이트는 @Embeddable 타입에 대한 컬렉션의 clear() 메서드를 호출하면 컬렉션에 속한 객체를 로딩하지 않고 한 번의 delete 쿼리로 삭제 처리를 수행한다.
- ⇒ 따라서 애그리거트의 특성을 유지하면서 이 문제를 해소하려면 결국 상속을 포기하고 @Embeddable로 매핑된 단일 클래스로 구현해야 한다.
→ 코드 유지보수와 성능의 두 가지 측면을 고려해서 구현 방식을 선택해야 한다.@Embeddable public class Image{ @Column(name = "image_type") private String imangeType; @Column(name = "image_path") private String path; @Temporal(TemporalType.TIMESTAMP) @Column(name = "upload_time") private Date uploadTime; ... **public boolean hasThumbnail(){ //성능을 위해 다형을 포기하고 if-else로 구현 if (imageType.equals("II") return true; else return false; }** }
- 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; ... }
- product에서 category로의 단방향 M:N 연관을 ID참조 방식으로 구현했다.
- ID참조를 이용한 애그리거트 간 단방향 M:N 연관은 밸류 컬렉션 매핑과 동일한 방식으로 설정한 것을 알 수 있다.
- @ElementCollection을 사용하기 때문에 Product를 삭제할 때 매핑에 사용한 조인 테이블의 데이터도 함께 삭제된다.
- 애그리거트를 직접 참조하는 방식을 사용했다면 영속성 전파, 로딩 전략을 고민해야 하는데 ID참조 방식을 사용함으로써 이런 고민을 할 필요가 없다.
- JPA 매핑 설정 시 애그리거트 내 모든 객체들은 항상 완전한 상태여야 한다.
연관 매핑 조회 방식//product는 완전한 하나여야 한다. Product product = productRepository.findById(id);
- FetchType.EAGER
- 조회 시점에서 애그리거트를 완전한 상태가 되도록 할 수 있다.
- 컬렉션이나 entity에 대한 fetch 속성을 즉시 로딩으로 설정하면 EntityManager#find() 메서드로 애그리거트 루트를 구할 때 연관된 구성요소를 DB에서 함께 읽어온다.
- But, 컬렉션에 대해 즉시 로딩 방식을 설정하면, 카타시안 조인으로 인한 쿼리 중복 문제가 발생한다.
@Entity @Table(name = "product") public class Product { ... //엔티티 @OneToMany( cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true, **fetch = FetchType.EAGER**) @JoinColumn(name = "product_id") @OrderColmn(name = "list_idx") private List<Image> images = new ArrayList<>(); //컬렉션 @ElementCollection(**fetch = FetcType.EAGER**) @CollectionTable(name = "product_option", joinColumns = @JoinColumn(name = "product_id")) @OrderColumn(name = "list_idx") private List<Option> options = new ArrayList<>(); ... }
- 이 매핑을 사용할 때 EntityManager#find() 메서드로 Product를 조회하면 하이버네이트는 Product를 위한 테이블, Image를 위한 테이블, Option을 위한 테이블을 조인한 쿼리를 실행한다.
select p.product_id, ... img.product_id, img.image_id, img.list_idx, ... , opt.product_id, opt.option_title, opt.option_value, opt.list_idx from product p left outer join image img on p.product_id=img.product_id left outer join product_option opt on p.product_id = opt.product_id where p.product_id=?
- 이 쿼리는 카타시안 조인을 사용하여 쿼리 결과에 중복을 발생시킨다.
- 먄약 한 개 제품에 대한 이미지가 20개이고 Option이 15개라면 EntityManager#find() 메서드는 250행의 쿼리를 리턴한다.
- 중복된 데이터들이 발생하는데, 실제 필요한 행 개수는 36(1+20+15) 인 것에 비하면 과도하게 많다.
- 이런 경우 조회 성능 때문에 즉시 로딩 방식을 사용하지만, 오히려 이 때문에 조회 성능이 나빠지는 문제가 발생한다.
- 상태를 변경하는 기능을 실행할 때 애그리거트 상태가 완전해야 한다.
- 굳이 상태 변경을 위해 조회 시점에 즉시 로딩을 이용할 필요는 없다.
- 표현 영역에서 애그리거트의 상태 정보를 보여줄 때 필요하다.
- 별도의 조회 전용 기능을 구현하면 된다.
@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); } }
- 지연 로딩을 하게 되면 추가 쿼리가 발생하게 되지만, 상태 변경 기능 실행 빈도보다 조회하는 기능을 실행하는 빈도가 훨씬 높기 때문에, 상태 변경을 위해 지연 로딩을 사용할 때 발생하는 추가 쿼리로 인한 실행 속도 저하는 문제 되지 않는다.
- 지연 로딩은 동작 방식이 항상 동일하기 때문에 즉시 로딩처럼 경우의 수를 따질 필요가 없다.
- 즉시 로딩은 @Entity나 @Embeddable에 대해 다르게 동작한다.
- 애그리거트는 완전한 상태여야 한다.
- 애그리거트 루트를 조회할 때
- 애그리거트 루트를 저장할 때 (속한 모든 객체를 저장)
- 애그리거트 루트를 삭제할 때 (속한 모든 객체를 삭제)
- @Embeddable 매핑
- 자동으로 함께 저장되고 삭제된다.
- cascade속성 추가 필요 없음
- @Entity 매핑
- cascade 속성을 추가로 설정해서
- 저장과 사제 시 함께 처리되도록 설정
6. 식별자 생성 기능@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true) @JoinColumn(name = "product_id") @OrderColumn(name = "list_idx") private List<Image> images = new ArrayList<>();
- 식별자 생성 방식
- 사용자가 직접 생성
- 도메인 로직으로 생성
- DB를 이용한 일렬번호 사용
- 식별자 생성 규칙이 있는 경우, 별도 서비스로 식별자 생성 기능을 분리한다.
- 식별자 생성 규칙은 도메인 규칙이므로 도메인 영역에 식별자 생성 기능을 위치시킨다.
public class **ProductIdService**{ public ProductId nextId(){ ...//정해진 규칙으로 식별자 생성 } }
public class CreateProductServie{ @Autowired private ProductIdService idService; @Autowired private ProductRepository productRepository; @Transactional public ProductId createProduct(ProductCreationCommand cmd){ //응용 서비스는 도메인 서비스를 이용해서 식별자를 생성한다. ProductId id = productIdService.nextId(); Product product = new Product(id, cmd.getDetail(), cmd.getPricet(),...); productRepository.save(product); return id; } }
- 리포지터리에 위치시킬 수도 있다.
public interface ProductRepository{ ...//save() 등 메서드 **ProductId nextId();** }
- 식별자 생성으로 DB의 자동 증가 칼럼을 사용할 경우 JPA의 식별자 매핑에서 @GeneratedValue를 사용한다.
@Entity @Table(name = "article") ... public class Article{ @Id **@GeneratedValue(strategy = GenerationType.IDENTITY)** private Long id; public Long getId() { return id; } }
- 자동 증가 칼럼은 DB의 insert 쿼리를 실행해야 식별자가 생성되므로, 도메인 객체를 리포지터리에 저장할 때 식별자가 생성된다.
- 생성 시점에서는 식별자를 알 수 없고, 도메인 객체를 저장한 뒤에 식별자를 구할 수 있다.
public class WriteArticleService{ private ArticleRepository articleRepository; public Long write(NewArticleRequest req){ Article article = new Article("제목", new ArticleContent("content", "type")); articleRepository.save(article); //EntityManager#save() 실행 시점에 식별자 생성 return article.getId() //저장 후 식별자 사용 가능 } }
- → 애그리거트에 맞게 즉시 로딩과 지연 로딩을 선택해야 한다.
- @Transactional public void removeOptions(ProductId id, int optIdxToBeDeleted) { //Product를 로딩한다. 컬렉션을 지연 로딩으로 설정했다면 Option은 로딩하지 않는다. Product product = productRepository.findById(id); //트랜잭션 범위이므로 지연 로딩으로 설정한 연관 로딩 가능 product.removeOption(optIdxToBeDeleted); }
'DevBookReview' 카테고리의 다른 글
[DDD START!] 5장 - 리포지터리의 조회 기능(JPA중심) (0) | 2022.01.07 |
---|---|
[DDD START!] 3장 - 애그리거트 (0) | 2021.12.31 |
[DDD START!] 2장 - 아키텍처 (0) | 2021.12.28 |
[DDD START!] 1장 - 도메인 (0) | 2021.12.28 |
Comments