오봉이와 함께하는 개발 블로그
JPA 활용 1 - 엔티티 클래스 개발 본문
엔티티 클래스 개발
- 엔티티 클래스에 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")
를 통해 외래키 이름을 설정함.
Order
와 Delivery
는 일대일 관계이기 때문에 외래키를 어디에 두어도 상관 없지만 주로 어떤 테이블에서 어떤 테이블을 조회하는지에 따라 외래키의 위치를 정하는 것이 좋다.주문
과 배송
에서는 주로 주문에서 배송 상태를 조회할 것이기 때문에 주문
에 외래키를 두어 연관관계 주인으로 설정하자.(@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)
를 사용한다.EnumType
은 ORDINAL
, 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
에서 사용하는 외래키로 Categoty
의 PK
다.
마찬가지로 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 - 웹 애플리케이션 개발
'BE > JPA' 카테고리의 다른 글
JPA 활용 1 - 변경 감지와 병합(merge) (0) | 2022.09.03 |
---|---|
JPA 활용 1 - 엔티티 설계시 주의점 (0) | 2022.09.02 |
JPA 활용 1 - 도메인 모델과 엔티티 설계 (0) | 2022.09.02 |
JPA - 벌크 연산 (0) | 2022.07.06 |
JPA - 다형성 쿼리, 엔티티 직접 사용, Named 쿼리 (0) | 2022.07.06 |