스프링부트 JPA활용 1 - 도메인 분석 설계(2)

목차

  • 📌요구사항 분석
  • 📌도메인 모델돠 테이블 설계
  • 📌엔티티 클래스 개발1
  • 📌엔티티 클래스 개발2
  • 📌엔티티 설계시 주의점





📌요구사항 분석

실제 동작 화면

실제 동작 화면



기능 목록

  • 회원 기능
    • 회원 등록
    • 회원 조회
  • 상품 기능
    • 상품 등록
    • 상품 수정
    • 상품 조회
  • 주문 기능
    • 상품 주문
    • 주문 내역 조회
    • 주문 취소
  • 기타 요구사항
    • 상품은 재고 관리가 필요하다.
    • 상품의 종류는 도서, 음반, 영화가 있다.
    • 상품을 카테고리로 구분할 수 있다.
    • 상품 주문시 배송 정보를 입력할 수 있다.





📌도메인 모델과 테이블 설계

도메인모델과 테이블 설계

회원, 주문 , 상품의 관계

  • 회원&주문 = 1:1
    • 회원은 여러 상품을 주문할 수 있다.
  • 주문&주문상품 = many:many
    • 주문을 할 때 여러상품을 주문할 수 있다. 또
      상품을 주문할 때 여러 주문을 할수있어서 다대다 관계가된다. 하지만 다대다 관계는 데이터베이스의 무결성을 해치기때문에 엔티티를 추가해서 일대다, 다대일로 풀어내는게 보통이다.
  • 상품분류
    • 싱품분류는 도서,음반,영화로 구분하며 상품이라는 공통 속성을 갖고있어 상속 구조로 표현 한다.

집접적인 다대다 관계는 피해야 하는 이유

  • 다대다 관계를 사용하면 데이터의 무결성을 위반하게되어 데이터의 삭제와 추가,수정을 할 때 많은 문제가 발생한다. 따라서 연결 테이블을 두어 데이터의 추가, 수정,삭제에 문제가 발생하지 않도록 할 수 있다.

회원 엔티티 분석

회원 엔티티 분석

Member(회원)

  • 스트링타입의 이름, 임베디드 타입의 Adress,리스트타입의 주문을 갖는다.Order(주문)
  • Member타입의 member,List타입의 orderItems, Delivery타입의 delivery,Data타입의orderData, OrderSatus타입의 status가있고 주문상태는 열거형을 사용해 주문(ORDER),취소(CANCEL)을 표현할 수 있다.

    주문은 상품과 다대다 관계이기떄문에 중간에 연결테이블을 두어 주문아이템테이블로 일대 다 관계이다.OrderItem(주문상품)
  • 주문 상품정보와 주문금액(orderPrice),주문수량(count)정보를 갖고 있다.Item(상품)
  • 이름,가격,stockQuntity를 갖고 있다.Delivery(배송)
  • 주문시 하나의 배송정보 생성. 주문과 배송은 일대일 관계CateGory(카테고리)
  • 상품과 다대다 관계를 맺는다.(다대다는 실무에서 사용하지 않지만 예를 위해 사용)parent , child 로 부모, 자식 카테고리를 연결한
    다.Address(주소)
  • 값 타입(임베디드 타입)이다. 회원과 배송(Delivery)에서 사용한다

테이블

테이블 분석

회원 주문

  • 일대다, 다대일의 양방향관계이다. 외래 키가 있는 주문을 연관관계의 주인으로 정하는것이 좋기때문에 주문에 연관관계를 정했다.
    또 일대다 관계에선 무조건 다에 포링키가 있게된다.(주문에 멤버를 갖고있기떄문에 멤버는 조회용으로 사용한다.)주문상품 주문
  • 다대일 양방향 관계이다. 외래 키가 주문상품에있어 주문상품이 연관관계의 주인이며 OrderItem.order 를 ORDER_ITEM.ORDER_ID 외래 키와 매핑한다.주문상품 상품
  • 다대일 단방향 관계다. OrderItem.item 을 ORDER_ITEM.ITEM_ID 외래 키와 매핑한
    주문 배송
  • 일대일 양방향 관계다. Order.delivery 를 ORDERS.DELIVERY_ID 외래 키와 매핑한다카테고리 상품
  • : @ManyToMany 를 사용해서 매핑한다.(실무에서 @ManyToMany는 사용하지 말자. 여기
    서는 다대다 관계를 예제로 보여주기 위해 추가했을 뿐이다





📌엔티티 클래스 개발

회원 엔티티

package jpabook.jpashop.domain;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;


@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 = "member" = order에있는 pk인 member를 읽기만하고 Member에서 수정이 이러나진 않는다는 뜻
    private List<Order> orders = new ArrayList<>();


}
  • @Column(name= "member_id") = pk 칼럼명

    엔티티 타입이 이미 Member이므로 테이블 타입이 없으면 구분 이어려워 관례상 테이블명+id를 많이 사용하여"member_id"를 사용 했다.

주문 엔티티

package jpabook.jpashop.domain;


import lombok.Getter;
import lombok.Setter;


import javax.persistence.Column;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "orders")
@Getter @Setter
public class Order {

    @Id @GeneratedValue
    @Column(name = "order_id")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "member_id") //포링키 이름
    private Member member;

    @OneToMany(mappedBy = "order")
    private List<OrderItem> orderItems = new ArrayList<>();

    @OneToOne
    @JoinColumn(name = "delivety_id")
    private Delivery delivery;

    private LocalDateTime orderData; //주문시간

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

주문 상태

package jpabook.jpashop.domain;

public enum OrderStatus {
    ORDER, CANCEL
}

주문상품 엔티티

package jpabook.jpashop.domain;

import jpabook.jpashop.domain.item.Item;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;

@Entity
@Getter
@Setter
public class OrderItem {

    @Id @GeneratedValue
    @Column(name = "order_item_id")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "item_id")
    private Item item;

    @ManyToOne
    @JoinColumn(name = "order_id") //외래키 맵핑
    private Order order;

    private int orderPrice; // 주문가격

    private int count; //주문 수량

}

상품 엔티티

package jpabook.jpashop.domain.item;

import jpabook.jpashop.domain.Category;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.Entity;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@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<>();
}

상품-도서 엔티티

package jpabook.jpashop.domain.item;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity
@DiscriminatorValue("B")
@Getter@Setter
public class Book extends Item{

    private String author;
    private String isbn;



}

상품-음반 엔티티

package jpabook.jpashop.domain.item;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity
@DiscriminatorValue("A")
@Getter
@Setter
public class Album extends Item{

    private String artist;
    private String etc;
}

상품-영화 엔티티

package jpabook.jpashop.domain.item;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity
@DiscriminatorValue("M")
@Getter
@Setter
public class Movie extends Item{
    private String director;
    private String actor;
}

배송 엔티티

package jpabook.jpashop.domain;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.Entity;
import javax.persistence.*;

@Entity
@Getter @Setter
public class Delivery {

    @Id @GeneratedValue
    @Column(name = "delivery_id")
    private Long id;

    @OneToOne(mappedBy = "delivery")
    private Order order;


    @Embedded
    private Address address;

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

}

배송 상태

package jpabook.jpashop.domain;

public enum DeliveryStatus {
    READY, COMP
}

카페고리 엔티티

package jpabook.jpashop.domain;

import jpabook.jpashop.domain.item.Item;
import lombok.Getter;
import lombok.Setter;


import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@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"), //현재 엔티티를 참조하는 fk
            inverseJoinColumns = @JoinColumn(name = "item_id")) // 반대방향 엔티티를 참조하는 fk
   private List<Item> items = new ArrayList<>();

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private  Category parent;

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

    //@JoinColumn(name = "parent_id")


}

주소 값 타입

package jpabook.jpashop.domain;

import lombok.Getter;

import javax.persistence.Embeddable;

@Embeddable
//유저테이블에 주소데이터를 하나의 주소관련데이터로 만들어 사용한다는 의미이다. ->유저테이블의 아래의 데이터도 같이 생성된다.
// (사실@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를 제거한다.
  • JPA 스펙상 엔티티나 임베디드타입(@Embeddable)은 자바 기본생성자를 public또는 protected로 설정해야하고 protected가 안전하기때 문에 더 권장한다.
  • JPA 가 이러한 제약을 두는 이유는 JPA 구현 라이브러리가 객체를 생성할 때 리플랙션 같은 기술을 사용할 수 있도록 지원해야 하기 때문이다.



📌엔티티 설계시 주의점

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

  • Setter가 모두 열려있으면 변경포인트가 너무 많아서 유지보수가 어렵다

모든 연관관계는 지연로딩으로 설정(중요!! 외우자)

  • 즉시로딩은 예측이 어렵고, 어떤 SQL이 실행될지 추적이 어렵다. 또JPQL을 실행할 때 N+1 문제가 자주 발생한다.
  • 실무에서는 모든 연관관계는 지연로딩으로 설정해야 한다.
  • @(OneToOne, ManyToOne) 관계는 기본전략이 EAGER이기때문에 직접 LAZY전략으로 설정해야한다.즉시로딩(EAGER)란?어떠한 엔티티를 조회할때 조회하고 싶지않은 정보도 조회한 엔티티와 연관되어있으면 한번에 다 조회가 된다.지연로딩(LAZY)란?참조객체들을 무시하고 해당 엔티티의 데이터만 가져온다.