Spring boot 도메인 분석 설계

2022. 11. 18. 14:11강의 정리/Spring 기초

반응형

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-%ED%99%9C%EC%9A%A9-1/dashboard

 

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런 | 강의

실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., - 강

www.inflearn.com

 

소프트웨어 개발 단계 

 

1. 요구사항 분석 // 이번 포스팅에서 다룰 주제

  • 문제 분석 단계
  • 개발할 소프트웨어의 기능과 제약조건, 목표등을 소프트웨어 사용자와 함께 명확히 정의
  • 요구명세서 작성

 

2. 시스템 명세

  • 시스템이 무엇을 수행해야하는 가를 정의
  • 시스템 기능 명세서 작성

 

3. 설계

  • 시스템 명세 단계에서 정의한 기능을 실제로 수행하기 위한 방법을 논리적을 결정하는 단계
    • 시스템 구조 설계 - 시스템을 구성하는 내부 프로그램이나 모듈간의 관계와 구조 설계
    • 프로그램 설계 -  프로그램 내의 각 모듈에서의 처리 절차나 알고리즘을 설계
    • 사용자 인터페이스 설계 - 사용자가 시스템을 사용하기 위해 보여지는 화면 설계

 

4. 프로그래밍

  • 설계 단계에서 논리적을 결정한 알고리즘을 프로그래밍 언어를 사용하여서 실제 프로그램을 작성하는 단계

 

5. 테스트

  • 1. 단위 테스트
    • 단위 테스트는 응용 프로그램에서 테스트 가능한 가장 작은 소프트웨어를 실행하여 예상대로 동작하는지 확인하는 테스트이다.
  • 2. 통합 테스트 
    • 통합 테스트는 단위 테스트보다 더 큰 동작을 달성하기 위해 여러 모듈들을 모아 이들이 의도대로 협력하는지 확인하는 테스트이다.
  • 3. 인수 테스트 
    • 인수 테스트는 사용자 스토리(시나리오)에 맞춰 수행하는 테스트이다.

 

6. 유지보수

 

 


 

도메인 분석 설계

1. 요구사항 분석

요구사항 명세서가 공통적으로 가지고 있는 특징들

  • ID : 내부 규칙에 따라 식별자를 부여한다. 하나의 요구사항에 하나의 식별자이다. 
  • 화면명 : 어느 화면에서 구현할 기능인지 기재한다. 화면에 속하지 않는 요구사항도 있을 수 있다.
  • 요구사항명 : 요구사항의 설명을 요약하여 기재한다.
  • 내용 : 요구사항의 설명을 요약하여 기재한다.
  • 중요도 : 상중하, 1~5 등 내부 규칙에 따라 부여한다.
  • 부서 / 작성자 : 요구사항을 기재한 담당자를 기재한다. 부서가 없는 경우 생략해도 무방하다.
  • 날짜 : 요구사항을 기재한 날짜를 명시한다.
  • 진행사항 (구현 여부) : 검토 예정, 진행 확정, 진행 불가, 추후 진행등 내부에서 결정된 사항을 기재한다.
  • 버젼명 : 요구사항이 변경될 수 있으므로 버전으로 표기한다.
  • 그 외 : 유형, 출처등

 

위의 양식을 따라 해당 강의 프로젝트의 요구사항 명세서를 작성해보면 다음과 같다. 

 

ID 화면명 중요도 요구사항 내용 날짜 작성자 진행사항 버젼명
0001 회원 5 회원가입을 할 수있다. 11/16 동백 반영 0.1.0
0002 회원 3 찾고자 회원을 조회하여 찾을 수 있다. 11/16 동백 반영 0.1.5
0003 상품 4 상품을 등록한다. 11/24 고길동 미반영 0.2.0
0004 상품 3 등록된 상품들을 볼 수 있다. 12/1 둘리 미반영 0.2.5
0005 주문 4 상품을 주문한다. 12/1 송풍 미반영 0.2.5
0006 주문 4 주문했던 상품들의 내역을 볼 수 있다.  12/4 풍풍 반영 0.3.0
0007 상품 3 상품은 재고 관리가 필요하다. 12/10 동백 미반영 0.3.0
0008 상품 4 상품의 종류는 카테고리로 분류할 수 있다. 12/10 동백 미반영 0.3.0
0009 주문 5 상품 주문시 배송 정보를 입력할 수 있다. 12/10 동백 미반영 0.3.0

 


 

2. 도메인 모델과 테이블 설계

데이터 모델링 단계

 

1. 업무 파악 : 요구사항 명세서등을 작성하면서, 만들고자 하는 도메인에 대해서 파악해야한다.

  • 위의 요구사항 명세서 참고

 

 

2. 개념적 데이터 모델링 :  개념적 데이터 모델링은 내가 하고자하는 일의 데이터 간의 관계를 구상하는 단계이다.

  • 핵심 엔터티와 그들간의 관계를 발견하고, 그것을 표현하기 위해서 엔터티-관계 다이어그램(ER 다이어그램)을 생성하는 것이다.

 

도메인 모델과 테이블 설계

 

 

3. 논리적 데이터 모델링 : 구체화된 업무중심의 데이터 모델을 만들어 내는데 이것을 논리적인 데이터 모델링이라 한다.

  • 이 단계에서 업무에 대한 Key, 속성, 관계등을 표시하며, 정규화 활동을 수행한다.

 

 

회원 엔티티 분석

  • 회원(Member): 이름과 임베디드 타입인 주소( Address ), 그리고 주문( orders ) 리스트를 가진다.

 

  • 주문(Order): 한 번 주문시 여러 상품을 주문할 수 있으므로 주문과 주문상품( OrderItem )은 일대다 관계
    다. 주문은 상품을 주문한 회원과 배송 정보, 주문 날짜, 주문 상태( status )를 가지고 있다. 주문 상태는 열
    거형을 사용했는데 주문( ORDER ), 취소( CANCEL )을 표현할 수 있다.

 

  • 주문상품(OrderItem): 주문한 상품 정보와 주문 금액( orderPrice ), 주문 수량( count ) 정보를 가지고
    있다. (보통 OrderLine , LineItem 으로 많이 표현한다.)

 

  • 상품(Item): 이름, 가격, 재고수량( stockQuantity )을 가지고 있다. 상품을 주문하면 재고수량이 줄어든
    다. 상품의 종류로는 도서, 음반, 영화가 있는데 각각은 사용하는 속성이 조금씩 다르다.

 

  • 배송(Delivery): 주문시 하나의 배송 정보를 생성한다. 주문과 배송은 일대일 관계다.

 

  • 카테고리(Category): 상품과 다대다 관계를 맺는다. parent , child 로 부모, 자식 카테고리를 연결한
    다.

 

  • 주소(Address): 값 타입(임베디드 타입)이다. 회원과 배송(Delivery)에서 사용한다.

 

 

강사님의 한마디

 

JPA 운영 단계에서는 다대다 관계를 쓰면 안된다! -> 일대다, 다대일로 풀어내야한다.

이는 아래 사진을 통해 설명하겠다.

 

 

양방향 관계가 아닌 일방향 관계를 사용해라

-> 예를 들어 위 ERD에서 멤버 < - > 주문의 경우 양방향으로 관계가 맺어져있는데(외래키) 시스템은 서로를 아예 동등하게 봄으로 주문이 멤버를 필요로 한다고 보는게 맞다.

 

 

회원 테이블 분석

  • ITEM: 앨범, 도서, 영화 타입을 통합해서 하나의 테이블로 만들었다. DTYPE 컬럼으로 타입을 구분한다.
    • 싱글 테이블 전략

 

JPA 운영 단계에서는 다대다 관계를 쓰면 안된다! -> 일대다, 다대일로 풀어내야한다.

  • 객체지향에서는 카테고리와 아이템 테이블을 서로 참조하는 것이 가능하다. 그러나 관계 데이터베이스는 그게 힘들다.
  • 그렇기에 중간 카테고리_아이템 테이블을 만들어서 카테고리(1) <-> 카테고리_아이템(N), 카테고리_아이템(N) <-> 아이템(1)을 만들었다.

 

  • 회원과 주문: 일대다 , 다대일의 양방향 관계다. 따라서 연관관계의 주인을 정해야 하는데, 외래 키가 있는 주
    문을 연관관계의 주인으로 정하는 것이 좋다. 그러므로 Order.member 를 ORDERS.MEMBER_ID 외래 키와
    매핑한다.

 

  • 주문상품과 주문: 다대일 양방향 관계다. 외래 키가 주문상품에 있으므로 주문상품이 연관관계의 주인이다. 
    그러므로 OrderItem.order 를 ORDER_ITEM.ORDER_ID 외래 키와 매핑한다.

 

  • 주문상품과 상품: 다대일 단방향 관계다. OrderItem.item 을 ORDER_ITEM.ITEM_ID 외래 키와 매핑한
    다.

 

  • 주문과 배송: 일대일 양방향 관계다. Order.delivery 를 ORDERS.DELIVERY_ID 외래 키와 매핑한다.
    카테고리와 상품: @ManyToMany 를 사용해서 매핑한다.(실무에서 @ManyToMany는 사용하지 말자. 여기
    서는 다대다 관계를 예제로 보여주기 위해 추가했을 뿐이다

 

 

강사님의 한마디

외래 키가 있는 곳(N측)을 연관관계의 주인으로 정해라.
> 연관관계의 주인은 단순히 외래 키를 누가 관리하냐의 문제이지 비즈니스상 우위에 있다고 주인으로 정하면
안된다.. 예를 들어서 자동차와 바퀴가 있으면, 사람들은 당연히 자동차가 중요하다고 생각을 하는데 이는 DB 설계에서는 잘못된 것이다. 일대다 관계에서 항상 다쪽에 외래 키가 있으므로 외래 키가 있는 바퀴를 연관관계의 주인으로 정하면 된다. 물론 자동차를 연관관계의 주인으로 정하는 것이 불가능 한
것은 아니지만, 자동차를 연관관계의 주인으로 정하면 자동차가 관리하지 않는 바퀴 테이블의 외래 키 값이
업데이트 되므로 관리와 유지보수가 어렵고, 추가적으로 별도의 업데이트 쿼리가 발생하는 성능 문제도 있

 

 


 

4. 물리적 데이터 모델링 : 최종적을 데이터를 관리할 데이터베이스를 선택하고, 선택한 데이터베이스에 실제 테이블을 만드는 작업을 말한다.

 

엔티티 클래스 개발

  • 실무에서는 가급적 Getter는 열어두고, Setter는 꼭 필요한 경우에만 사용하는 것을 추천
    • 이론적으로 Getter, Setter 모두 제공하지 않고, 꼭 필요한 별도의 메서드를 제공하는게 가장 이상적
      이다. 하지만 실무에서 엔티티의 데이터는 조회할 일이 너무 많으므로, Getter의 경우 모두 열어두는 것이
      편리하다. Getter는 아무리 호출해도 호출 하는 것 만으로 어떤 일이 발생하지는 않는다. 하지만 Setter는
      문제가 다르다. Setter를 호출하면 데이터가 변한다. Setter를 막 열어두면 가까운 미래에 엔티티에가 도대
      체 왜 변경되는지 추적하기 점점 힘들어진다. 그래서 엔티티를 변경할 때는 Setter 대신에 변경 지점이 명확
    • 즉 엔티티에는 가급적 Setter를 사용하지 말자

 

DDL로 생성 후 반드시 그대로 사용하는 것이 아닌 만들어진 테이블과 필드들을 검토하기

 

회원 엔티티

package com.jpabook.jpashop.domain;

import lombok.Getter;
import lombok.Setter;

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

@Entity
@Getter @Setter // Lombok을 사용하기에 게터 세터 어노테이션 가능
public class Member {

    @Id @GeneratedValue
    // 엔티티는 타입(ex:Meber)가 있으므로, id 필드만으로 구별이 쉽지만, DB의 경우 타입이 없기에 id라고 이름을 지으면 구별하기 어렵다.
    @Column(name = "member_id") // 보통 '테이블명+id'를 많이 사용한다. member_id라는 컬럼에 매핑한다.
    private Long id;

    private String username;

    @Embedded // 내장 타입을 포함했다는 의미
    private Address address;

    // 양방향 연관관계에서 주인이 아닌 쪽(1)은 MappedBy 속성을 넣어주고, 값은 연관 관계 주인인 Order.member
    // mappedBy를 지정해주면, 읽기 전용 필드가 되고, 값을 생성, 변경할 수 없다.
    // Order 테이블에 있는 member 필드에 의해서 나는 매핑된거야!
    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();
}

 

 

주문 엔티티

package com.jpabook.jpashop.domain;

import lombok.Getter;
import lombok.Setter;

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

@Entity
@Table(name = "orders") // 테이블 이름을 지정하지않으면 자동으로 Order
@Getter @Setter
public class Order {

    @Id @GeneratedValue
    @Column(name = "order_id") // DB 컬럼 명을 DB이름_필드이름
    private Long id;

    // 회원(1) : 주문(N)이기에, 주문(N)Many에 to 회원 외래키(FK)One를 둔다.
    @ManyToOne(fetch = FetchType.LAZY) // FetchType.Lazy는 JPA의 프록시와 지연로딩의 기초적인 내용을 공부해야한다.
    @JoinColumn(name = "member_id") // FK이름 지정
    private Member member; // Order.member

    // 양방향 연관관계 -> 양쪽에 모두 서로의 외래키가 존재하는 경우
    // Db는 외래키 하나만 바꾸면 되지만, 객체 지향은 둘 다 바꿔줘야 한다. 이 간극을 해결하기 위해서 객체 지향에서 둘 중 하나를 주인으로 정한다.
    // 이 경우는 연관관계 주인을 정해주어야 한다. 그 이유는 Order.member를 통해 값을 바꿀 수도있고, Member.orders를 통해 값을 바꿀 수 있기에
    // 예를 들어 Order.member는 값을 변경했는데, 연관된 Member.orders는 값이 변경되지 않은 경우 둘 중 뭘 믿어야할지 JPA는 어려워함
    // 그래서 둘 중 N측(Order)을 주인으로 설정한다.
    // 그렇게하면 Order.member를 변경했을 경우, JPA는 Order.member를 변경했다는 것을 인지하고, 그 것을 중점으로 DB 업데이트를 한다.


    // 양방향 연관관계에서 주인이 아닌 쪽(1)은 MappedBy 속성을 넣어주고, 값은 연관 관계 주인인 OrderItem.order
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) // 부모 엔티티가 영속화될 때 자식 엔티티도 같이 영속화되고, 부모 엔티티가 삭제될 때 자식 엔티티도 삭제되는 등 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 전이되는 것을 의미합니다.
    // cascade = CascadeType.ALL를 통해서 orderItems에 데이터를 넣어두고,
    private List<OrderItem> orderItems = new ArrayList<>();

    /* 기존 방식 = 1. Order 엔티티 저장하고 2. 그다음 Order.OrderItem들을 엔티티당 각각 persist를 호출해야 했음.\
        3. 그다음 마지막으로 다시 Order를 Persist 해줘야함.
     persist(orderItemA)
     persist(orderItemB)
     persist(orderItemC)
     persist(order)
     */

    /* 현재 방식 = Casacade.All은 persist를 전파시켜주어서(영속성 전이), Order를 persist하면 Order.orderItems를 같이 persist해준다.
        삭제 또한 마찬가지로 같이 진행해준다.
    persist(order)
     */

    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    // Order.delivery또한 Order가 persist될 때 같이 persist된다.
    @JoinColumn(name = "delivery_id")
    private Delivery delivery; // 배송 정보

    private LocalDateTime orderDate; // 주문 시간
    
    @Enumerated(EnumType.STRING)
    private OrderStatus status; // 주문상태 [ORDER. CANCEL]

    //==연관 관계 메소드==//
    /*
    양방향 관계의
    Order랑 Member가 있을 때, Member가 주문하면 Member.orders에도 넣어주어야한다.
    그리고 Order.member에도 저장을 해야한다.

     */

    public void setMember(Member member){ // 주문한 멤버를 세팅할 때,
        this.member = member; // Order.member에 멤버를 넣어준다.
        member.getOrders().add(this); // Order.member.orders에도 해당 주문을 넣어준다. 즉 this는 Order 객체를 가리킨다.
    }

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

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

 

 

 

주소 엔티티

package com.jpabook.jpashop.domain;

import lombok.Getter;

import javax.persistence.Embeddable;

@Embeddable // JPA Entity안의 Column을 하나의 객체로써 사용을 하고 싶을 때 사용, 어딘가에 내장될 수 있음을 알림
@Getter // 값 타입은 반드시 Getter만 사용하자. 그 이유는 생성될 때의 값이 변경되면 안되기 때문이다.
public class Address {

    // 다른 엔티티에서 Address안의 엔티티를 Address라는 하나의 객체로 사용할 수 있다.
    private String city;
    private String streets;
    private String zipcode;

    // public, protected중 protected로 설정하는 것을 추천
    protected Address() { // JPA가 생성할 때 @Embeddable가 있는 클래스는 리프랙션, 프록시등의 기술을 사용하기 위해서,기본 생성자를 요구한다.

    }

    // > 참고: 값 타입은 변경 불가능하게 설계해야 한다.
    //> @Setter 를 제거하고, 생성자에서 값을 모두 초기화해서 변경 불가능한 클래스를 만들자. JPA 스펙상 엔티
    // 티나 임베디드 타입( @Embeddable )은 자바 기본 생성자(default constructor)를 public 또는
    // protected 로 설정해야 한다. public 으로 두는 것 보다는 protected 로 설정하는 것이 그나마 더 안전
    // 하다.
    //> JPA가 이런 제약을 두는 이유는 JPA 구현 라이브러리가 객체를 생성할 때 리플랙션 같은 기술을 사용할 수있도록 지원해야 하기 때문이다
    public Address(String city, String streets, String zipcode) {
        this.city = city;
        this.streets = streets;
        this.zipcode = zipcode;
    }

}

 

 

주문 엔티티

package com.jpabook.jpashop.domain;

import lombok.Getter;
import lombok.Setter;

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

@Entity
@Table(name = "orders") // 테이블 이름을 지정하지않으면 자동으로 Order
@Getter @Setter
public class Order {

    @Id @GeneratedValue
    @Column(name = "order_id") // DB 컬럼 명을 DB이름_필드이름
    private Long id;

    // 회원(1) : 주문(N)이기에, 주문(N)Many에 to 회원 외래키(FK)One를 둔다.
    @ManyToOne(fetch = FetchType.LAZY) // FetchType.Lazy는 JPA의 프록시와 지연로딩의 기초적인 내용을 공부해야한다.
    @JoinColumn(name = "member_id") // FK이름 지정
    private Member member; // Order.member

    // 양방향 연관관계 -> 양쪽에 모두 서로의 외래키가 존재하는 경우
    // Db는 외래키 하나만 바꾸면 되지만, 객체 지향은 둘 다 바꿔줘야 한다. 이 간극을 해결하기 위해서 객체 지향에서 둘 중 하나를 주인으로 정한다.
    // 이 경우는 연관관계 주인을 정해주어야 한다. 그 이유는 Order.member를 통해 값을 바꿀 수도있고, Member.orders를 통해 값을 바꿀 수 있기에
    // 예를 들어 Order.member는 값을 변경했는데, 연관된 Member.orders는 값이 변경되지 않은 경우 둘 중 뭘 믿어야할지 JPA는 어려워함
    // 그래서 둘 중 N측(Order)을 주인으로 설정한다.
    // 그렇게하면 Order.member를 변경했을 경우, JPA는 Order.member를 변경했다는 것을 인지하고, 그 것을 중점으로 DB 업데이트를 한다.


    // 양방향 연관관계에서 주인이 아닌 쪽(1)은 MappedBy 속성을 넣어주고, 값은 연관 관계 주인인 OrderItem.order
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) // 부모 엔티티가 영속화될 때 자식 엔티티도 같이 영속화되고, 부모 엔티티가 삭제될 때 자식 엔티티도 삭제되는 등 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 전이되는 것을 의미합니다.
    // cascade = CascadeType.ALL를 통해서 orderItems에 데이터를 넣어두고,
    private List<OrderItem> orderItems = new ArrayList<>();

    /* 기존 방식 = 1. Order 엔티티 저장하고 2. 그다음 Order.OrderItem들을 엔티티당 각각 persist를 호출해야 했음.\
        3. 그다음 마지막으로 다시 Order를 Persist 해줘야함.
     persist(orderItemA)
     persist(orderItemB)
     persist(orderItemC)
     persist(order)
     */

    /* 현재 방식 = Casacade.All은 persist를 전파시켜주어서(영속성 전이), Order를 persist하면 Order.orderItems를 같이 persist해준다.
        삭제 또한 마찬가지로 같이 진행해준다.
    persist(order)
     */

    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    // Order.delivery또한 Order가 persist될 때 같이 persist된다.
    @JoinColumn(name = "delivery_id")
    private Delivery delivery; // 배송 정보

    private LocalDateTime orderDate; // 주문 시간
    
    @Enumerated(EnumType.STRING)
    private OrderStatus status; // 주문상태 [ORDER. CANCEL]

    //==연관 관계 메소드==//
    /*
    양방향 관계의
    Order랑 Member가 있을 때, Member가 주문하면 Member.orders에도 넣어주어야한다.
    그리고 Order.member에도 저장을 해야한다.

     */

    public void setMember(Member member){ // 주문한 멤버를 세팅할 때,
        this.member = member; // Order.member에 멤버를 넣어준다.
        member.getOrders().add(this); // Order.member.orders에도 해당 주문을 넣어준다. 즉 this는 Order 객체를 가리킨다.
    }

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

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

 

주문아이템 엔티티

package com.jpabook.jpashop.domain;

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

import javax.persistence.*;

@Entity
@Table(name="order_item")
@Getter @Setter
public class OrderItem { // 주문(1) - 주문&아이템(N) - 아이템(1)

    @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; // 주문 당시의 수량

}

 

아이템 엔티티

package com.jpabook.jpashop.domain.Item;

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

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

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 상속관계 전략을 부모 엔티티(클래스)에 해주어야한다.
// 싱글 테이블 전략시 부모 엔티티를 기준으로 자식 엔티티들이 모두 한 테이블에 생성된다.
@DiscriminatorColumn(name = "dtype") // type별로 구분하는 것
@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> cagegories = new ArrayList<Category>();
}

 

앨범 엔티티

package com.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 com.jpabook.jpashop.domain.Item;

import lombok.Getter;
import lombok.Setter;

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

@Entity
@DiscriminatorValue("B") // 싱글 테이블이기에, DB에서는 자식 테이블들을 분리할 기준이 필요하다.
@Getter @Setter
public class Book extends Item {

    private String author;
    private String isbn;
}

 

영화 엔티티

package com.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 com.jpabook.jpashop.domain;

import com.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는 최대한 피하는게 좋고, 중간 테이블을 만들어주는 게 맞다.
    // ex) 카테고리 - 카테고리&아이템 - 아이템
    // 다대다관계는 @JoinTable을 통해서 중간 테이블을 만들어주어야한다.
    // @JoinTable은 각 테이블들의 PK값들을 인자로 받아서, 중계 테이블을 만들어준다.
    // 실전에서 사용하지않는 이유는 중간 테이블에 각 테이블 PK 말고는 값을 추가할 수 없기 때문이다.
    @ManyToMany
    @JoinTable(name = "category_item",
            joinColumns = @JoinColumn(name = "category_id"), // joinColumns는 중간 테이블에 있는 category_id
            inverseJoinColumns = @JoinColumn(name = "item_id")) // inverseJoinColumns는 중간 테이블에 있는 item_id
    private List<Item> items = new ArrayList<>();

    // Category 구조는 같은 엔티티 내의 계층구조이다. 카테고리(부모) 1 : N 카테고리(자식)
    // 부모는 하나이니까 ManyToOne
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Category parent;

    // 자식은 여러개니까 OneToMany
    @OneToMany(mappedBy = "parent") //
    private List<Category> child = new ArrayList<>();

    //==연관관계 메소드==//
    public void addChildCategory(Category child) {
        this.child.add(child);
        child.setParent(this);
    }
}

 

 

배송 엔티티

package com.jpabook.jpashop.domain;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

@Entity
@Getter @Setter
public class Delivery {

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

    // OneToOne 또한 반드시 연관관계의 주인을 정해주어야한다.
    // 이 때 주인은 어느쪽에서 더 많이 참조하는지를 보면서 정하면 된다. 예를 들어서
    // 유저가 주문.배송을 찾는 것과 배송.주문을 찾는 것중 주문.배송을 더 많이 찾기에 주문에 FK를 넣는다.
    @OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY)
    private Order order;

    @Embedded
    private Address address;

    // Enum Type은 무조건 STRING으로 하자. ORDINAL로 할 경우 도중 데이터 변경시 장애가 생긴다.
    @Enumerated(EnumType.STRING)
    private DeliveryStatus status; // ENUM [READY(준비), COMP(배송)]

}

 

 

Enum 클래스들

package com.jpabook.jpashop.domain;

public enum DeliveryStatus {
    READY, COMP
}
package com.jpabook.jpashop.domain;

public enum OrderStatus {
    ORDER, CANCEL
}

 

 

https://galid1.tistory.com/592

 

JPA - Entity의 가독성을 높이자(@Embedded, @Embeddable, @AttributeOverride 사용법)

@Embedded, @AttributeOverride JPA에서는 Entity안의 값을 조금 더 의미있는 값으로 표현하기 위해 응집된 하나의 객체로 데이터를 표현합니다. 이번 포스팅에서는 이 처럼 JPA의 Entity를 조금 더 확실한 의

galid1.tistory.com

 

 

 

엔티티 설계시 주의점

 

  • Setter가 모두 열려있다. 변경 포인트가 너무 많아서, 유지보수가 어렵다. 나중에 리펙토링으로 Setter 제거
    • 세터 없이 개발하는 방법은 추후 설명을 해주신다고 하셨음.

 

  • 모든 연관관계는 지연로딩으로 설정!
    • 즉시로딩은 예를 들어서 멤버를 조회할 때 연관된 주문도 한번에 조회하는 것이다.
    • 즉시로딩( EAGER )은 예측이 어렵고, 어떤 SQL이 실행될지 추적하기 어렵다. 특히 JPQL을 실행할 때 N+1 
      문제가 자주 발생한다.
      • N+1을 쉽게 설명하면 멤버를 조회할 때 연관된 모든 테이블들을 조회하는 것이다.
      • N+1의 뜻은 날린 쿼리는 1개인데, 그에 따른 결과 쿼리는 N개임을 의미한다.
    • 실무에서 모든 연관관계는 지연로딩( LAZY )으로 설정해야 한다.
      연관된 엔티티를 함께 DB에서 조회하고자 하면, fetch join 또는 엔티티 그래프 기능을 사용한다.
    • @XToOne(OneToOne, ManyToOne) 관계는 기본이 즉시로딩이므로 직접 지연로딩으로 설정해야 한
      다.
       
Fetch join과 엔티티 그래프는 뭘까???

 

  • 컬렉션은 필드에서 초기화 하자.
    • What Collection : https://gangnam-americano.tistory.com/41
    • 컬렉션은 필드에서 바로 초기화 하는 것이 안전하다.
    • null 문제에서 안전하다.
    • 하이버네이트는 엔티티를 영속화 할 때, 컬랙션을 감싸서 하이버네이트가 제공하는 내장 컬렉션으로 변경한
      다. 만약 getOrders() 처럼 임의의 메서드에서 컬력션을 잘못 생성하면 하이버네이트 내부 메커니즘에 문
      제가 발생할 수 있다. 따라서 필드레벨에서 생성하는 것이 가장 안전하고, 코드도 간결하다.
JPA에서 컬렉션은 언제 사용될까???

 

 

  • 하이버네이트 기존 구현: 엔티티의 필드명을 그대로 테이블의 컬럼명으로 사용
  • 하이버네이트 신규 구현: 엔티티의 필드명을 테이블의 컬럼명 스타일에 맞게 아래와 같이 바꿔준다. 
    • 스프링 부트 신규 설정 (엔티티(필드) 테이블(컬럼))
    • 카멜 케이스 언더스코어(memberPoint member_point)
    • .(점) _(언더스코어)
    • . 대문자 → 소문자

 

 

 

 


 

 

 

 

반응형

'강의 정리 > Spring 기초' 카테고리의 다른 글

Spring 실전편  (1) 2022.11.15
Spring 기초 정리본  (2) 2022.11.06
자바야 어 반갑다 - 객체 지향 프로그래밍이란?  (0) 2022.11.01