우주먼지
article thumbnail

1. 💡 엔티티 개발

엔티티 클래스를 작성할 때, Getter는 열어두고 Setter는 필요한 경우에만 사용한다.

변경 지점이 명확하도록 변경을 위한 비즈니스 메서드를 별도로 제공한다.

 

기본적으로 단방향 매핑으로 진행하되,
Category < - > Item은 학습을 위해 ManyToMany로 진행하며 실무에서 ManyToMany는 절대 사용하지 말자.

 

기본적으로 Global Fetch 전략은 Lazy로 설정하되 필요에 따라 Eager로 설정한다.

 

추후 createdAt, modifiedAt 같은 엔티티 공통 컬럼에 대한 Auditor를 만들 예정

 

임베디드 값 타입인 Address에 대한 설명을 하자면, 값 타입은 기본적으로 Immutable 해야 하기 때문에
Setter 대신 생성자를 통해서 원본 값 변경이 불가능하게 하고,
@NoArgsConstructor 대신 기본 생성자의 접근제어자를 Protected로 설정하여 더 안전하게 설계한다.

 

1.1. Member

<java />
@Entity @Getter public class Member { // PK @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "member_id") private Long id; @Column private String name; @Embedded private Address address; }

1.2. Order

<java />
@Entity @Getter @Table(name = "orders") public class Order { // PK @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "order_id") private Long id; // 멤버와 주문은 1:N으로 단방향 매핑 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") private Member member; // 주문은 배송과 1:1 관계이다 // 1:1 관계에서 Access가 더 많은 쪽을 키의 주인으로 지정했음 // 주문을 볼때 배송정보가 무조건 필요하니 FetchType은 즉시 전략을 사용한다 @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) @JoinColumn(name = "delivery_id") private Delivery delivery; // 주문 시간 @Column private LocalDateTime orderDate; // 주문의 상태 필드 [ORDER, CANCEL] @Enumerated(EnumType.STRING) private OrderStatus status; // 연관관계 편의 메서드 public void setMember(Member member) { this.member = member; } public void setDelivery(Delivery delivery) { this.delivery = delivery; delivery.setOrder(this); } }

1.3. Item

<java />
@Entity @Getter @Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 상속관계 매핑 전략 지정 @DiscriminatorColumn(name = "dtype") // 하위 클래스에 @DiscriminatorValue 지정해줘야함 public abstract class Item { // PK @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "item_id") private Long id; // 상품 이름 @Column private String name; // 상품 가격 @Column private int price; // 상품 수량 @Column private int stockQuantity; // 다대다 매핑 @ManyToMany(mappedBy = "items") private List<Category> categories = new ArrayList<>(); }

1.4. Address

<java />
/* 임베디드 값 타입 클래스 */ @Embeddable @Getter public class Address { @Column private String city; @Column private String street; @Column private String zipCode; public Address(String city, String street, String zipCode) { this.city = city; this.street = street; this.zipCode = zipCode; } protected Address() {} }

1.5. OrderItem

<java />
/* 주문과 상품을 이어주는 조인 테이블 */ @Entity @Getter public class OrderItem { // PK @Id @GeneratedValue @Column(name = "order_item_id") private Long id; // 상품과 주문상품은 1:N 관계로 단방향 매핑 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "item_id") private Item item; // 주문과 주문상품은 1:N 관계로 단방향 매핑 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "order_id") private Order order; // 주문 가격 @Column private int orderPrice; // 주문 수량 @Column private int count; // 연관관계 편의 메서드 public void setOrder(Order order) { this.order = order; } }

1.6. Delivery

<java />
@Entity @Getter public class Delivery { // PK @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "delivery_id") private Long id; // 배송과 주문은 1:1 관계이다 @OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY) @Setter private Order order; // 임베디드 값 타입 @Embedded private Address address; // 배송 상태 [READY, COMP] @Enumerated(EnumType.STRING) private DeliveryStatus status; }

1.7. Category

<java />
@Entity @Getter public class Category { // PK @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "category_id") private Long id; // 카테고리 이름 @Column private String name; // 다대다는 중간 테이블이 필요하므로 JoinTable로 만들어준다. // joinColumns와 inverseJoinColumns는 조인테이블에 FK 컬럼을 만들어준다. @ManyToMany(fetch = FetchType.LAZY) @JoinTable(name = "category_item", joinColumns = @JoinColumn(name = "category_id"), inverseJoinColumns = @JoinColumn(name = "item_id")) private List<Item> items = new ArrayList<>(); // 카테고리의 계층 구조를 위한 Self 양방향 연관관계 매핑 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent_id") @Setter private Category parent; @OneToMany(mappedBy = "parent") private List<Category> child = new ArrayList<>(); // 연관관계 편의 메서드 public void addChildCategory(Category child) { this.child.add(child); child.setParent(this); } }

1.8. Movie

<java />
@Entity @Getter @DiscriminatorValue("M") public class Movie extends Item { @Column private String director; private String actor; }

1.9. Album

<java />
@Entity @Getter @DiscriminatorValue("A") public class Album extends Item { @Column private String artist; @Column private String etc; }

1.10. Book

<java />
@Entity @Getter @DiscriminatorValue("B") public class Book extends Item { @Column private String author; @Column private String isbn; }

1.11. Enum

<java />
public enum DeliveryStatus { READY, COMP } public enum OrderStatus { ORDER, CANCEL }

1.12. 결과 테이블 확인

엔티티 클래스들을 작성한 후 H2 Database에 테이블이 잘 만들어졌는지 확인한다.


2. 💡 엔티티 설계 주의점

 

2.1. 엔티티는 가급적 Setter 사용 X

  • Setter는 필요할 때만 사용 (유지보수성)

 

2.2. 모든 연관관계는 지연로딩으로 설정

  • Global Fetch 전략은 기본적으로 Lazy로 설정하자.
  • 즉시로딩인 Eager은 예측이 어렵고 어떤 SQL이 실행될지 추적하기 어렵다.
  • 특히 JPQL을 실행할 때 N+1 문제가 자주 발생한다.
  • 연관된 엔티티를 조회하려면 Fetch Join이나 Entity Graph를 사용하자.
  • x:1(1:1, N:1) 관계는 기본적으로 즉시로딩이므로 지연로딩으로 설정해야 한다.

 

2.3. 컬렉션은 필드에서 초기화

  • NPE 방어
  • Hibernate는 엔티티를 Persist 할때, 컬렉션을 감싸서 내장 컬렉션으로 변경한다.
  • getOrders()처럼 임의의 메서드에서 컬렉션을 잘못 생성하면 Hibernate 내부적으로 문제가 발생한다.
  • 따라서 필드레벨에서 컬렉션을 초기화 하는건이 안전하고 코드의 간결성도 챙길 수 있다.

'Inflearn 강의 > JPA 실전 활용' 카테고리의 다른 글

도메인 분석 설계  (0) 2023.03.23
프로젝트 초기 설정  (0) 2023.03.22
profile

우주먼지

@o귤o

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

검색 태그