본문 바로가기

스프링 (인프런)/스프링부트

엔티티 클래스 개발

1. 엔티티 개발

1) Member

@Entity
@Getter @Setter
public class Member {
    @Id @GeneratedValue
    @Column( name = "member_id" )
    private Long id;

    private String name;

    @Embedded
    private Address address;

    @OneToMany( mappedBy = "member" )               // 일대다 관계. 연관관계 주인을 넘겨주기 위해 'mappedBy' 활용
    private List<Order> orders = new ArrayList<>();
}

id와 name은 일반적인 약속 대로 적었다. (이하 엔티티들은 생략)

- Address는 임베디드 엔티티이므로 @Embedded 어노테이션을 달았다.

- Order는 일대다 관계이므로 지네릭 List로 받고, 연관관계의 주인이 order 쪽에 있으므로 mappedBy를 달아준다.

 

 

 

2) Order

@Entity
@Getter @Setter
@Table( name = "orders" )               // DB에서 'ORDER'를 못 받음
public class Order {
    @Id @GeneratedValue
    @Column( name = "order_id" )
    private Long id;

    @ManyToOne( fetch = FetchType.LAZY )                          // 다대일
    @JoinColumn( name = "member_id" )    // FK 설정
    private Member member;

    @OneToMany( mappedBy = "order", cascade = CascadeType.ALL )
    private List<OrderItem> orderItems = new ArrayList<>();

    @OneToOne( fetch = FetchType.LAZY, cascade = CascadeType.ALL)                           // 일대일은 둘 다 연관관계의 주인이 될 수 있다
    @JoinColumn( name = "delivery_id" )  // order에서 delivery를 찾을 경우가 많으므로 연관관계의 주인으로
    private Delivery delivery;

    private LocalDateTime orderDate;    // 주문 시간

    @Enumerated( EnumType.STRING )
    private OrderStatus status;         // 주문 상태 [ORDER, CANCEL]
}

- @Table로 이름을 명시하는 이유는 DB에서 'ORDER'를 예약어로 걸어뒀기 때문이다.

- Member와는 다대일 관계이고 연관관계의 주인이므로 LAZY fetch @JoinColumn으로 FK를 명시해준다.

- OrderItem과는 일대다 관계이므로 mappedBy ALL cascade를 걸어준다.

- Delivery와는 일대일 관계이지만, 연관관계의 주인을 Order 쪽에서 가져가기로 했으므로 @JoinColumn으로 FK를 명시해준다.

- orderDate는 주문 시간을 나타내고 LocalDateType을 활용한다.

- OrderStatus는 열거형인데, 여기서 참고해야 할 점이 하나 있다.

    - 열거형은 DB에 기본적으로 숫자로 들어간다. ( EnumType.ORDINAL )

    - 근데 숫자는 만약 1, 2, 3이 미리 있는 와중에 두번째에 새로운 열거형을 추가하게 되면 곤란해진다.

    - 그래서 가급적 STRING으로 명시하는 것이 추천된다.

 

 

 

3) OrderStatus (enum)

public enum OrderStatus {
    ORDER,
    CANCEL
}

- 주문 상태는 주문주문 취소로만 이뤄져 있다.

- 테이블로 만들어지는 것이 아니고 컬럼으로 쓰이기만 하므로 @Entity를 쓰지 않는다.

 

 

 

4) OrderItem

@Entity
@Table( name = "order_item" )
@Getter @Setter
public class OrderItem {
    @Id @GeneratedValue
    @Column( name = "order_item_id" )
    private Long id;

    @ManyToOne( fetch = FetchType.LAZY )
    @JoinColumn( name = "item_id" )
    private Item item;

    @ManyToOne( fetch = FetchType.LAZY )
    @JoinColumn( name = "order_id" )
    private Order order;

    private int orderPrice;     // 주문 가격
    private int count;          // 주문 수량
}

- 아마 Order에서와 동일한 이유로 @Table을 쓴 것 같다.

- Item과 다대일 관계이므로 LAZY fetch @JoinColumn을 쓴다.

- Order와 다대일 관계이므로 LAZY fetch @JoinColumn을 쓴다.

 

 

 

5) Item

@Entity
@Inheritance( strategy = InheritanceType.SINGLE_TABLE )
@DiscriminatorColumn( name = "dtype" )
@Getter @Setter
public abstract class Item {
    @Id @GeneratedValue
    @Column( name = "item_id" )
    private Long id;

    private String name;

    private int price;

    private int stockQuantity;

    @ManyToMany( mappedBy = "items" )
    private List<Category> categories = new ArrayList<Category>();
}

- Item은 상속관계 매핑으로 Album, Book, Movie 총 세 개의 엔티티를 자식으로 갖는다.

    - 그래서 @Inheritance를 쓰고, SINGLE_TABLE 전략은 이 세 개 엔티티의 어트리뷰트를 나누지 않고 Item에서 안고 간다는 뜻이다.

    - @DiscriminationColumn은 자식 엔티티를 나누는 컬럼의 이름을 'dtype'으로 명시한다.

- 참고로, 비즈니스 로직 상 상품에서 주문상품쪽으로 조회를 갈 일이 없기 때문에 아예 필드로 적지 않았다.

- Category와 다대다 관계라 @ManyToMany를 사용했고, 카테고리쪽에서 상품을 포함하고 제외하는 경우가 많을 것이므로 mappedBy를 걸었다.

 

 

 

6) Album ( Item의 자식 엔티티 )

@Entity
@DiscriminatorValue("A")
@Getter @Setter
public class Album extends Item {
    private String artist;
    private String etc;
}

- @DiscrimiatorValue로 Item 테이블에서 어떤 이름으로 분류될 것인지 명시한다.

- Movie나 Book도 위와 비슷하게 작성하므로 생략한다.

 

 

 

7) Delivery

@Entity
@Getter @Setter
public class Delivery {
    @Id @GeneratedValue
    @Column( name = "delivery_id" )
    private Long id;

    @OneToOne( mappedBy = "delivery", fetch = FetchType.LAZY )
    private Order order;

    @Embedded
    private Address address;

    @Enumerated( EnumType.STRING )
    private DeliveryStatus status;      // READY, COMP
}

- Order와 일대일 관계고 연관관계의 주인을 넘겨줬으므로 mappedBy를 걸었고 LAZY fetch를 두었다.

- Address를 임베디드 엔티티로 가진다.

- Order에서와 비슷하게 @Enumerated의 STRING 타입으로 명시해서 ORDINAL을 피한다.

 

 

 

8) Category

@Entity
@Getter @Setter
public class Category {
    @Id @GeneratedValue
    @Column( name = "category_id" )
    private Long id;

    private String name;

    @ManyToMany
    @JoinTable( name = "category_item",
            joinColumns = @JoinColumn(name = "category_id"),
            inverseJoinColumns = @JoinColumn(name = "item_id"))
    private List<Item> items = new ArrayList<>();

    @ManyToOne( fetch = FetchType.LAZY )
    @JoinColumn( name = "parent_id" )
    private Category parent;

    @OneToMany( mappedBy = "parent" )
    private List<Category> categories = new ArrayList<>();
}

- Item과 다대다 관계다.

    - 그런데 Order와 Item 간 관계와 다르게 엔티티를 두지 않고, DB로 넘어가면서 중간 테이블을 만들기로 했다.

    - 이를 위해 @JoinTable이 필요하고, joinColumns에는 지금 있는 카테고리의 id를, inverseJoinColumns에는 반대편의 id를 건다.

    - 다시 한 번 언급하자면, @JoinTable은 별도의 어트리뷰트를 추가할 수 없기 때문에 실무에서 사용하지 않는다.

- 카테고리는 계층 구조를 나타내줘야 한다.

    - child는 다대일의 관계를 가지므로 @ManyToOne으로 parent를 하나 가지고, @JoinColumns로 FK를 가진다.

    - parent는 일대다의 관계를 가지므로 @OneToMany로 child를 List로 가지고, mappedBy를 건다.

 

 

 

9) Address

@Embeddable
@Getter
public class Address {
    private String city;
    private String street;
    private String zipcode;

    protected Address() { }

    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }
}

- 참고로, 값 타입은 변경이 불가능하도록 설계해야 한다.

    - 그래서 Setter를 달지 않는다.

    - 기본 생성자는 필수이므로 달아는 두되, protected로 접근 제어자를 걸면 좋다.

 

 

 

2. 엔티티 설계시 주의점

1) 엔티티에는 가급적 Setter를 사용하지 말자.

- 애플리케이션 변경 시에 엔티티가 어느 Setter 혹은 다른 메서드에서 바뀌는지 전부 추적해야 하는 부담이 생긴다.

 

 

 

2) 모든 연관관계는 지연로딩으로 설정하자.

- 디폴트 값인 즉시로딩은 관련된 데이터를 연속적으로 전부 끌어오기 때문에 매우 비효율적이다.

- 대신 연관된 엔티티는 fetch join 혹은 엔티티 그래프 기능을 활용한다.

- @OneToOne, @ManyToOne 관계는 기본이 즉시로딩이므로 꼭 바꿔줘야 한다.

 

 

 

3) 컬렉션은 필드에서 초기화하자.

- NullPointerException에서 안전해진다.

- 하이버네이트는 엔티티를 persist()할 때 컬렉션을 감싸서 하이버네이트의 내장 컬렉션으로 변경한다.

Member member = new Member();
System.out.println(member.getOrders().getClass());
em.persist(member);
System.out.println(member.getOrders().getClass());
  
//출력 결과
class java.util.ArrayList
class org.hibernate.collection.internal.PersistentBag

- 그래서 만약 persist() 이후에 member에게 다른 ArrayList 컬렉션을 새롭게 두면 하이버네이트가 관리하지 못하게 된다.

 

 

 

4) 테이블, 컬럼 이름 생성 전략

- 자바에서 써준 필드명과 DB에 등록되는 테이블, 컬럼 이름이 다르다.

- 스프링 부트 신규 설정 ( 엔티티(필드) -> 테이블(컬럼) )

    - 카멜 케이스 -> 언더스코어 : ex) memberPoint -> member_point

    - .(점) -> _(언더스코어)

    - 대문자 -> 소문자

 

 

 

5) Cascade

@OneToMany( mappedBy = "order", cascade = CascadeType.ALL )
private List<OrderItem> orderItems = new ArrayList<>();

- 위 코드와 같이 'cascade = CascadeType.ALL'라고 설정하면,

- persist(order)로만 저장해도 order와 연관된 orderItems가 함께 자동으로 persist()에 호출된다.

 

 

 

6) 연관관계 편의 메서드

// 연관관계 편의 메서드
public void setMember(Member member) {
    this.member = member;
    member.getOrders().add(this);
}

public void addOrderItem(OrderItem orderItem) {
    orderItems.add(orderItem);
    orderItem.setOrder(this);
}

public void setDelivery(Delivery delivery) {
    this.delivery = delivery;
    delivery.setOrder(this);
}

- 양방향 관계에서 한쪽에서 참조하는 쪽의 엔티티를 추가하면 다른 쪽에서도 메서드를 호출할 필요 없도록 한꺼번에 서로 더한다.

- 다만, 이는 꼭 연관관계 주인 쪽에서 가지는 것은 아니고 비즈니스 로직 상 주로 접근하는 쪽에서 하면 된다.

    - 그래서 위 코드에서도 Order 쪽에서 addOrderItem()을 한다.

 

 

 

***

해당 게시글은 인프런 김영한 님의 "실전! 스프링 부트와 JPA 활용" 수업을 기반으로 작성되었습니다.

'스프링 (인프런) > 스프링부트' 카테고리의 다른 글

상품 도메인 개발  (0) 2023.07.06
애플리케이션 구현 준비  (0) 2023.07.05
도메인 모델과 테이블 설계  (0) 2023.07.03
JPA와 DB 설정, 동작확인  (0) 2023.07.03
프로젝트 환경설정  (0) 2023.07.03