/////
Search
Duplicate
3️⃣

매핑 구현 (JPA에 대한 이해가 없어서 어렵다.. 다시 보자)

1. 엔티티와 밸류 기본 매핑 구현

: 애그리거트와 JPA 매핑을 위한 기본 규칙은 다음과 같다.
애그리거트 루트는 엔티티이므로 @Entity로 매핑한다.
: 한 테이블에 엔티티와 밸류 데이터가 같이 있다면
밸류는 @Embeddable로 매핑 설정한다.
밸류 타입 프로퍼티는 @Embedded로 매핑 설정한다.
: 루트 엔티티인 Order는 @Entity로 매핑, 밸류인 Orderer, ShippingInfo, Address, Receiver는 @Embeddable로 매핑한다.
: 필드 단에서는 이런 밸류들을 @Embedded를 사용해서 밸류 타입 프로퍼티를 설정한다.

2. 기본 생성자

: 엔티티와 밸류의 생성자는 객체를 생성할 때 파라미터를 전달받는다.
: Receiver가 불변 타입이면 생성 시점에 필요한 값을 모두 전달받으므로 값을 변경하는 set 메서드를 제공하지 않는다. 이는 Receiver 클래스에 기본 생성자를 추가할 필요가 없다는 것을 의미한다.
: 하지만 JPA에서 @Entity@Embeddable로 클래스를 매핑하려면 기본 생성자를 제공해야한다.
: DB에서 데이터를 읽어와 매핑된 객체를 생성할 때 기본 생성자를 이용해서 객체를 생성하기 때문이다.
: 이런 기술적인 제약으로 Receiver와 같은 불변 타입은 기본 생성자가 필요 없음에도 불구하고 다음과 같이 기본 생성자를 추가해야 한다.
@Embeddable public class Receiver { @Column(name = "receiver_name") private String name; @Column(name = "receiver_phone") private String phone; protected Receiver() {} public Receiver(String name, String phone) { this.name = name; this.phone = phone; } }
Java
복사
: 기본 생성자는 JPA 프로바이더가 객체를 생성할 때만 사용하며 다른 코드에서 이를 사용할 경우 불완전한 객체를 만드므로 protected로 선언한다.

3. 필드 접근 방식 사용

: JPA는 필드와 메서드의 두 가지 방식으로 매핑을 처리할 수 있는데, 메서드 방식을 사용하려면 get/set 메서드를 구현해주어야 한다.
: 엔티티에 프로퍼티를 위한 공개 get/set 메서드를 추가하면 도메인의 의도가 사라지고 객체가 아닌 데이터 기반으로 엔티티를 구현할 가능성이 높아진다. 특히 set은 캡슐화를 깨는 원인이 될 수 있다.
: 엔티티가 객체로서 역할을 수행하려면 외부에 set 메서드 대신 그 의도가 잘 드러날 수 있는 기능을 제공해야 한다. 상태 변경을 위한 setState() 메서드보다 주문 취소를 위한 cancel()이라던지 말이다.

4. AttributeConverter를 이용한 밸류 매핑 처리

: int, long, String, LocalDate와 같은 타입은 DB 테이블의 한 개 칼럼에 매핑된다. 이와 비슷하게 밸류 타입의 프로퍼티를 한 개 칼럼에 매핑해야 할 때도 있다.
: 두 개 이상의 프로퍼티를 가진 밸류 타입을 한 개 칼럼에 매핑하려면 @Embeddable 애너테이션만으로는 처리할 수 없다. 이럴 때 사용할 수 있는 것이 AttributeConverter이다.
: AttributeConverter는 밸류 타입과 칼럼 데이터 간의 변환을 처리하기 위한 기능을 정의하고 있다.
package javax.persistence; public interface AttributeConverter <X, Y> { public Y convertToDatabaseColumn (X attribute); public X convertToEntityAttribute (Y dbData); }
Java
복사
: 다음과 같은 예시로 구현이 가능
package com.myshop.common.jpa; import com.myshop.common.model.Money; import javax.persistence.AttributeConverter; @Converter(autoApply = true) public class MoneyConverter implements AttributeConverter<Money, Integer> { @Override public Integer convertToDatabaseColumn(Money money) { return money == null ? null : money.getValue(); } @Override public Money convertToEntityAttribute(Integer value) { return value == null ? null : new Money(value); } }
Java
복사
: AttributeConverter의 구현체는 @Converter 애너테이션을 적용한다. 또한 @Converter 애너테이션의 autoApply 속성을 true로 지정하면 모델에 출현하는 모든 Money 타입의 프로퍼티에 대해 MoneyConverter를 자동으로 적용한다. false로 적용할 경우, 프로퍼티 값을 변환할 때 사용할 컨버터를 직접 지정해야 한다.

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

: Order 엔티티는 한 개 이상의 OrderLine을 가질 수 있다. OrderLine에 순서가 있다면 다음과 같이 List 타입을 이용해서 컬렉션을 프로퍼티로 사용할 수 있다.
public class Order { private List<OrderLine> orderLines; ... }
Java
복사
: Order와 OrderLine을 저장하기 위한 테이블은 다음과 같이 매핑 가능하다.
: 밸류 컬렉션을 저장하는 ORDER_LINE 테이블은 외래키를 이용해서 엔티티에 해당하는 PURCHASE_ORDER 테이블을 참조한다. 이 키는 컬렉션이 속할 엔티티를 의미한다. List 타입의 컬렉션은 인덱스 값이 필요하므로 ORDER_LINE 테이블에는 인덱스 값을 저장하기 위한 별도 칼럼인 line_idx도 존재한다.
: 밸류 컬렉션을 별도 테이블로 매핑할 때는 @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; ... } @Embeddable public class OrderLine { @Embedded private ProductId productId; ... }
Java
복사
: OrderLine의 매핑을 함께 표시했는데 OrderLine에는 List의 인덱스 값을 저장하기 위한 프로퍼티가 존재하지 않는다. 이는 List 타입 자체가 인덱스를 갖고 있기 때문이다. JPA는 @OrderColumn 애너테이션을 이용해서 지정한 칼럼에 리스트의 인덱스 값을 저장한다.
: @CollectionTable은 밸류를 저장할 테이블을 지정, name 속성은 테이블의 이름을 지정해야 하며 joinColumns 속성은 외래키로 사용할 칼럼을 지정한다. 위 코드의 경우 외래키가 하나인 경우로, 2개 이상인 경우에는 @JoinColumn의 배열을 이용해서 외래키 목록을 지정한다.

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

: 밸류 컬렉션을 별도 테이블이 아닌 한 개 칼럼에 저장해야할 때가 있다. 도메인 모델 상에서는 Set 자료구조로 데이터를 저장하고 데이터베이스에서는 이를 콤마로 구분하여 하나의 문자열로 저장해야한다고 할때, AttributeConverter를 사용하면 밸류 컬렉션을 한 개 칼럼에 쉽게 매핑할 수 있다.
// 밸류 객체 생성 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); } }
Java
복사
@Converter public class EmailSetConveter 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); } }
Java
복사
⇒ 이렇게 구현이 끝난 후에는 단순히 @Convert(converter = EmailSetConveter)을 Entity의 EmailSet 타입 컬렉션에 추가해주면 된다.

7. 밸류를 이용한 ID 매핑

: 식별자라는 의미를 부각시키기 위해 식별자 자체를 밸류 타입으로 만들 수도 있다. 이는 예제에서 OrderNo, MemberId 등으로 이 경우에는 @Id 대신 @EmbeddedId 애너테이션을 사용한다.
@Entity @Table(name = "purchase_order") public class Order { @EmbeddedId private OrderNo number; ... } // JPA에서 식별 자 타입은 Serializable 타입이어여 하므로 식별자를 사용할 밸류 타입은 Serializable 인터페이스를 상속 받아야 한다. @Embeddable public class OrderNo implements Serializable { @Column(name = "order_number") private String number; ... }
Java
복사
: 중요) JPA에서 식별자 타입은 Serializable 타입이어야 하므로 식별자로 사용할 밸류 타입은 Serializable 인터페이스를 상속받아야 한다.
: 밸류 타입으로 식별자를 구현할 때 얻을 수 있는 장점은 식별자에 기능을 추가할 수 있다는 것, 예를 들어 주문번호가 버전이 업데이트되면서 변경되었다면 주문번호에 업데이트 전, 후를 구분할 수 있는 기능을 넣을 수 있다.
: JPA는 내부적으로 엔티티를 비교할 목적으로 equals()와 hashcode() 값을 사용하므로 식별자로 사용할 밸류 타입은 이 두 메서드를 알맞게 구현해야 한다.

8. 별도 테이블에 저장하는 밸류 매핑

: 애그리거트에서 루트 엔티티를 뺀 나머지 구성요소는 대부분이 밸류 타입이다. 루트 엔티티 외에 또 다른 엔티티가 있다면 이는 진짜 엔티티인지 의심해봐야하는 부분이다.
: 단지 별도 테이블에 데이터를 저장한다고 해서 엔티티가 아니다. 주문 애그리거트 또한 OrderLine을 다른 테이블에 저장하지만 OrderLine 자체는 밸류이다.
: 밸류가 아닌 엔티티가 확실하다면 해당 엔티티가 다른 애그리거트인지는 아닌지 확인해야 한다. 특히 자신만의 독자적인 라이프 사이클을 갖는다면 구분되는 애그리거트일 가능성이 높다.
: 애그리거트에 속한 객체가 밸류인지 엔티티인지 구분하는 방법은 고유 식별자를 갖는지 확인하면 된다.
: 식별자를 찾을 때 매핑되는 테이블의 식별자를 애그리거트의 구성요소의 식별자와 동일한 것으로 착각하면 안된다.
: 별도의 테이블에 PK가 있다고 해서 테이블과 매핑되는 애그리거트 구성요소가 항상 고유 식별자를 갖는 것은 아니기 때문이다.
: 그림만 봤을 때 ARTICLE_CONTENT 테이블의 ID 칼럼이 식별자이므로 ARTICLE_CONTENT와 매핑되는 ArticleContent를 엔티티로 생각해서 Article과 ArticleContent를 두 엔티티 간의 1-1 연관으로 매핑할 수 있지만 ArticleContent를 Article의 내용을 담고 있는 밸류 타입으로 보는 것이 맞다.
: 이는 ARTICLE_CONTENT의 ID는 식별자지만 이 식별자를 사용하는 이유는 ARTICLE과 연관관계 설정을 위함이지 별도의 식별자는 아니기 때문이다.
: ArticleContent는 밸류이므로 @Embeddable로 매핑한다.
: ArticleContent와 매핑되는 테이블은 Artible과 매핑되는 테이블과 다른데, 이때 밸류를 매핑한 테이블을 지정하기 위해 @SecondaryTable@AttributeOverride를 사용한다.
: @SecondaryTable의 name 속성은 밸류를 저장할 테이블을 지정한다.
@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")), @AttributeOverride(name = "contentType", column = @Column(table = "article_content")) }) @Embedded private ArticleContent content; ... }
Java
복사
: @SecondaryTable을 이용하면 아래 코드를 실행할 때 두 테이블을 조인해서 데이터를 조회한다.
: 한 가지 단점은 @SecondaryTable를 사용하면 목록 화면에 Article을 조회할 때 article_content 테이블까지 조인해서 테이터를 읽어오는데 이는 원하는 결과가 아니며 5장의 조회 전용 쿼리를 실행하여 해결할 수 있다.

9. 밸류 컬렉션을 @Entity로 매핑하기

: 개념적으로 밸류이나 구현 기술의 한계나 팀 표준으로 인해 @Entity를 사용해야 할 때도 있다.
: 예로 제품의 이미지 업로드 방식에 따라 이미지 경로와 섬네일 이미지 제공 여부가 달라질 때, JPA는 @Embeddable 타입의 클래스 상속 매핑을 지원하지 않는데 상속 구조를 갖는 밸류 타입을 사용할려면 @Entity를 이용해서 상속 매핑으로 처리해야 한다.
: @Entity로 매핑하기에 식별자와 구현 클래스 구분을 위한 타입 식별 칼럼을 추가해야한다.

10. 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; ... }
Java
복사