리포지터리 인터페이스
는 애그리거트와 같이 도메인 영역에 속함리포지터리를 구현한 클래스
는 인프라스트럭처 영역에 속함public interface OrderRepository {
Order findById(OrderNo no); //1. ID로 애그리거트 조회
void save(Order order); //2. 애그리거트 저장
}
👇 JPA의 EntityManager
를 이용해서 위의 인터페이스를 구현한 클래스
@Repository
public class JpaOrderRepository implements OrderRepository {
@PersistenceContext
private EntityMAnager entityManager;
@Override
public Order findById(OrderNo id) {
return entityMAnager.find(Order.class, id)
}
@Override
public void save(Order order) {
entityManager.persist(order);
}
}
스프링과 JPA로 구현할 때는
스프링 데이터 JPA
사용
- 리포지터리 인터페이스만 정의하면, 리포지터리 구현 객체는 자동 생성됨
- 실질적으로 리포지터리 인터페이스 구현한 클래스 작성할 일 거의 없음
+) JPQL을 이용한 애그리거트 조회, 애그리거트 삭제 기능
org.springframwork.data.repository.Repository<T, ID>
인터페이스 상속T
는 엔티티 타입을 지정하고, ID
는 식별자 타입을 지정Order save(Order entity)
void save(Order entity)
Order findBiId(OrderNo id)
-> 엔티티 존재하지 않으면, null 반환Optional<Order> findById(OrderNo id)
-> 값이 없는 Object 반환List<Order> findByOrder(Orderer orderer)
List<Order> findByOrdererMemberId(MemberID memberId)
-> Orderer 객체의 memberId 프로퍼티void delete(Order order)
-> 삭제할 엔티티 전달void deleteBiId(OrderNo id)
-> 식별자 전달[애그리거트와 JPA 매핑의 기본 규칙]
@Entity
로 매핑@Embeddable
로 매핑@Embedded
로 매핑@Entity
@Tagble(name = "purchase_order")
public class Order {
...
@Embedded
private Orderer orderer;
@Embedded
private ShippingInfo shippingInfo;
...
}
Order
는 JPA의 @Entity
로 매핑@Embedded
사용해서 밸류 타입 프로퍼티 설정@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;
...
}
Orderer
는 밸류이므로 @Embeddable
로 매핑@Embeddable
타입에 설정한 칼럼 이름과 실제 칼럼 이름이 다를 때, @AttributeOverrides
이용해서 특정 프로퍼티와 매핑할 칼럼 이름 변경MemberId
의 id
프로퍼티와 매핑되는 테이블 칼럼 이름은 member_id
임. 그러나 Orderer의 memberId
프로퍼티와 매핑되는 칼럼 이름은 order_id
이므로 @AttributeOverrides
애너테이션 이용@Embaddable
public class MemberId implements Serializable {
@Column(name = "member_id")
private String id;
...
}
BUT!! JPA에서
@Entity
와@Embeddable
로 클래스 매핑하려면 기본 생성자 필요함
protected
로 선언[JPA 매핑 처리 방식]
@Entity
@Access(AccessType.FIELD) //필드 방식 사용
public class Order {
...
}
@Access
를 이용해서 명시적으로 접근 방식 지정하지 않으면 @Id
나 @EmbeddedId
의 위치에 따라 접근 방식 결정@AttributeConverter
: 두 개 이상의 프로퍼티를 가진 밸류 타입을 한 개 칼럼에 매핑1000
)과 단위(mm
)의 두 프로퍼티를 갖고 있는 Length -> DB 테이블에는 두 프로퍼티 값을 합친 WIDTH(1000mm
) 형식으로 저장public interface AttributeConverter<X,Y> {
//X는 밸류타입, Y는 DB 타입
public Y convertToDatacaseColumn(X attribute); //밸류 -> DB 칼럼
public X convertToEntityAttribute(Y dbData); //DB 칼럼 -> 밸류
}
@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);
}
}
autoApply = true
: 모델에 출현하는 모든 Money 타입의 프로퍼티에 대해 MoneyConverter를 자동으로 적용@ElementCollection
와 @CollectionTable
을 함께 사용@Entity
@Table(name = "purchase_order")
public class Order {
@EmbeddedId
private OrderNo number;
...
@ElementCollection(fetch = FetchType.EAGER)
@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;
...
}
@CollectionTable
: 밸류를 저장할 테이블 지정joinColumns
: 외부키로 사용할 칼럼 지정@OrderColumn
: 지정한 칼럼에 List의 인덱스 값 저장AttributeConverter
사용ex) 이메일 주소 목록을 Set으로 보관하고 DB에 한 개 칼럼에 콤마로 구분지어 저장하는 경우
public class EmailSet {
//이메일 집합을 위한 밸류 타입 추가
private Set<Email> emails = new HashSet<>();
private EmailSet(Set<Email> emails) {
this.emails.addAll(emails);
}
public Set<Email> getEmails() {
return Collections.unmodifiableSet(emails);
}
}
@Converter
public class EmailSetConveter implements AttributeConveter<EmailSet, String> {
//AttributeConveter 구현
@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);
}
}
@Id
대신 @EmbeddedId
사용Serializable
인터페이스를 구현해야 함@Entity
@Table(name = "purchase_order")
public class Order {
@EmbeddedId
private OrderNo number;
...
}
@Embeddable
public class OrderNo implements Serializable {
//Serializable 구현
@Column(name = "order_number")
private String number;
public boolean is2ndGeneration() { //시스템 세대 구분할 수 있는 기능 추가
return number.startsWith("N");
}
// ...
}
@Entity
@Table(name = "article")
@SecondaryTable(name = "article_content", pkJoinColumns = @PrimaryKeyJoinColumn(name = "id"))
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@AttributeOverrides({
@AttributeOverride(name = "content",
column = @Column(table = "article_content", name = "content")),
@AttributeOverride(name = "contentType",
column = @Column(table = "article_content"), name = "content_type"))
})
@Embedded
private ArticleContent content;
...
}
@SecondaryTable
의 name
속성은 밸류를 저장할 테이블 지정, pkJoinColumns
속성은 밸류 테이블에서 엔티티 테이블로 조인할 때 사용할 칼럼@AttributeOverrides
: 해당 밸류 데이터가 저장된 테이블 이름 지정@Embeddable
대신에 @Entity
사용해서 상속 매핑 처리@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) //@Inheritance의 strategy 값을 SINGLE_TABLE으로 설정
@DiscriminatorColumn(name = "image_type") //타입 구분용으로 사용할 칼럼 지정
@Table(name = "image")
public abstract class Image {
...
}
@Entity
@DiscriminatorValue("II") //Image 상속받은 클래스 매핑 설정
public class InternalImage extends Image {
...
}
@Entity
@Table(name = "product")
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<>();
}
clear()
메서드의 사용 빈도가 높으면 전체 서비스 성능에 문제가 발생하므로 상속 포기하고 @Embeddable
로 매핑된 단일 클래스 구현항상 코드 유지 보수와 성능의 두 가지 측면 고려해서 구현 방식 선택하기!!
FetchType.EAGER
)애그리거트는 개념적으로 하나여야 하지만, 루트 엔티티 로딩 시점에 애그리거트에 속한 모든 객체를 로딩해야 하는 것은 아님
-> 애그리거트 내의 모든 연관을 즉시 로딩으로 설정할 필요 없음
-> 애그리거트에 맞게 즉시 로딩과 지연 로딩 선택하기!!
애그리거트가 완전한 상태이다
= 애그리거트 루트를 조회할 때뿐만 아니라 저장, 삭제할 때로 하나로 처리해야 함
@Embebddable
매핑 타입은 함께 저장되고 삭제되므로 cascade
속성을 추가로 설정 안해도 됨@Entity
타입에 대한 매핑은 cascade
속성 설정해야 됨@OneToOne
, @OnetoMany
는 cascade
속성의 기본값 없음CascadeType.PERSIST
, @CascadeType.REMOVE
설정@GeneratedValue
사용4장에서 구현한 리포지터리는 DIP 원칙 어기고 있음
@Entity
, @Table
, @Id
, @Column
등의 애너테이션 사용BUT!! 구현 기술에 의존하지 않는 도메인 모델의 개발은 복잡함