Spring Boot/Spring Boot JPA-활용편1 강의 정리

[SpringBoot-JPA 활용편1] 자바 ORM 표준 JPA 프로그래밍 - 활용편1: 2. 도메인 분석 설계

조찬국 2024. 2. 26. 22:00
728x90

강의 출처:
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

 

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

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

www.inflearn.com

해당 강의는 Inflearn에 등록된 김영한님의 Spring Boot 강의입니다.

이번시간에는 도메인 분석 및 설계를 할것이다. 우선 이번 강의에서 최종적으로 구현하고자 하는 기능을 살펴본 후에 필요한 객체들을 생성하고, 기본편에서 배운 연관관계를 매핑하자.

0. 요구사항 분석 및 기능 목록

개발자는 사용자의 요구사항을 먼저 분석하고, 필요한 기능들을 파악하여 기능 명세서를 작성해야 한다.

이번 강의에서는 위와 같은 기능들을 하는 웹 페이지를 만들어 볼 것이다.

 

(+a 기능 명세서는 요즘 노션 으로 작성하는 편입니다.

아래 사진은 이해를 돕기 위해 저희 팀이 프로젝트에서 만든 기능 명세서를 예를 들겠습니다.)

기능 명세서

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

회원, 주문, 상품의 관계: 회원은 여러 상품을 주문할 수 있다. 그리고 한 번 주문할 때 여러 상품을 선택할 수 있으므로 주문과 상품은 다대다 관계다. 하지만 이런 다대다 관계는 관계형 데이터베이스는 물론이고 엔티티에서도 거의 사용하 지 않는다. 따라서 그림처럼 주문상품이라는 엔티티를 추가해서 다대다 관계를 일대다, 다대일 관계로 풀어냈다.

 

상품 분류: 상품은 도서, 음반, 영화로 구분되는데 상품이라는 공통 속성을 사용하므로 상속 구조로 표현했다.

(아래 사진에서 오른쪽 사진을 보고 엔티티를 만들면 됩니다.)

연관관계 매핑 분석

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

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

주문상품과 상품: 다대일 단방향 관계다. OrderItem.itemORDER\_ITEM.ITEM_ID 외래 키와 매핑한다.

주문과 배송: 일대일 양방향 관계다. Order.deliveryORDERS.DELIVERY_ID 외래 키와 매핑한다.

카테고리와 상품: @ManyToMany 를 사용해서 매핑한다.(실무에서 @ManyToMany는 사용하지 말자. 여기서는 다대 다 관계를 예제로 보여주기 위해 추가했을 뿐이다)

2. 엔티티 클래스 개발

domain

 

ltem상속을 표현을 위해 domain 패키지내 따로 패키지를 만들어서 사용했습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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")
    private List<Order> orders=new ArrayList<>();


}

Order 객체

package jpabook.jpashop.domain;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name="orders")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)//기본생성자를 protected로 생성하는 롬복
public class Order {
    @Id
    @GeneratedValue
    @Column(name="order_id")
    private Long id;

    //멤버와의 연관관계 설정(order가 many임)
    @ManyToOne(fetch = FetchType.LAZY) //★★"X"toOne관계는 LAZY 지연로딩으로 설정 해야함.
    @JoinColumn(name="member_id") //외래키(주문 회원)
    private Member member;

    //OrderItem(주문 상품)과의 연관관계 설정
    //영속성전이(cascade)도 설정(부모인 order에 매핑하고, order에 저장하면 연관된 엔티티도 영속상태로 만들 수 있음.)
    //★한 주문에 여러 아이템 가능-> 일일이 em.persist(item1,2,3..) 하면 불편함. 이런 매핑에는 영속성 전이 필요
    @OneToMany(mappedBy = "order",cascade = CascadeType.ALL)
    private List<OrderItem> orderItems=new ArrayList<>();

    //배송과의 연관관계 설정, 주문 하면 배달도 자동으로 영속성 전이(cascade)하게
    @OneToOne(cascade = CascadeType.ALL,fetch = FetchType.LAZY)
    @JoinColumn(name = "delivery_id") //외래키
    private Delivery delivery; //배송 정보

    private LocalDateTime orderDate;//주문 시간

    @Enumerated(EnumType.STRING)//ENUM은 무조건 String 타입으로 설정해야 한다
    private OrderStatus status;// 주문 상태..enum 클래스 생성-> (ORDER,CANCEL 둘중에 하나)

    //★★★연관관계 메서드(양방향 관계 설정)★★★

    /*1. 양방향 연관관계 방법1.( 둘다 따로따로 설정)...원래 양방향 세팅 방법
    public static void main(String[] args) {
        Member member=new Member();
        Order order=new Order();
        //양방향 설정
        member.getOrders().add(order); //멤버의 Order 리스트에 order객체 추가
        order.setMember(member); //order의 멤버를 세팅
    }
    */

    //2. ★★연관관계 편의 메서드를 이용하는 방법 ┐
    // 다쪽인 외래키가 갖고 있는 애가 설정하거나 콘트롤 하는쪽이 좋다.

    //Member
    public void setMember(Member member)
    {
        this.member=member; //1. 오더에서 외래키인 this.member 설정
        member.getOrders().add(this); //2. 멤버에서 오더 설정( this->Order 자신의 객체변수)
    }
    //Delivery
    public void setDelivery(Delivery delivery)
    {
        this.delivery=delivery; //1.오더에서 외래키인 this.delivery를 설정
        delivery.setOrder(this);//2. 일대일 이므로 리스트에 추가하는게 아님.
    }
    //OrderItem,Order 연관관계 추가
    public void addOrderItem(OrderItem orderItem)
    {
        orderItems.add(orderItem); //1. 오더에 orderItem 설정
        orderItem.setOrder(this); //2. orderItem에 오더설정
    }

}

주문 상태

public enum OrderStatus {
    ORDER,CANCEL
}

OrderItem객체

package jpabook.jpashop.domain;

import jakarta.persistence.*;
import jpabook.jpashop.domain.item.Item;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity @Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)//기본생성자를 protected로 생성하는 롬복
public class OrderItem {


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

    //Item객체에 대한 연관관계 매핑
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "item_id")
    private Item item;

    //Order에 대한 연관관계 매핑
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="order_id")
    private Order order; //주문과의 연관관계

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

}

Item객체

package jpabook.jpashop.domain.item;

import jakarta.persistence.*;
import jpabook.jpashop.domain.Category;
import jpabook.jpashop.exception.NotEnoughStockException;
import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;

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


    //상속관계 매핑
    private String name;
    private int price;
    private int stockQuantity;

    //Category와 연관관계 매핑
    @ManyToMany(mappedBy = "items")
    private List<Category> categories= new ArrayList<>();

}

Item- Album객체

package jpabook.jpashop.domain.item;

import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import lombok.Getter;
import lombok.Setter;

@Entity @Getter @Setter
@DiscriminatorValue("A") //Dtype에 나올 이름
public class Album extends Item {
    private String artist;
    private String etc;
}

Item- Book객체

package jpabook.jpashop.domain.item;

import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import lombok.Getter;
import lombok.Setter;

@Entity @Getter @Setter
@DiscriminatorValue("B") //Dtype에 나올 이름
public class Book extends Item {
    private String author;
    private String isbn;
}

Item- Movie객체

package jpabook.jpashop.domain.item;

import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import lombok.Getter;
import lombok.Setter;

@Entity @Getter @Setter
@DiscriminatorValue("M") //Dtype에 나올 이름
public class Movie extends Item {
    private String director;
    private String actor;

}

Delivery객체

package jpabook.jpashop.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

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

    //Order와의 일대일 관계 매핑
    @OneToOne(mappedBy ="delivery",fetch = FetchType.LAZY )
    @JoinColumn
    private Order order;


    @Embedded
    private Address address;

    @Enumerated(EnumType.STRING)//ENUM은 무조건 String 타입으로 설정해야 한다
    private DeliveryStatus status; //배달 상태(READY,COMP:준비,완료)
}

DeliveryStatus객체

package jpabook.jpashop.domain;

public enum DeliveryStatus {
    READY,COMP
}

Category객체

package jpabook.jpashop.domain;

import jakarta.persistence.*;
import jpabook.jpashop.domain.item.Item;
import lombok.Getter;
import lombok.Setter;

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;
    //Item과 연관관계 매핑
    @ManyToMany
    //다대다는 중간테이블을 만들어야함.(컬럼을 새로 짜줘야함)
    @JoinTable( name="category_item",
            joinColumns = @JoinColumn(name="category_id"), //자기자신
            inverseJoinColumns = @JoinColumn(name="item_id") //item쪽, 반대쪽
    )
    private List<Item> items=new ArrayList<>();

    //카테고리 구조-> 위에서 아래로-> 부모 자식 연관관계로 매핑

    //부모(One으로)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="parent_id")
    private Category parent; //★★★Category 자기 자식을 parent이름으로 객체 생성
    //자식(Many로)
    @OneToMany(mappedBy = "parent")
    private List<Category> child=new ArrayList<>();

    //★★★카테고리 연관관계 편의 매소드★★★
    public void addChildCategory(Category child)
    {
        child.setParent(this); //1. 자식에 부모(Category parent)를 연관관계
        this.child.add(child); //2. 부모를 자식에게 관계
    }

}

Address 객체

package jpabook.jpashop.domain;

import jakarta.persistence.Embeddable;
import lombok.Getter;

@Embeddable //내장형
@Getter
public class Address {
    private String city;
    private String street;
    private String zipcode;

    //기본 클래스(jpa의 reflection,proxy생성을 위해서 남겨둠)
    //protected로 설정하여 함부러 new 하지 못하게 하기 위해서
    protected Address() {

    }
    //★★★★내장형 값 타입은 "생성자로" 값을 받아 "set" 하는 게 좋다.★★★
    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }
}

엔티티 클래스 개발 후, H2 DB**에 잘 매핑 됐는지 확인.**

main함수 실행 후 다음과 위의 빨간색 클릭하여 db새로고침을 한다.

예제의 단순함을 Item은 싱글 테이블 전략으로 만들었다.

3. 엔티티 설계시 주의점

1. 모든 연관관계는 지연로딩으로-> XToOne 관계에 fetch를 LAZY로

CTRL+N-> Text 에 “ToOne” 입력해서 찾기.

 

 

 

 

2. 테이블의 컬럼명 자동 변경(db관례-> 언더스코어,소문자)

3. 논리명 vs 물리명

 

물리명은 논리명에 직접 이름을 적어서 표현가능.

이름 명시 안하면 그냥 자동으로 논리(암시)명 사용

물리명 사용 예

물리명 사용 예

728x90