IT recording...
[스프링 JPA1] 1. 요구사항 분석 및 도메인 셜계 본문
[원문링크]
https://adorable-aspen-d23.notion.site/JPA1-1-2994f80a6bec41d2a82c48c787786bac
김영한님의 [실전!스프링부트와JPA활용1 - 웹 어플리케이션개발] 강의를 듣고 작성한 글입니다.
1. 요구사항 분석
- 회원 기능
- 회원 등록
- 회원 조회
- 주문 기능
- 상품 주문
- 주문 내역 조회
- 주문 취소
- 상품 기능
- 상품 등록
- 상품 수정
- 상품 조회
- 기타 요구사항
- 상품은 재고 관리가 필요하다.
- 상품의 종류는 도서, 음반, 영화가 있다.
- 상품을 카테고리로 구분할 수 있다.
- 상품 주문시 배송 정보를 입력할 수 있다.
2. 설계
2-1. 도메인 모델 설계
- 요구사항 분석을 바탕으로 어떤 도메인이 필요한지 살핀다.
- 각 도메인 간 연간관계를 파악한다.
- 회원-주문(1:N)
- 한 회원은 여러개의 주문을 가질 수 있다.
- 주문은 하나의 회원에 매핑된다.
- 주문-배송(1:1)
- 한 주문은 하나의 배송 정보를 가진다.
- 주문-상품(M;N)
- 하나의 주문에서 여러 상품을 선택할 수 있다.
- 한 상품은 여러개의 주문에 매핑될 수 있다.
- ⇒ 주문상품이라는 엔티티를 추가하여 1:N, N:1관계로 풀어낸다.
2-2. 엔티티 설계 (객체)
- 도메인 모델과 유사한 모양을 갖는다.
- Member
- 임베디드 타입인 Address를 갖는다.
- List<Order> 를 갖는다. (양방향 관계이기 때문에 표시하였지만 실무에서는 양방향 관계를 많이 사용하지 않는다. 회원이 주문을 갖는 것이 아니라, 주문이 회원을 갖는 것으로 생각하자)
- Order
- Enum을 활용하여 status를 표현한다.
2-3. 테이블 설계 (RDBMS)
: 설계된 객체를 토대로 RDBMS는 어떻게 설계되는지 살펴보자
- 1:N 관계에서 항상 다 쪽에 외래키가 존재한다.
- 외래 키가 있는 곳을 연관관계의 주인으로 정하자.
연관관계의 주인은 단순히 외래 키를 누가 관리하냐의 문제이지, 비즈니스상 우위에 있는 것이 아니다.
자동차와 바퀴가 존재할 때 (1:N) 외래키는 바퀴쪽에 존재하고, 이에 따라 바퀴를 연관관계의 주인으로 설정할 수 있다.
자동차를 연관관계의 주인으로 정하게 되면, 자동차가 관리하지 않는 바퀴 테이블의 외래 키 값이 업데이트 되므로
관리와 유지보수가 어렵고, 추가적으로 별도의 업데이트 쿼리가 발생하는 성능 문제도 발생한다.
3. 엔티티 클래스 개발
- 주의
- Getter만 열고, Setter는 가급적 열지 말자
- Setter를 막 열어두면 가까운 미래에 엔티티가 도대체 어디서 왜 변경되는지 한참 찾고 있어야 한다. 따라서 변경 지점이 명확하도록 , 변경을 위한 비즈니스 메서드를 별도로 제공해야 한다.
- Getter만 열고, Setter는 가급적 열지 말자
3-1. Member
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
@Embedded //내장 타입 사용
private Address address;
@OneToMany(mappedBy = "member") //회원과 주문은 1대다관계 //나는 연관관계 거울이에요~! //Order테이블에 있는 member 필드에 의해 매핑된거야.
//읽기전용이 됨. 여기다가ㅏ 세팅해도 세팅안됨, ORDER의 member에 세팅해야 함
// mappedBy : 양방향 관계에서 관계의 주인을 설정해 주는 것, order테이블에 있는 member필드에 의해 매핑된거야 = 주인이 아니야
private List<Order> orders = new ArrayList<>();
public Long getId() {
return id;
}
}
기본 id
- @Id
- @GeneratedValue → Sequential하게 증가한다.
- @Column(name = “xxx_id”) → rdbms에서 사용할 컬럼 이름을 등록한다.
- 엔티티의 식별자로는 id를 사용해도 되지만, (→ member.id처럼 사용 가능)
- 테이블은 타입이 없으므로 구분이 어렵다. 따라서 member_id 와 같이 명시적으로 적어준다.
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
내장타입 (Value)
**@Embedded** //내장 타입 사용
private Address address;
**@Embeddable** //jpa의 내장타입, 어디엔가 내장이 될 수 있다.
@Getter
public class Address {
private String city;
private String street;
private String zipcode;
protected Address(){ //JPA스펙 상 protected로 만들자
}
public Address(String city, String street, String zipcode) { //값타입 생성자에서만 변경 가능하게 하기
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
}
연관관계 매핑 - 주인 지정하기
- 1:N의 N쪽이 보통 연관관계의 주인이다.
- 주인 → @JoinColumn(name = “slave_id”)
- slave → @OneToMany(mappedBy = “owner”)
- 읽기 전용이 된다. (나는 연관관계 거울이에요~!)
<--Member-->
@OneToMany(mappedBy = "member") //읽기전용 , Order의 member 필드에 연관된다.
private List<Order> orders = new ArrayList<>();
<--Order-->
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id") //member 테이블의 member_id와 조인한다.
private Member member; //주문 회원
3-2. Order
@Entity
@Table(name="orders") //테이블 이름 orders로 설정 (예약어때문)
@Getter @Setter
public class Order {
@Id @GeneratedValue
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY) //order와 member는 다대1 관계
@JoinColumn(name="member_id") //forign key 지정
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에서 order를 주인으로 봄
private Delivery delivery;
private LocalDateTime orderDate; //주문시간
@Enumerated(EnumType.STRING)
private OrderStatus status; //주문상태 [ORDER, CANCEL]
//==연관관계 메서드==//
// 양방향 연관관계를 가질 때 양쪽 객체에 모두 데이터를 넣는 것이 좋은데, 어느 하나를 잊을 수도 있으므로 묶어놓는 것임 (원자적으로)
//주도권을 가지고 있는 쪽이 가지고 있는게 좋음
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);
}
}
@xxxToOne 에서의 지연 로딩 설정
- OneToOne, ManyToOne
- 기본이 즉시로딩(EAGER)이므로 지연 로딩으로 설정해야 한다.
- 즉시로딩은 예측이 어렵고, 어떤 SQL이 실행될지 추적하기 어렵다.
- JPQL실행 시 N+1 문제가 발생한다.
- 실무에서는 모든 xxxToOne 관계를 LAZY 지연 로딩으로 설정하자
**@ManyToOne(fetch = FetchType.LAZY)** //order와 member는 다대1 관계
@JoinColumn(name="member_id") //forign key 지정
private Member member;
연관관계 메서드
- 양방향 연관관계를 가질때, 객체에서는 양쪽 객체에 모두 데이터를 넣는다. (db에서는 면 되지만)
- 둘 다 업데이트를 해야 하는데 어느 하나를 잊을 수도 있으므로 묶어서 관리하는 연관관계 메서드를 만든다.
- 연관관계의 주도권을 가지고 있는 쪽이 보유한다.
//==연관관계 메서드==//
public void setMember(Member member){
this.member = member; //order꺼
member.getOrders().add(this); //member꺼
}
public void addOrderItem(OrderItem orderItem){
orderItems.add(orderItem); //order꺼
orderItem.setOrder(this); //orderItem꺼
}
public void setDelivery(Delivery delivery){
this.delivery = delivery; //order꺼
delivery.setOrder(this); //delivery꺼
}
Enum 클래스
- 항상 string으로 지정해주어야 한다.
@Enumerated(EnumType.STRING)
private OrderStatus status; //주문상태 [ORDER, CANCEL]
public enum OrderStatus {
ORDER, CANCEL
}
3-3. OrderItem
@Entity
@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) //orderItem이 다 관계
@JoinColumn(name = "order_id")
private Order order;
private int oderPrice; //주문 당시 가격
private int count; //주문 당시 수량
}
3-4. Item
@Entity
**@Inheritance(strategy = InheritanceType.SINGLE_TABLE)** //상속할때 //Joined,tableper,singleTable이 있음
**@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<>();
}
@Entity
@Getter
@Setter
**@DiscriminatorValue("B")**
public class Book extends Item{
private String author;
private String isbn;
}
3-5. Delivery
@Entity
@Getter
@Setter
public class Delivery {
@Id
@GeneratedValue
@Column(name = "delivery_id")
private Long id;
@OneToOne(fetch = FetchType.LAZY, mappedBy = "delivery")
private Order order;
@Embedded
private Address address;
@Enumerated(EnumType.STRING) //enum쓸 때 string을 써야 순서가 바뀌더라도 꼬이지 않음
private DeliveryStatus status; //READY, COMP
}
3-6. 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> child = new ArrayList<>(); //자식
//==연관관계 메서드==//
public void addChildCategory(Category child){
this.child.add(child);
child.setParent(this);
}
}
- 실무에서는 ManyToMany를 사용하지 말자
- 중간 테이블에 컬럼을 추가할 수 없고, 세밀하게 쿼리 실행이 어렵다.
- 대신 중간 엔티티(CategoryItem)을 만들고, @ManyToOne, @OneToMany 로 매핑해서 사용하자.
- ex) 주문 - 주문상품 - 상품
3-7. 기타 주의사항
- 컬렉션은 필드에서 초기화 하자.
- Null에서 안전할 수 있다.
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = **new ArrayList<>();**
'Spring' 카테고리의 다른 글
[스프링 JPA1] 2. 도메인 개발 (0) | 2022.02.15 |
---|---|
[Spring] 개발 중 마주한 오류들 - 1 (0) | 2022.02.15 |
[스프링 MVC1] 7. 웹 페이지 만들기 (0) | 2022.02.07 |
[스프링 MVC1] 6. MVC 기본 기능 (0) | 2022.01.15 |
[스프링 MVC1] 5. MVC 패턴 (0) | 2022.01.14 |
Comments