도메인 분석 설계 - 엔티티 클래스 개발

2023. 5. 4. 23:24·JPA/1편- 실전! 스프링 부트와 JPA 활용

엔티티 클래스 개발


 

위 회원 엔티티 관계도를 보고 VO 생성 코드를 만들어 본다.

 

회원(Member) 엔티티

package jpabook.jpashop.domain;
import javax.persistence.*; 
import java.util.ArrayList; 
import java.util.List;

@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 를 사용하고 PK 컬럼명은 member_id 를 사용했다.

엔티티는 타입(여기서는 Member )이 있으므로 id 필드만으로 쉽게 구분할 수 있다. 테이블은 타입이 없으므로 구분이 어렵다. 그리고 테이블은 관례상 테이블명 + id 를 많이 사용한다. 참고로 객체에서 id 대신에 memberId 를 사용해도 된다. 중요한 것은 일관성이다.

 

주문(Order) 엔티티

package jpabook.jpashop.domain;


import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "ORDERS")
@Getter @Setter
public class Order {
    @Id
    @GeneratedValue
    @Column(name = "order_id")
    private Long id;

    private LocalDateTime orderDate;

    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="member_id")
    private Member member;

    @OneToMany(mappedBy = "order",cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();

    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus;

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

 

주문상품(OrderItem)엔티티

package jpabook.jpashop.domain;

import jpabook.jpashop.domain.item.Item;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

@Entity
@Table(name = "order_item")
@Getter @Setter
public class OrderItem {
    @Id
    @GeneratedValue
    @Column(name = "order_item_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "item_id")
    private Item item;

    private int orderPrice;
    private int count;
}

 

주문상태(OrderStatus)

package jpabook.jpashop.domain;

public enum OrderStatus {
    ORDER, CANCEL
}

 

배송(Delivery)엔티티

package jpabook.jpashop.domain;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

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

 

배송상태(DeliveryStatus)

package jpabook.jpashop.domain;

public enum DeliveryStatus {
    READY, COMP
}

 

주소(Address) 값 타입(임베디드 타입)

package jpabook.jpashop.domain;

import lombok.AllArgsConstructor;

import javax.persistence.Embeddable;

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

    protected Address() {
    }
}
※ 참고
 값 타입은 변경 불가능하게 설계해야 한다. > @Setter 를 제거하고, 생성자에서 값을 모두 초기화해서 변경 불가능한 클래스를 만들자. JPA 스펙상 엔티티나 임베디드 타입( @Embeddable )은 자바 기본 생성자(default constructor)를 public 또는 protected 로 설정해야 한다. public 으로 두는 것 보다는 protected 로 설정하는 것이 그나마 더 안전하다. > JPA가 이런 제약을 두는 이유는 JPA 구현 라이브러리가 객체를 생성할 때 리플랙션 같은 기술을 사용할 수 있도록 지원해야 하기 때문이다.

 

아이템(Item) 엔티티

package jpabook.jpashop.domain.item;

import jpabook.jpashop.domain.Category;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@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 int price;

    private String name;

    private int stockQuantity;
    
    @ManyToMany(mappedBy = "items")
    private List<Category> categories = new ArrayList<>();


}

 

아이템 - 책 엔티티

package jpabook.jpashop.domain.item;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

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

 

아이템 - 영화 엔티티

package jpabook.jpashop.domain.item;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity
@DiscriminatorValue("M")
@Getter @Setter
public class Movie extends Item{

    private String director;
    private String actor;
}

 

아이템 - 앨범 엔티티

package jpabook.jpashop.domain.item;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

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

 

카테고리(Category) 엔티티

package jpabook.jpashop.domain;

import jpabook.jpashop.domain.item.Item;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
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;

    @ManyToMany
    @JoinTable(name = "category_item",
            joinColumns = @JoinColumn(name = "category_id"),
            inverseJoinColumns = @JoinColumn(name = "item_id"))
    private List<Item> items = new ArrayList<>();

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Category parent;

    @OneToMany(mappedBy = "parent")
    private List<Category> child = new ArrayList<>();

}
※ 참고 
실무에서는 @ManyToMany 를 사용하지 말자
@ManyToMany 는 편리한 것 같지만, 중간 테이블( CATEGORY_ITEM )에 컬럼을 추가할 수 없고, 세밀하게 쿼리를 실행하기 어렵기 때문에 실무에서 사용하기에는 한계가 있다. 중간 엔티티( CategoryItem 를 만들고 @ManyToOne , @OneToMany 로 매핑해서 사용하자. 정리하면 대다대 매핑을 일대다, 다대일 매핑으로 풀어내서 사용하자.

 

※ 참고 

이론적으로 Getter, Setter 모두 제공하지 않고, 꼭 필요한 별도의 메서드를 제공하는게 가장 이상적이다. 하지만 실무에서 엔티티의 데이터는 조회할 일이 너무 많으므로, Getter의 경우 모두 열어두는 것이 편리하다. Getter는 아무리 호출해도 호출 하는 것 만으로 어떤 일이 발생하지는 않는다. 하지만 Setter는 문제가 다르다. Setter를 호출하면 데이터가 변한다. Setter를 막 열어두면 가까운 미래에 엔티티에가 도대체 왜 변경되는지 추적하기 점점 힘들어진다. 그래서 엔티티를 변경할 때는 Setter 대신에 변경 지점이 명확하도록 변경을 위한 비즈니스 메서드를 별도로 제공해야 한다

 

 

 

엔티티 설계시 주의점


엔티티에서는 가급적 Setter를 사용하지 말자.

  • Setter가 열려있으면 변경 포인트가 너무 많아 유지보수가 어렵다.

 

모든 연관관계는 지연로딩으로 설정

  • 즉시로딩(EAGER)은 예측이 어렵고, 어떤 SQL이 실행될 지 추적하기 어렵다.
  • 특히, JPQL을 실행할 때 N+1 문제가 자주 발생한다.
  • 실무에서 모든 연관관계는 지연로딩( LAZY )으로 설정해야 한다.
  • ManyToOne이나 OneToOne과 같은 xxxToOne 매핑은 기본 전략이 EAGER이므로 수동으로 LAZE로 바꾸도록 해야 한다.
  • 연관된 엔티티를 함께 DB에서 조회해야 하면, fetch join 또는 엔티티 그래프 기능을 사용한다.

 

컬렉션은 필드에서 초기화 하자.

  • 컬렉션은 필드에서 바로 초기화 하는것이 안전하다.
  • null 문제에서 안전하다.
  • 하이버네이트는 엔티티를 영속화 할 때, 컬렉션을 감싸서 하이버네이트가 제공하는 내장 컬렉션으로 변경한다.
  • → 만약 getOrders() 처럼 임의의 메서드에 컬렉션을 잘못 생성하면 하이버네이트 내부 메커니즘에 문제가 발생할 수 있다.
Member member = new Member();
System.out.println(member.getOrders().getClass());
em.persist(member);
System.out.println(member.getOrders().getClass());
//출력 결과
class java.util.ArrayList
class org.hibernate.collection.internal.PersistentBag

 

테이블 컬럼명 생성 전략

 

스프링 부트에서 하이버네이트 기본 매핑 전략을 변경해서 실제 테이블 필드명은 다름 

  • https://docs.spring.io/spring-boot/docs/2.1.3.RELEASE/reference/htmlsingle/#howtoconfigure-hibernate-naming-strategy
  • http://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/ Hibernate_User_Guide.html#naming

하이버네이트 기존 구현: 엔티티의 필드명을 그대로 테이블 명으로 사용(SpringPhysicalNamingStrategy)

 

스프링 부트 신규 설정(엔티티(필드)→테이블(컬럼))

  1. 카멜 케이스 → 언더스코어(memberPoint => member_point)
  2. .(점) => _(언더스코어)
  3. 대문자 => 소문자

 

 

적용 2단계

 

  1. 논리명 생성: 명시적으로 컬럼, 테이블명을 직접 적지 않으면 ImplictNamingStrategy사용spring.jpa.hibernate.naming.implict-strategy: 테이블이나, 컬럼명을 명시하지 않을 때 논리명 적용.
  2. 물리명 적용:
    spring.jpa.hibernate.naming.physical-strategy: 모든 논리명에 적용됨, 실제 테이블에 적용
    (username → usernm등으로 회사 규정대로 바꿀 수 있음)

 

스프링 부트 기본 설정

spring.jpa.hibernate.naming.implicit-strategy:
org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
spring.jpa.hibernate.naminig.physical-strategy:
org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
'JPA/1편- 실전! 스프링 부트와 JPA 활용' 카테고리의 다른 글
  • 상품 도메인 개발
  • 회원 도메인 개발
  • 애플리케이션 구현 준비
  • 도메인 분석 설계
s_y_130
s_y_130
  • s_y_130
    About SY
    s_y_130
  • 전체
    오늘
    어제
    • 분류 전체보기 (434)
      • JAVA (54)
        • 더 자바 8 (0)
        • JAVA (41)
        • JAVA (JVM) (13)
      • Computer Science (86)
        • CS Basic (7)
        • OOP (11)
        • Design Pattern (16)
        • Network (8)
        • HTTP (6)
        • WEB (22)
        • OS (16)
      • DataBase (29)
        • DB theory (15)
        • MySQL (14)
        • Redis (0)
      • Collection Framework (1)
        • 구현 (1)
      • Data Structure (14)
        • Linear (9)
        • Non-Linear (5)
      • Algorithm (19)
        • Basic (12)
        • 응용 (2)
        • 완전 탐색(Brute Force) (1)
        • 다익스트라 (1)
        • Algorithm Problem (3)
      • Spring (104)
        • 스프링 핵심 원리 - 기본편 (9)
        • 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 (7)
        • 스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술 (11)
        • 스프링 DB 1편 - 데이터 접근 핵심 원리 (6)
        • 스프링 DB 2편 - 데이터 접근 활용 기술 (10)
        • 스프링 핵심 원리 - 고급편 (13)
        • 스프링 부트 - 핵심 원리와 활용 (9)
        • Spring Security 6.x (2)
        • Spring Batch (2)
        • Spring Cloud로 개발하는 MSA (1)
        • 재고시스템으로 알아보는 동시성이슈 해결방법 (4)
        • 개념 (27)
        • 테스트 (0)
        • Annotation (1)
        • Error Log (2)
      • TEST (0)
        • 부하 테스트 (0)
        • Practical Testing: 실용적인 테스트.. (0)
      • JPA (40)
        • 자바 ORM 표준 JPA 프로그래밍 (12)
        • 1편- 실전! 스프링 부트와 JPA 활용 (7)
        • 2편- 실전! 스프링 부트와 JPA 활용 (4)
        • 실전! 스프링 데이터 JPA (6)
        • 실전! Querydsl (6)
        • 개념 (5)
      • 백엔드 부트캠프[사전캠프] (35)
        • TIL (12)
        • 문제풀이 (23)
      • 백엔드 부트캠프 (3)
        • Calculator (3)
      • Open Source (0)
      • Book Study (1)
        • Morden Java in Action (1)
        • Real MySQL 8.0 Vol.1 (0)
        • TDD : By Example (0)
      • AWS (0)
        • EC2 (0)
      • git (2)
      • AI (22)
        • Machine Learning (17)
        • Deep Learning (0)
        • TensorFlow (1)
        • PyTorch (1)
        • YOLO (1)
        • Data Analysis (0)
        • Ai code Error (1)
        • Numpy (1)
      • MY (0)
      • WEB (15)
        • Django (3)
        • WEB 개념 (1)
        • React (1)
        • Maven (10)
      • Python (6)
      • 기초수학 (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
s_y_130
도메인 분석 설계 - 엔티티 클래스 개발
상단으로

티스토리툴바