Peony의 기록 창고 🌼
article thumbnail
반응형

실제 동작하는 화면은 다음과 같다.

기능 목록

  • 회원 - 등록, 조회
  • 상품 - 등록, 수정, 조회
  • 주문 - 주문, 주문 내역 조회, 주문 취소
  • 기타 요구사항
    • 상품은 재고 관리가 필요하다.
    • 상품의 종류는 도서, 음반, 영화가 있다.
    • 상품을 카테고리로 구분할 수 있다.
    • 상품 주문시 배송 정보를 입력할 수 있다.

 

도메인 분석 설계

도메인 모델과 테이블 설계

도메인 모델

  • 회원, 주문, 상품의 관계
    : 회원은 여러 상품을 주문할 수 있다. 그리고 한 번 주문할 때 여러 상품을 선택할 수 있으므로 주문과 상품은 다대다 관계다. 따라서 그림처럼 주문상품이라는 엔티티를 추가해서 다대다 관계를 일대 다, 다대일 관계로 풀어냈다. 다만, 카테고리와 상품은 경험을 위해 다대다로 두었다.
  • 상품 분류 : 도서, 음반, 영화는 상품이라는 공통 속성을 사용하므로 상속 구조로 표현한다.

 

테이블 설계

 

테이블 분석

  • MEMBER : 회원 엔티티의 Address 임베디드 타입 정보가 회원 테이블과 배송 테이블에 들어갔다.
  • ITEM : 앨범, 도서, 영화 타입을 통합해서 하나의 테이블로 DTYPE컬럼을 통해 구분하도록 만들었다. (싱글 테이블 전략)

 

주문 테이블이 ORDERS 인 이유는 order by가 예약어로 잡고 있는 경우가 많기 때문에 관례상 변경한 것이다.

데이터베이스 네이밍 방식은 회사마다 다르지만 보통은 대문자 + _소문자 + _ 로 지정해서 일관성 있게 사용한다.

실제 코드에서는 소문자 + _ 방식을 사용해보자.

 

연관관계 분석

  • 회원과 주문 : 일대다, 다대일의 양방향 관계
    • 외래 키가 있는 주문을 연관관계의 주인으로 설정
  • 주문상품과 주문 : 다대일 양방향 관계 - 주문상품이 연관관계의 주인
  • 주문상품과 상품 : 다대일 단방향 관계
  • 주문과 배송 : 일대일 양방향 관계

외래 키가 있는 곳을 연관관계의 주인으로 정하자.

연관관계의 주인은 단순히 외래 키를 누가 관리하냐의 문제이지 비즈니스 상 우위에 있다고 주인으로 정하면 안된다. 그렇지 않으면 관리와 유지보수가 어렵고, 업데이트 쿼리가 발생하는 성능 문제도 있을 수 있다.

 

엔티티 클래스 개발

  • 예제에서는 설명을 쉽게하기 위해 엔티티 클래스에 Getter, Setter를 모두 열고, 최대한 단순하게 설계했다.
  • 실무에서는 가급적 Getter는 열어두고, Setter는 꼭 필요한 경우에만 사용하는 것을 추천한다.
    : Setter를 막 열어두면 엔티티에 변경 되는 원인을 추적하기 힘들다. 그래서 Setter 대신에 변경 지점이 명확하도록 변경을 위한 비즈니스 메서드를 별도로 제공해야 하는 것이 좋다.

 

회원 엔티티

@Getter @Setter
@Entity
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<>();
}

주문 엔티티

@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)
    private Delivery delivery;

    private LocalDateTime orderDate;

    @Enumerated(EnumType.STRING)
    private OrderStatus status; //주문상태 [ORDER, CANCEL]

    //연관관계 편의 메서드
    public void setMember(Member member) {
        //기존 관계 제거
        if (this.member != null) {
            this.member.getOrders().remove(this);
        }

        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);
    }
}

주문 상태

public enum OrderStatus {
    ORDER, CANCEL
}

주문 상품 엔티티

@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; //주문 수량
}

상품 엔티티

@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<>();
}

도서, 음반, 영화 엔티티

@Entity
@Getter @Setter
@DiscriminatorValue("B")
public class Book extends Item {
    private String author;
    private String isbn;
}

@Entity
@Getter @Setter
@DiscriminatorValue("A")
public class Album extends Item {
    private String artist;
}

@Entity
@Getter @Setter
@DiscriminatorValue("M")
public class Movie extends Item {
    private String director;
    private String actor;
}

배송 엔티티

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

    @OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY)
    private Order order;

    @Embedded
    private Address address;

    @Enumerated(EnumType.STRING)
    private DeliveryStatus status; //ENUM [READY, COMP]
}

배송 상태

public enum DeliveryStatus {
    READY, COMP
}

카테고리 엔티티

@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);
    }
}

주소 값 타입

@Embeddable
@Getter
public class Address {
    private String city;
    private String street;
    private String zipcode;

    protected Address(){
    }
    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }
}

값 타입은 변경 불가능하게 설계해야 한다. JPA 스펙상 엔티티나 임베디드 타입은 자바 기본 생성자를 public 또는 protected로 설정해야한다. public으로 두는 것 보다는 Protected로 설정하는 것이 그나마 더 안전하다. JPA가 이런 제약을 두는 이유는 JPA 구현 라이브러리가 객체를 생성할 때 리플랙션 같은 기술을 사용할 수 있도록 지원해야하기 때문이다.

 

엔티티 설계시 주의점

  • Setter 사용하지 말자.
  • 모든 연관관계는 지연로딩으로!
    • 즉시 로딩은 어떤 SQL이 실행될 지 추적하기 어렵다. 특히 JPQL을 사용할 때 N + 1 문제가 자주 발생한다ㅏ.
    • 연관된 엔티티를 함께 DB에서 조회해야 하면, fetch join 또는 엔티티 그래프 기능을 사용
    • @XToOne 관계는 기본이 즉시 로딩이므로 지연 로딩으로 바꿔주자.
  • 컬렉션은 필드에서 초기화 하자.
    • null 문제에서도 안전하고, 코드도 간결해진다.

 

이 글은 김영한 님의 "실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발"을 듣고 정리한 내용입니다.

반응형
profile

Peony의 기록 창고 🌼

@myeongju