참고: 실제 코드에서는 DB에 소문자 + _(언더스코어) 스타일을 사용하겠다.**
데이터베이스 테이블명, 컬럼명에 대한 관례는 회사마다 다르다. 보통은 대문자 + _(언더스코어)나 소문자 + _(언 더스코어) 방식 중에 하나를 지정해서 일관성 있게 사용한다.
**참고: 외래 키가 있는 곳을 연관관계의 주인으로 정해라.**
연관관계의 주인은 단순히 외래 키를 누가 관리하냐의 문제이지 비즈니스상 우위에 있다고 주인으로 정하면 안된 다.. 예를 들어서 자동차와 바퀴가 있으면, 일대다 관계에서 항상 다쪽에 외래 키가 있으므로 외래 키가 있는 바퀴 를 연관관계의 주인으로 정하면 된다. 물론 자동차를 연관관계의 주인으로 정하는 것이 불가능 한 것은 아니지만, 자동차를 연관관계의 주인으로 정하면 자동차가 관리하지 않는 바퀴 테이블의 외래 키 값이 업데이트 되므로 관리 와 유지보수가 어렵고, 추가적으로 별도의 업데이트 쿼리가 발생하는 성능 문제도 있다.
연관관계의 주인을 다쪽(N)에서 관리하는 것이 더 쉬운 이유는 다음과 같습니다:
- 외래 키 관리 용이성:
- 다(N)쪽에서 외래 키를 관리하는 경우, 외래 키는 항상 다수의 엔티티가 존재하는 테이블에 위치하게 됩니다. 이는 데이터베이스의 구조와 맞아떨어지며, 각 다수의 엔티티가 자신이 속한 하나의 엔티티를 참조하는 형태가 됩니다. 예를 들어, 바퀴(다쪽)에 외래 키가 있으면, 각 바퀴가 자신이 속한 자동차를 참조하게 됩니다.
- 연관관계의 주인이 외래 키를 관리하는 이유:
- 외래 키를 소유한 엔티티가 연관관계의 주인입니다. 주인이 아닌 쪽에서 외래 키를 변경할 수 없도록 해야 하기 때문에, 외래 키를 소유한 쪽에서 연관관계를 관리하는 것이 자연스럽습니다. 예를 들어, 자동차(1쪽)에서 바퀴(N쪽)의 외래 키를 관리하는 것은 부자연스럽고, 바퀴가 어떤 자동차에 속하는지를 바꾸려면 바퀴 엔티티를 수정하는 것이 맞습니다.
- 성능과 유지보수:
- 외래 키가 있는 테이블에서만 업데이트가 이루어지기 때문에, 추가적인 업데이트 쿼리가 발생하지 않습니다. 예를 들어, 자동차가 외래 키를 관리하면 자동차를 업데이트할 때마다 관련된 모든 바퀴의 외래 키를 업데이트해야 하는데, 이는 성능과 유지보수에 불리합니다.
- 데이터의 일관성:
- 다쪽에서 외래 키를 관리하면, 데이터의 일관성을 유지하기가 더 쉽습니다. 바퀴가 어느 자동차에 속하는지 바꾸고자 할 때, 바퀴 테이블의 외래 키만 업데이트하면 됩니다.
참고: 이론적으로 Getter, Setter 모두 제공하지 않고, 꼭 필요한 별도의 메서드를 제공하는게 가장 이상적이다. 하지만 실무에서 엔티티의 데이터는 조회할 일이 너무 많으므로, Getter의 경우 모두 열어두는 것이 편리하다. Getter는 아무리 호출해도 호출 하는 것 만으로 어떤 일이 발생하지는 않는다. 하지만 Setter는 문제가 다르다. Setter를 호출하면 데이터가 변한다. Setter를 막 열어두면 가까운 미래에 엔티티가 도대체 왜 변경되는지 추적 하기 점점 힘들어진다. 그래서 엔티티를 변경할 때는 Setter 대신에 변경 지점이 명확하도록 변경을 위한 비즈니 스 메서드를 별도로 제공해야 한다.
package com.example.jpaspring.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
private String name;
@Embedded
private Address address;
@OneToMany(mappedBy = "member")
private List<Orders> orders = new ArrayList<>();
}
참고: 엔티티의 식별자는 `id` 를 사용하고 PK 컬럼명은 `member_id` 를 사용했다. 엔티티는 타입(여기서는 `Member` )이 있으므로 `id` 필드만으로 쉽게 구분할 수 있다. 테이블은 타입이 없으므로 구분이 어렵다. 그리고 테
이블은 관례상 `테이블명 + id` 를 많이 사용한다. 참고로 객체에서 `id` 대신에 `memberId` 를 사용해도 된다. 중 요한 것은 일관성이다.
주문 엔티티
package com.example.jpaspring.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter @Setter
@Table(name = "orders")
public class Orders {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "orders_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@OneToMany(mappedBy = "orders", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<OrderItem>();
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
private LocalDateTime orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus status;
// == 연관관계 메서드 == //
public void setMember(Member member) {
this.member = member;
member.getOrders().add(this);
}
public void addOrderItem(OrderItem orderItem) {
orderItems.add(orderItem);
orderItem.setOrders(this);
}
public void setDelivery(Delivery delivery){
this.delivery = delivery;
delivery.setOrders(this);
}
}
주문 상태
package com.example.jpaspring.entity;
public enum OrderStatus {
ORDER, CANCEL
}
주문 상품 엔티티
package com.example.jpaspring.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter @Setter
public class OrderItem {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "order_item_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "orders_id")
private Item item;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "orders_id")
private Orders orders;
private int orderPrice;
private int count;
}
상품엔티티
package com.example.jpaspring.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
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(strategy = GenerationType.IDENTITY)
@Column(name = "item_id")
private Long id;
private String name;
private String price;
private int stockQuantity;
@ManyToMany(mappedBy = "items")
private List<Category> categories = new ArrayList<>();
}
상품 - 도서 엔티티
package com.example.jpaspring.entity;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import lombok.Getter;
import lombok.Setter;
@Entity
@DiscriminatorValue("B")
@Getter @Setter
public class Book extends Item{
private String author;
private String isbn;
}
상품 - 음반 엔티티
package com.example.jpaspring.entity;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import lombok.Getter;
import lombok.Setter;
@Entity
@DiscriminatorValue("A")
@Getter @Setter
public class Album extends Item{
private String artist;
private String etc;
}
상품 - 영화 엔티티
package com.example.jpaspring.entity;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import lombok.Getter;
import lombok.Setter;
@Entity
@DiscriminatorValue("M")
@Getter @Setter
public class Movie extends Item{
private String director;
private String actor;
}
배송엔티티
package com.example.jpaspring.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
public class Delivery {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "delivery_id")
private Long id;
@OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY)
private Orders orders;
@Embedded
private Address address;
@Enumerated(EnumType.STRING)
private DeliveryStatus status;
}
배송 상태
package com.example.jpaspring.entity;
public enum DeliveryStatus {
READY, COMP // 준비, 배송
}
카테고리 엔티티
package com.example.jpaspring.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
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(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);
}
}
참고: 실무에서는 `@ManyToMany` 를 사용하지 말자
`@ManyToMany` 는 편리한 것 같지만, 중간 테이블( `CATEGORY_ITEM` )에 컬럼을 추가할 수 없고, 세밀하게 쿼리
를 실행하기 어렵기 때문에 실무에서 사용하기에는 한계가 있다. 중간 엔티티( `CategoryItem` 를 만들고 `@ManyToOne` , `@OneToMany` 로 매핑해서 사용하자. 정리하면 다대다 매핑을 일대다, 다대일 매핑으로 풀어내
서 사용하자.
**주소 값 타입**
package com.example.jpaspring.entity;
import jakarta.persistence.Embeddable;
import lombok.Getter;
@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 구현 라이브러리가 객체를 생성할 때 리플랙션 같은 기술을 사용할 수 있도 록 지원해야 하기 때문이다.
엔티티 설계 시 주의점
Setter 사용을 지양하자
- 모든 Setter가 열려 있으면 변경 포인트가 많아져 유지보수가 어려워집니다.
- 엔티티에 Setter를 모두 열어두기보다는 필요한 부분에만 Setter를 만들고, 생성자나 빌더 패턴을 이용해 객체를 생성하고 초기화하는 것이 좋습니다.
모든 연관관계는 지연로딩(LAZY)으로 설정하자
- 즉시 로딩(EAGER)은 예측이 어렵고, 어떤 SQL이 실행될지 추적하기 어렵습니다. 특히 JPQL을 실행할 때 N+1 문제를 발생시킬 수 있습니다.
- 실무에서는 모든 연관관계를 지연로딩으로 설정하는 것이 좋습니다. 연관된 엔티티를 실제 사용할 때만 조회하게 됩니다.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
연관된 엔티티를 함께 조회해야 하는 경우
- Fetch Join: 연관된 엔티티를 한 번에 조회할 때 사용합니다.
List<Order> orders = em.createQuery(
"select o from Order o join fetch o.member", Order.class)
.getResultList();
엔티티 그래프: 엔티티의 연관관계를 명시적으로 설정하여 조회할 때 사용합니다.
@EntityGraph(attributePaths = {"member", "orderItems"})
List<Order> findAll();
컬렉션은 필드에서 초기화하자
- null 문제를 방지하고, 하이버네이트 내부 메커니즘에 문제를 피하기 위해 컬렉션을 필드에서 초기화하는 것이 안전합니다.
@OneToMany(mappedBy = "orders", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
'JPA' 카테고리의 다른 글
[JPA] JPA 활용1 - 웹 계층 개발 - 회원 도메인 개발 (0) | 2024.06.30 |
---|---|
[JPA] JPA 활용1 - 프로젝트 환경설정 (0) | 2024.06.30 |
[JPA] JPA 기본값 타입 (0) | 2024.06.30 |
[JPA] JPA 프록시와 연관관계 관리 (0) | 2024.06.30 |
[JPA] JPA 고급 매핑 (0) | 2024.06.29 |