728x90
반응형
쇼핑몰을 주제로하여 간단한 웹 애플리케이션을 개발해보려 한다.
요구사항 분석
기능목록
- 회원 기능 : 회원 등록, 조회
- 상품 기능 : 상품 등록, 수정, 조회
- 주문 기능 : 상품 주문, 주문내역 조회, 주문 취소
- 기타 요구사항
- 상품은 재고 관리가 필요하다.
- 상품의 종류는 도서, 음반, 영화가 있다.
- 상품을 카테고리로 구분할 수 있다.
- 상품 주문시 배송정보를 입력할 수 있다.
도메인 모델과 테이블 설계
관계도
- 회원, 주문, 상품의 관계는 회원은 여러 주문을 할 수 있고, 그 주문에는 여러 상품이 담겨있다. 이때 주문과 상품은 다대다 관계이기 때문에 중간 엔티티 주문상품을 추가하여 일대다, 다대일 관계로 풀어낸다.
- 상품은 도서, 음반, 영화로 구성되며 상속 관계를 가진다.
엔티티 및 테이블 설계
- 회원이 주문을 하기 때문에 회원이 주문리스트를 가지는 것이 잘 설계한 것 같지만, 실무에서는 회원이 주문을 참조하는 것이 아닌 주문이 회원을 참조하기 때문에 주문 테이블에 회원을 가진다.
- 주소 값타입 엔티티는 회원과 배송 테이블에 각각의 컬럼으로 생성된다.
- 상품 테이블은 싱글 테이블 전략을 사용하여 상속관계에 있는 필드 전부 컬럼으로 생성하여 관리한다.
- 상품 테이블 내
DTYPE
컬럼을 통해 도서, 음반, 영화의 구분값을 저장한다. - 주문 테이블의 이름이
ORDER
가 아니라ORDERS
인 이유는 데이터베이스의 예약어ORDER BY
가 있기 때문에 관례상ORDERS
로 많이 사용한다.
연관관계 매핑 분석
- 양방향 관계에서는 연관관계의 주인이 매우 중요한데 이때 외래키가 있는 쪽을 연관관계의 주인으로 정하는 것이 설계도 깔끔하고 성능도 좋고 관리, 조작하기 쉽다.
- 연관관계의 주인은 단순히 외래키를 누가 관리하나의 문제이지 비즈니스상 우위에 있다고 주인으로 정하면 안된다.
엔티티 클래스 개발
주의사항
- 엔티티, 테이블 설계에 맞춰서 개발
- 연관관계를 주의하여 구성하고 연관관계 주인 선정이 중요하다.
- 실무에서는 가급적
Getter
만 열어두고,Setter
는 꼭 필요할 경우에만 사용해야 한다. 왜냐하면Getter
의 경우 아무리 호출해도 어떠한 일이 발생하지 않지만,Setter
의 경우 미래에 엔티티가 어떻게 변경되었는지 추적하기 힘들어지기 때문에 엔티티 변경시엔Setter
대신에 별도의 메소드를 구성하여 변경해야 한다. - 해당 예제에서는 쉽게 구성하기 위해
Getter
,Setter
모두 열어두고 사용한다.
엔티티 구현
회원 (Member)
package jpabook.jpashop.domain;
@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;
@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
private Delivery delivery;
private LocalDateTime orderDate;
@Enumerated(EnumType.STRING) // ENUM 클래스 값 형식 설정
private OrderStatus status; // ORDER, CANCEL
}
주문 상태 (OrderStatus)
package jpabook.jpashop.domain;
public enum OrderStatus {
ORDER, CANCEL
}
주문 상품 (OrderItem)
package jpabook.jpashop.domain;
@Entity
@Table(name = "order_item")
@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; // 주문 수량
}
상품 (Item)
package jpabook.jpashop.domain.item;
@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<>();
}
음반 (Album)
package jpabook.jpashop.domain.item;
@Entity
@DiscriminatorValue(value = "A") // 구분 컬럼 값 지정
@Getter @Setter
public class Album extends Item {
private String artist;
private String etc;
}
도서 (Book)
package jpabook.jpashop.domain.item;
@Entity
@DiscriminatorValue(value = "B") // 구분 컬럼 값 지정
@Getter @Setter
public class Book extends Item {
private String author;
private String isbn;
}
영화 (Movie)
package jpabook.jpashop.domain.item;
@Entity
@DiscriminatorValue(value = "M") // 구분 컬럼 값 지정
@Getter @Setter
public class Movie extends Item {
private String director;
private String actor;
}
배송 (Delivery)
package jpabook.jpashop.domain;
@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
}
배송 상태 (DeliveryStatus)
package jpabook.jpashop.domain;
public enum DeliveryStatus {
READY, COMP
}
카테고리 (Category)
package jpabook.jpashop.domain;
@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<>();
// 현재 엔티티 기준 부모니까 나의 부모는 1개
@ManyToOne
@JoinColumn(name = "parent_id")
private Category parent;
// 현재 엔티티 기준 자식이니까 나의 자식은 여러개
@OneToMany(mappedBy = "parent")
private List<Category> child = new ArrayList<>();
}
- 실무에서는
@ManyToMany
를 사용하지 않는것을 추천한다. 다대다 관계가 편리한 것 같지만 중간 테이블에 추가적인 컬럼을 구성할 수 없고 세밀하게 쿼리를 실행할 수 없기 때문에 한계가 있다. 중간 엔티티를 구성하여 일대다, 다대일 매핑으로 구성하여 사용하는 것을 추천한다.
주소 (Address)
package jpabook.jpashop.domain;
@Embeddable
@Getter
public class Address {
private String city;
private String street;
private String zipcode;
// setter를 제공 안하고 생성자를 통해 값 설정하는 것을 권장
protected Address() {}
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
}
- 값 타입은 변경 불가능하게 설계해야 한다.
Getter
만 열고 생성자에서 값을 모두 초기화해서 변경 불가능한 클래스로 구현한다.- JPA 스펙상 엔티티나 임베디드 타입은 자바 기본 생성자를
public
또는protected
로 설정해야 하는데public
보다는protected
가 안전하기 때문에protected
로 설정한다. - JPA가 기본생성자 권한 제약을 두는 이유는 JPA 구현 라이브러리가 객체를 생성할 때 리플랙션 같은 기술을 사용할 수 있도록 지원해야 하기 때문이다.
구현 결과
엔티티 설계 시 주의사항
- 엔티티에는 가급적 Setter를 사용하지 말아야 한다.
- Setter가 열려있다면 변경 포인트가 너무 많아 유지보수가 어렵다. - 모든 연관관계는 지연로딩으로 설정해야 한다.
- 즉시로딩은 예측이 어렵고 어떤 SQL이 실행될 지 추적하기 어렵다.
- 특히 JPQL을 실행할 때 N+1문제가 자주 발생한다.
- 연관된 엔티티를 함께 조회해야 한다면 FETCH JOIN 또는 엔티티 그래프 기능을 사용한다.
-@OneToOne
,@ManyToOne
관계는 기본이 즉시로딩이므로 직접 지연로딩으로 설정해야 한다. (@OneToMany
,@ManyToMany
는 기본이 지연로딩) - 컬렉션은 필드에서 초기화 하여 사용해야 한다.
- 컬렉션을 필드에서 바로 초기화 하여 사용할 경우 NULL관련 문제에서 안전하다.
- 하이버네이트는 엔티티를 영속할 때 컬렉션을 감싸서 하이버네이트가 제공하는 내장 컬렉션으로 변경하게 되는데 만약 임의의 메소드에서 컬렉션을 잘못 생성하거나 수정하면 하이버네이트 내부 매커니즘에 문제가 발생할 수 있다. 따라서 컬렉션을 필드에서 바로 초기화 하여 사용해야 하며, 절대로 컬렉션을 교체하지 말고 생성 시점의 객체 그대로 사용해야 한다. - 테이블 컬럼명 생성 전략
- 엔티티의 필드명을 그대로 테이블의 컬럼명으로 사용한다.
- 스프링 부트에서는 카멜 표기법을 팟홀 표기법으로 구성하고.(점)
을_(언더바)
, 대문자를 소문자로 변경하여 자동으로 구성한다. - Cascade
- 특정 엔티티를 영속성 상태로 만들때 연관된 엔티티도 함께 영속성 상태로 변경하는것으로 ALL 설정 시 영속상태 변경이나 삭제가 함께 진행된다. - 연관관계 편의 메소드
- 연관관계를 편하게 연결하기 위한 메소드로 핵심적으로 조작하는 곳에 구현하는 것이 좋다.
- 양방향 관계일 경우 사용하면 특히 좋다.
728x90
반응형
'Dev > JPA' 카테고리의 다른 글
[JPA] JPA 활용 I - 회원 도메인 개발 (0) | 2021.09.16 |
---|---|
[JPA] JPA 활용 I - 애플리케이션 구현 준비 (0) | 2021.09.16 |
[JPA] JPA 활용 I - 프로젝트 환경설정 (0) | 2021.09.14 |
[JPA] JPA 활용 I - 강좌 소개 (0) | 2021.09.14 |
[JPA] 객체지향 쿼리 문법 - 중급문법(2) (0) | 2021.09.11 |
댓글