데이터 보관소로 RDBMS를 사용할 때, 객체 기반의 도메인 모델과 관계형 데이터 모델간의 매핑을 처리하는 기술인 ORM을 사용
리포지터리 인터페이스는 애그리거트와 같이 도메인 영역에 속하고
리포지터리를 구현한 클래스는 인프라스트럭처 영역에 속함
리포지터리 구현 클래스를 인프라스트럭처 영역에 위치시켜서 인프라스트럭처에 대한 의존을 낮춰야 함
//Repository -> JpaRepository에서는 findById, save 등의 메서드를 자동으로 제공해줌
public interface OrderRepository extends Repository<Order, OrderNo> {
Order findById(OrderNo id);
void save(Order order);
}
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 기본 생성자
4.3.3 필드 접근 방식 사용
JPA 는 필드
와 메서드
의 두 가지 방식으로 매핑 처리 함.
4.3.4 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);
}
}
4.3.5 밸류 컬렉션: 별도 테이블 매핑
@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를 사용하여 한 개 컬럼에 매핑함
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를 사용해야 할 때도 있음
@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{
...
}
ex) Image 테이블에 10개의 컬럼이 존재할 경우 List인 images를 삭제하게 된다면 Image 테이블의 컬럼들을 조회하는 select 1번 + 삭제하기위한 Delete 4번 = 5번의 쿼리가 발생함
하지만 @Embedded 컬렉션을 사용한다면 JPA에서는 @Embedded를 삭제할 때 컬렉션에 속한 객체들을 조회하지 않고 삭제함 때문에 delete 쿼리 1번만으로 데이터를 삭제할 수 있음
--> 선택사항...
4.3.10 ID 참조와 조인 테이블을 이용한 단방향 M-N 매핑
@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;
...
}
애그리거트의 속한 객체가 모두 모여야 완전한 하나가 되고 애그리거트는 개념적으로 하나여야 함
-> 즉시 로딩(FetchType.EAGER)으로 설정한다면 조회 시점에 완전한 상태될 수 있음
하지만, 실제로 상태를 변경하는 시점에 필요한 구성요소만 로딩할 수 있으며 N+1과 같은 다양한 문제를 발생시킬 수 있기에 루트 엔티티를 로딩하는 시점에 애그리거트에 속한 객체를 모두 로딩해야 하는 것은 아님
애그리거트는 완전한 상태여야 함
--> 이는 조회할 때뿐만 아니라 저장하고 삭제할 때도 마찬가지
@Embeddable 매핑 타입은 함께 저장되고 삭제되므로 cascade 속성을 추가로 설정할 필요가 없지만 @Entity 타입은 cascade = {CascadeType.PERSIST, CascadeType.REMOVE} 속성을 사용해야 함
식별자 생성 방식의 종류
식별자는 크게 세가지 방식으로 생성한다.
JPA를 사용하면 의존성이 생기므로 DIP 위배 함
저번 스터디의 결론으로는 DIP를 완벽하게 지키면서 의존성을 가져가지 않는 것도 중요하지만 개발의 편의성을 가져가는 것도 중요하기에 어느정도의 의존성이 생기는건 감수