SpringBoot와 JPA, Thymeleaf를 사용하여 간단한 쇼핑몰을 구현하려 합니다. 프로젝트 생성이나 Gradle 빌드는 깃허브 BincoShop을 참고해주세요! 포스팅은 도메인&테이블 설계 -> 엔티티 개발 -> 회원 서비스 -> 상품 서비스 -> 주문 서비스 순으로 진행됩니다. 포스팅의 잘못된 부분은 언제든 댓글로 남겨주시면 수정하겠습니다😀
개요
지난 시간에 포스팅한 도메인과 테이블 설계에 이어서 엔티티 클래스들 먼저 개발을 진행합니다. 폴더 구조는 하단 사진과 같고, 만일 포스팅을 따라 하는 분이 계시다면 엔티티끼리 연관되는 부분이 많기 때문에 하단 끝까지 따라 하셔야 오류가 안 납니다😀
Member Entity🧍
@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<>();
}
가장 먼저 회원 엔티티를 설계합니다. 회원 엔티티는 위의 테이블 설계와 동일하게 id, name, address, orders 필드가 들어갑니다. 여기서 address(주소)는 내장 타입을 사용할 예정이고, orders는 추후에 만들 Order Entity와 매핑될 예정입니다.
내장 타입을 사용하기 위해서 @Embedded 어노테이션을 사용하였고, orders와의 매핑은 @OneToMany를 사용했습니다. 양방향 관계일 때는 연관관계의 주인을 정해주어야 하기 때문에 多쪽에 속한 order 테이블을 연관관계의 주인으로 설정하였습니다.
Address 내장 타입
@Embeddable
@Getter
public class Address {
private String city;
private String street;
private String zipcode;
}
@Embeddable 어노테이션을 사용해서 주소 내장 타입을 생성하였습니다. 사실 Addres 클래스에 내장 타입인 것을 선언하였기 때문에, 위에서 만든 Member Entity에서는 어노테이션을 제거해도 되지만, 가독성을 위해 양쪽 다 추가하였습니다.
Address 내장 타입도 미리 설계한 테이블 필드에 맞게 각 컬럼을 추가하였습니다.
Order Entity📝
@Entity
@Getter
@Setter
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
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")
private Delivery delivery;
private LocalDateTime orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus status;
}
가장 먼저 특이한 것은 Order Entity에는 Table 명을 기입하였습니다. 저번 포스팅에서도 언급했지만, order는 예약어로 사용되기 때문에(정확히는 order by) 관례상 orders로 테이블명을 많이 사용합니다. 클래스는 Order로 설정하였기 때문에 따로 Table 명을 지정해줍니다.
두 번째로 눈여겨볼 곳은 Member Entity와의 매핑 관계입니다. Member Entity에서 @OneToMany로 설정하였기 때문에, Order Entity에서는 반대로 @ManyToOne으로 설정합니다. 하지만 여기서 중요한 부분은 FetchType을 수동으로 설정한 것입니다. 혹여나 Eager, Lazy 타입을 모르시면 이곳을 클릭하셔서 선수 학습해주세요!
그리고 Member 와는 연관관계상 Order가 주인이기 때문에 @JoinColum 어노테이션을 이용해 설정을 하였습니다.
그다음 필드인 orderItems는 한 주문에 여러 가지 아이템이 들어갈 수 있기 때문에 @OneToMany를 이용했습니다. 여기서도 연관관계상 Order는 One 쪽에 속하기 때문에 mappedBy를 사용해 의존하는 상태로 설정했습니다.
한 가지 눈여겨볼 점은 cascade를 사용한 부분입니다. 모든 entity는 저장하기 위해서 각자 persist 해야 하지만, cascade를 활용하면 한 번에 persist 할 수 있습니다. 이 점은 저장, 삭제 모두 동일하게 적용합니다.
그다음은 delivery 컬럼입니다. 주문과 배달은 1:1 매칭 관계이므로 @OneToOne 어노테이션을 사용하였습니다. 이 부분도 위와 동일하게 Lazy와 cascade를 설정하였습니다.
위에서 설명한 연관관계의 주인은 항상 One 쪽에 설정을 하였는데 이 부분은 @OneToOne 이기 때문에, 누가 주인이 될 것인지는 개발자의 재량인 것 같습니다. 통상 주문에 의해 배달이 시작되기 때문에 저는 Order Entity를 주인으로 설정하였고, 그에 따라 @JoinColumn으로 관계를 매핑했습니다.
orderDate는 주문 날짜를 뜻합니다. Java 8 버전 이후에는 LocalDateTime 기능을 사용할 수 있게 되었는데, import 하여 사용만 한다면 Hibernate가 자동으로 세팅해줍니다.
마지막으로 status 컬럼입니다. OrderStatus Enum 클래스를 가져다 사용하는데 여기서 유의할 점은 @Enumerated 어노테이션 안의 EnumType을 String으로 설정한 것입니다.
EnumType은 ordinal과 String이 있는데, ordinal은 숫자로 표기됩니다. 숫자로 표기되면 추후에 배달 상태가 추가될 경우 인덱스상 밀려날 수 있고, 밀려나게 되면 이미 저장된 DB값들을 모두 수정해야 하는 곤란한 상황이 일어날 수 있기 때문에 String으로 설정하였습니다.
주문 상태 Enum
public enum OrderStatus {
ORDER,CANCEL
}
주문 상태는 Enum 클래스를 사용했습니다. ORDER는 주문이 들어간 상태이고, CANCEL은 환불인 상태입니다. 혹여나 Enum에 대해서 잘 모르신다면 이곳을 클릭하셔서 확인해주세요😄
OrderItem Entity🛒
@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
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)
@JoinColumn(name = "order_id")
private Order order;
private int orderPrice;
private int count;
//생성 메서드//
public static OrderItem createOrderItem(Item item, int orderPrice, int count) {
OrderItem orderItem = new OrderItem();
orderItem.setItem(item);
orderItem.setOrderPrice(orderPrice);
orderItem.setCount(count);
item.removeStock(count);
return orderItem;
}
}
OrderItem Entity는 @NoArgsConstructor 어노테이션을 이용해서 PROTECTED로 설정하였습니다. 이 부분은 다른 사람이 다른 스타일로 생성자를 만드는 것을 막기 위함인데, 다음 포스팅에서 자세히 설명할 것 같네요!
지금은 다른 개발자가 new OrderItem()으로 생성자를 생성하지 못하게 하고 오직 createOrderItem 메서드로만 생성자를 만들 수 있게 보호해주는 역할을 한다고 생각하시면 될 것 같습니다.
Item Entity와의 매핑 관계는 테이블 설계와 맞게 @ManyToOne으로 설정하였고, Many에 속하기 때문에 연관관계의 주인이 되는 건 다들 아시겠죠?
그다음 컬럼인 order와도 똑같이 작용하기 때문에 설명은 생략하겠습니다.
그리고 주문 가격과 수량을 나타내 주는 orderPrice, count 컬럼이 들어갑니다.
Delivery Entity🚚
@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)
private DeliveryStatus status;
}
Delivery Entity도 위와 마찬가지로 테이블 설계를 보면서 작성을 하였습니다. Order Entity와는 아까 설정한 것처럼 @OneToOne 매핑을 하였고, 연관관계의 주인을 Order로 설정했었기 때문에 mappedBy를 이용하여 의존하고 있음을 설정해줍니다.
Address 클래스는 내장 타입이기 때문에 @Embedded 어노테이션을 사용하였고, DeliveryStatus는 Enum 타입으로 설정할 예정이기 때문에, @Enumerated를 사용하였습니다.
배달 상태 Enum
public enum DeliveryStatus {
READY, COMP
}
배달 상태는 Enum class로 배달 준비단계인 READY와 진행 단계인 COMP로 설정하였습니다.
Item Entity🥡
@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<>();
}
Item Entity는 싱글 테이블 전략을 사용하였습니다. 싱글 테이블은 여러 가지 아이템을 테이블 하나로 묶어서 사용하고, 구분 컬럼(DTYPE)으로 어떤 자식 데이터인지를 구분합니다. 조회할 때 조인을 사용하지 않기 때문에 일반적으로 가장 빠른 성능을 가지고 있습니다. Entity에서는 아래와 같은 코드로 적용하였고, @DiscriminatorColumn을 이용하여 구분 컬럼을 설정하였습니다.
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")
Category Entity는 다음 포스팅에서 진행하기 때문에, 다음 포스팅에서 설명하도록 하겠습니다.
Album Entity
@Entity
@DiscriminatorValue("A")
@Getter
@Setter
public class Album extends Item{
private String artist;
private String etc;
}
Album Entity는 추상 클래스인 Item을 상속받아서 구현합니다. Item Entity에서 설정한 Dtype(구분 컬럼)을 @DiscriminatorValue 어노테이션을 사용해서 Album의 앞글자 A로 설정하였습니다.
artist와 etc는 테이블 설계에 맞게 구현했습니다.
Book Entity
@Entity
@DiscriminatorValue("B")
@Getter
@Setter
public class Book extends Item {
private String author;
private String isbn;
}
Movie Entity
@Entity
@DiscriminatorValue("M")
@Getter
@Setter
public class Movie extends Item {
private String director;
private String actor;
}
👨👩👦👦 오픈채팅방 운영
취업을 준비하는 예비 개발자분들을 위한 질문&답변할 수 있는 공간을 만들었습니다. 취업과 이직을 하기 위해서 어떤 걸 중점적으로 준비해야 하는지부터 포트폴리오&이력서 작성법 등 다양한 질문들을 받고 답변을 드립니다. 참여하셔서 다양한 정보 얻고 가시면 좋을 것 같네요😁
참여코드 : 456456
https://open.kakao.com/o/gVHZP8dg
👨💻 전자책 출간
아울러 제가 🌟비전공자에서 2년만에 보안 전문 중견기업으로 이직 한 방법들을 정리한 전자책을 출간 하게 되었습니다. 어떤 걸 공부해야 하는지, 이직을 위해서 무엇을 준비해야 하는지, 제가 받았던 기술 면접 리스트 등 다양한 목차로 구성되어 있습니다. 또한, 구매 시 1:1 채팅을 이용하여 포트폴리오 첨삭을 도와드리고 있습니다. 🐕전자책으로 얻은 모든 수익은 유기견 센터 '팅*벨 입양센터'에 후원될 예정입니다. 관심 있으신 분들은 아래 링크를 참고해주세요😁
마치며
지금까지 엔티티 개발을 했습니다. 아직 카테고리 부분은 남아있는데 이 부분은 다음 포스팅에서 진행할게요. 포스팅의 모든 내용은 김영한 선배님의 강의를 참고했습니다. 부족한 글솜씨여서 이해가 안 되는 부분들은 하단 링크를 통해 강의를 들으시는걸 추천드립니다 :)
관련 포스팅
Reference
'JPA' 카테고리의 다른 글
SpringBoot JPA 쇼핑몰 회원 서비스 개발 (1) | 2022.09.14 |
---|---|
SpringBoot JPA 쇼핑몰 엔티티 개발Ⅱ (0) | 2022.09.14 |
SpringBoot JPA 쇼핑몰 도메인&테이블 설계 (0) | 2022.09.07 |
JPA 영속성 관리 Dirty Checking & Merge (0) | 2022.09.06 |
JPA Eager Loading VS Lazy Loading (0) | 2022.09.05 |