[SpringBoot-JPA 활용편1] 자바 ORM 표준 JPA 프로그래밍 - 활용편1: 2. 도메인 분석 설계
실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 강의 - 인프런
실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., 스프
www.inflearn.com
해당 강의는 Inflearn에 등록된 김영한님의 Spring Boot 강의입니다.
이번시간에는 도메인 분석 및 설계를 할것이다. 우선 이번 강의에서 최종적으로 구현하고자 하는 기능을 살펴본 후에 필요한 객체들을 생성하고, 기본편에서 배운 연관관계를 매핑하자.
0. 요구사항 분석 및 기능 목록
개발자는 사용자의 요구사항을 먼저 분석하고, 필요한 기능들을 파악하여 기능 명세서를 작성해야 한다.
이번 강의에서는 위와 같은 기능들을 하는 웹 페이지를 만들어 볼 것이다.
(+a 기능 명세서는 요즘 노션 으로 작성하는 편입니다.
아래 사진은 이해를 돕기 위해 저희 팀이 프로젝트에서 만든 기능 명세서를 예를 들겠습니다.)
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는 사용하지 말자. 여기서는 다대 다 관계를 예제로 보여주기 위해 추가했을 뿐이다)
2. 엔티티 클래스 개발
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 물리명
물리명은 논리명에 직접 이름을 적어서 표현가능.
이름 명시 안하면 그냥 자동으로 논리(암시)명 사용
물리명 사용 예