오봉이와 함께하는 개발 블로그

JPA 활용 1 - 엔티티 클래스 개발 본문

BE/JPA

JPA 활용 1 - 엔티티 클래스 개발

오봉봉이 2022. 9. 2. 21:46
728x90

엔티티 클래스 개발

  • 엔티티 클래스에 Getter, Setter를 모두 열고, 최대한 단순하게 설계
  • 실무에서는 가급적 Getter는 열어두고, Setter는 꼭 필요한 경우에만 사용하는 것을 추천

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

회원 엔티티

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

양방향 참조 관계에서 어떤 테이블의 값이 변경됐을 때 외래키 값을 업데이트 해야할지 JPA는 알 수 없기 때문에 @OneToMany(mappedBy = "member")를 통해 해당 컬럼은 조회용으로만 사용하게 설정했다.

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

주문 엔티티

@Entity
@Table(name = "orders")
@Getter
@Setter
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, 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);
    }

@JoinColumn(name = "member_id")를 통해 외래키 이름을 설정함.

OrderDelivery는 일대일 관계이기 때문에 외래키를 어디에 두어도 상관 없지만 주로 어떤 테이블에서 어떤 테이블을 조회하는지에 따라 외래키의 위치를 정하는 것이 좋다.
주문배송에서는 주로 주문에서 배송 상태를 조회할 것이기 때문에 주문에 외래키를 두어 연관관계 주인으로 설정하자.(@JoinColumn(name = "delivery_id"))

주문상태

public enum OrderStatus {
    ORDER, CANCEL
}

주문상품 엔티티

@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 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
@Getter
@Setter
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")
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<>();
}

구현체를 가지고 사용하기 때문에 추상 클래스로 만들었다(abstract)
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
InheritanceType으로는 SINGLE_TABLE, TABLE_PER_CLASS, JOINED가 있다.

  • JOINED : 가장 정규화된 스타일
  • SINGLE_TABLE : 한 테이블에 다 넣는 스타일
  • TABLE_PER_CLASS : 추상 클래스를 구현하는 클래스마다 테이블을 생성하는 전략

@DiscriminatorColumn(name = "dtype")을 통해 구분할 수 있도록 한다.

@ManyToMany(mappedBy = "items")로 카테고리와 아이템의 다대다 관계를 풀어낼 때 조회용 테이블로 설정했다.

상품 - 음반, 도서, 영화 엔티티

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

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

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

@DiscriminatorValue("A")를 통해서 구분한다.(@DiscriminatorColumn(name = "dtype"))

배송 엔티티, 배송 상태

@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; // READY(준비), COMP(배송)
}

public enum DeliveryStatus {
    READY, COMP
}

내장 타입이기 때문에 @Embedded를 사용한다.
@Enumerated(EnumType.STRING)를 사용한다.
EnumTypeORDINAL, STRING이 있는데 ORDINAL을 사용하면 순서가 작용해서 숫자로 표현되기 때문에 항목 추가가 발생하면 꼬이게 되어 문제가 생긴다.
꼭!!! STRING을 사용하도록 하자.

@OneToOne(mappedBy = "delivery")
주문(Order)과 배송(Delivery)에서 주문에 외래키를 두어 연관관계 주인으로 설정했기 때문에 mappedBy를 통해 조회만 할 수 있게 설정하자.

카테고리 엔티티

@Entity
@Getter @Setter
public class Category {
    @Id
    @GeneratedValue
    @Column(name = "catagory_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);
    }
}

설계를 보면 다대다 관계에서 중간에 CATEGORY_ITEM이라는 매핑 테이블을 두어 사용하기 때문에 @JoinTable(name = "category_item"을 통해 설정했다.
joinColumns = @JoinColumn(name = "category_id")CATEGORY_ITEM에서 사용하는 외래키로 CategotyPK다.
마찬가지로 inverseJoinColumns = @JoinColumn(name = "item_id")ITEM에 있는 PK다.

카테고리는 보통 계층 구조로 이루어져 있는데, 아래 코드를 통해 계층 구조를 표현했다.
같은 엔티티에 대하여 셀프로 양방향 연관관계를 걸었다.

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Category parent;

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

주소 값 타입

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

값 타입은 변경 불가능하게 설계해야 한다.

@Setter를 제거하고, 생성자에서 값을 모두 초기화해서 변경 불가능한 클래스를 만들자.

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

출처 : 인프런 김영한 지식공유자님 강의 - 실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발
728x90
Comments