@ManyToOne(fetch = FetchType.EAGER)와 N+1 문제
문제 설명
@ManyToOne(fetch = FetchType.EAGER)
설정은 N+1 문제를 유발할 가능성이 높습니다. 예를 들어, 아래와 같은 상황을 가정해보겠습니다.
코드 예시
@Entity
public class Order {
@Id
private Long id;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "member_id")
private Member member;
}
@Entity
public class Member {
@Id
private Long id;
private String name;
}
JPQL 예시
List<Order> orders = entityManager.createQuery("SELECT o FROM Order o", Order.class).getResultList();
동작 방식
SELECT o FROM Order o
를 실행하면Order
엔티티 100건이 조회됩니다.FetchType.EAGER
설정 때문에, 각Order
에 연관된Member
엔티티도 즉시 로드됩니다.- 따라서, 각
Order
에 대해 개별적으로Member
를 조회하는 쿼리가 실행됩니다.
결과적으로 아래와 같은 SQL 쿼리가 실행됩니다:
1. SELECT * FROM orders; // Order 100건 조회
2. SELECT * FROM members WHERE id = ?; // Member를 100번 반복 조회
즉, Order
가 100건이면, 기본 쿼리 1번과 추가적으로 연관된 Member
를 100번 조회하는 쿼리가 실행되어 총 101개의 쿼리가 발생합니다.
해결 방법
1. FetchType.LAZY 사용
FetchType.LAZY
로 설정하여, Member
를 실제로 접근할 때만 조회하도록 변경합니다.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
2. JPQL에서 JOIN FETCH 사용
데이터가 필요하다면 한 번의 쿼리로 연관된 데이터를 조회합니다.
List<Order> orders = entityManager.createQuery(
"SELECT o FROM Order o JOIN FETCH o.member", Order.class).getResultList();
3. Hibernate Batch Size 설정
N+1 문제를 완화하기 위해 Hibernate의 @BatchSize
를 사용하여 특정 배치 크기만큼 연관된 엔티티를 한 번에 가져오도록 설정합니다.
@Entity
@BatchSize(size = 10)
public class Order {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
}
4. Second Level Cache 활용
자주 조회되는 연관 데이터를 캐싱하여 쿼리 호출 횟수를 줄일 수 있습니다.
결론
FetchType.EAGER
를 사용하는 경우 연관 관계가 많거나 데이터 양이 많다면 N+1 문제가 심각해질 수 있습니다. 꼭 필요한 경우에만 사용하거나 JOIN FETCH
와 같은 전략을 병행하는 것이 좋습니다.
@OneToMany와 CascadeType.ALL 설명
CascadeType.ALL의 역할
@OneToMany
에 cascade = CascadeType.ALL
을 설정하면, 연관된 엔티티에 대한 작업(예: persist
, merge
, remove
등)이 부모 엔티티(Order
)에 수행될 때 자식 엔티티(OrderItem
)에도 동일하게 적용된다는 뜻입니다.
예시 코드
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
주요 설정
mappedBy = "order"
:OrderItem
엔티티가Order
를 참조하는 관계라는 것을 지정합니다. 즉,OrderItem
쪽에서@ManyToOne
으로Order
를 참조하고 있어야 합니다.cascade = CascadeType.ALL
:Order
엔티티에 대해 수행되는 모든 작업(persist
,merge
,remove
등)이 연관된OrderItem
엔티티에도 자동으로 적용됩니다.List<OrderItem> orderItems
:Order
는 여러OrderItem
과 연관되어 있으며, 이를 리스트로 관리합니다.
일반적인 동작 (cascade가 없는 경우)
cascade 설정이 없으면, 각각의 자식 엔티티를 개별적으로 persist
해야 합니다.
persist(OrderItemA);
persist(OrderItemB);
persist(OrderItemC);
persist(Order);
Cascade 동작
cascade = CascadeType.ALL
이 설정되어 있다면, persist(Order)
만 호출해도 JPA가 자동으로 Order
와 연관된 모든 OrderItem
을 persist
합니다.
왜 그런가?
Order
를 persist
할 때, JPA는 Order
에 있는 연관된 orderItems
리스트를 탐색하여 각 OrderItem
을 자동으로 저장합니다. 따라서 개발자가 개별적으로 persist(OrderItemA)
, persist(OrderItemB)
등을 호출할 필요가 없습니다.
실제 동작 예시
Order order = new Order();
OrderItem itemA = new OrderItem();
OrderItem itemB = new OrderItem();
OrderItem itemC = new OrderItem();
// 연관 관계 설정
itemA.setOrder(order);
itemB.setOrder(order);
itemC.setOrder(order);
order.getOrderItems().add(itemA);
order.getOrderItems().add(itemB);
order.getOrderItems().add(itemC);
// cascade 설정이 있으므로 persist(Order)만 호출
entityManager.persist(order);
결과
Order
와 OrderItemA
, OrderItemB
, OrderItemC
가 모두 저장됩니다. JPA가 내부적으로 Order
의 orderItems
리스트를 순회하며 각 OrderItem
에 대해 persist
를 호출합니다.
Cascade의 장점
- 코드 간결화: 부모만 처리해도 자식 엔티티가 자동으로 관리됩니다.
- 유지보수 용이: 연관 엔티티 간의 동작을 일관되게 처리할 수 있습니다.
Cascade 사용 시 주의점
- 트랜잭션 관리:부모 엔티티와 자식 엔티티는 같은 트랜잭션 내에서 관리됩니다. 자식 엔티티 중 하나라도 문제가 발생하면 트랜잭션 전체가 롤백될 수 있습니다.
- CascadeType.ALL:모든 작업에 대해 전파되므로, 특정 작업(
remove
등)이 예상치 않게 연관 엔티티에도 적용될 수 있습니다. 필요에 따라 적절한 Cascade 타입(PERSIST
,MERGE
,REMOVE
등)만 사용해야 합니다.
결론
persist(Order)
만 호출하면 Order
와 연관된 모든 OrderItem
이 자동으로 저장되는 이유는 cascade = CascadeType.ALL
설정으로 인해 Order
의 상태 변화가 OrderItem
으로 전파되기 때문입니다.
JPA 연관관계 편의 메서드
왜 연관관계 편의 메서드가 필요한가?
JPA에서는 객체와 관계형 데이터베이스 간의 매핑을 처리합니다. 하지만 객체 간의 연관 관계는 양방향으로 설정되지만, 관계형 데이터베이스에서는 외래 키를 통해 단방향으로 관리됩니다. 이로 인해 객체와 데이터베이스 간의 관계를 동기화하려면 다음과 같은 문제가 발생할 수 있습니다:
- 양쪽 엔티티의 연관 관계를 각각 설정하지 않으면, 불일치가 발생합니다.
- 코드가 지저분해지고, 실수가 발생하기 쉽습니다.
연관관계 편의 메서드는 이런 문제를 해결하고, 연관 관계를 양방향으로 올바르게 설정해 줍니다.
예시 코드와 설명
예시 1: @OneToMany
와 @ManyToOne
연관 관계
엔티티 구조
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL)
private List<Order> orders = new ArrayList<>();
// 연관관계 편의 메서드
public void addOrder(Order order) {
this.orders.add(order);
order.setMember(this); // 반대쪽 연관관계도 설정
}
}
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
private String productName;
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
public void setMember(Member member) {
this.member = member;
}
}
연관 관계 설정 시 문제점
Member member = new Member();
Order order = new Order();
order.setMember(member);
member.getOrders().add(order);
두 번의 설정이 필요합니다. order.setMember(member)
와 member.getOrders().add(order)
가 누락되면 연관 관계가 깨질 수 있습니다.
연관관계 편의 메서드 사용
Member member = new Member();
Order order = new Order();
// 연관관계 편의 메서드 사용
member.addOrder(order);
addOrder
메서드 내부에서 member.getOrders().add(order)
와 order.setMember(member)
를 동시에 처리합니다. 코드가 간결해지고 실수를 방지할 수 있습니다.
예시 2: @OneToOne
연관 관계
엔티티 구조
@Entity
public class User {
@Id @GeneratedValue
private Long id;
private String name;
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
private Profile profile;
// 연관관계 편의 메서드
public void setProfile(Profile profile) {
this.profile = profile;
profile.setUser(this); // 반대쪽 설정
}
}
@Entity
public class Profile {
@Id @GeneratedValue
private Long id;
private String bio;
@OneToOne
@JoinColumn(name = "user_id")
private User user;
public void setUser(User user) {
this.user = user;
}
}
편의 메서드 사용
User user = new User();
Profile profile = new Profile();
// 편의 메서드 사용
user.setProfile(profile);
user.setProfile(profile)
만 호출하면, profile.setUser(user)
도 자동으로 호출되어 양방향 관계가 설정됩니다.
예시 3: @ManyToMany
연관 관계
엔티티 구조
@Entity
public class Student {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToMany
@JoinTable(name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private List<Course> courses = new ArrayList<>();
// 연관관계 편의 메서드
public void addCourse(Course course) {
this.courses.add(course);
course.getStudents().add(this); // 반대쪽 설정
}
}
@Entity
public class Course {
@Id @GeneratedValue
private Long id;
private String title;
@ManyToMany(mappedBy = "courses")
private List<Student> students = new ArrayList<>();
public void addStudent(Student student) {
this.students.add(student);
student.getCourses().add(this); // 반대쪽 설정
}
}
편의 메서드 사용
Student student = new Student();
Course course = new Course();
// 편의 메서드 사용
student.addCourse(course);
student.addCourse(course)
를 호출하면, course.getStudents().add(student)
도 설정됩니다. 양방향 관계가 일관되게 관리됩니다.
연관관계 편의 메서드 작성 시 주의사항
- 한쪽에서만 연관관계를 관리하도록 구현:양쪽에서 관계를 설정하면 순환 참조가 발생할 수 있습니다. 편의 메서드에서 한쪽이 주인 역할을 수행하고, 다른 쪽은 참조만 하도록 제한합니다.
- 중복 설정 방지:같은 관계를 중복으로 설정하지 않도록 내부 로직에서 체크가 필요합니다.
- 일관성 유지:양방향 관계를 편의 메서드를 통해 관리하면, 데이터의 일관성을 보장할 수 있습니다.
결론
연관관계 편의 메서드를 사용하면 코드의 가독성과 유지보수성이 높아지고, 관계 설정 시 발생할 수 있는 실수를 방지할 수 있습니다.
❓질의
다음과 같은 관계가 있다:
- 회원 - 주문: 1:N
- 주문 - 배송: 1:1
- 주문 - 주문상품: 1:N
- 주문상품 - 상품: 1:N
주문 Class의 연관관계 편의 메서드
//==연관관계 편의 메서드==//
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);
}
addOrderItem(OrderItem orderItem)
메서드는 OrderItem
클래스에 있어도 되는 것 아닌가? 어떤 기준으로 연관관계 편의 메서드를 위치시키는지 궁금하다.
연관관계 편의 메서드 위치
연관관계 편의 메서드는 엔티티 간의 양방향 관계를 설정하거나 관리하는 역할을 합니다. 메서드를 특정 클래스에 위치시킬 때는 다음 기준을 따릅니다:
1. 왜 Order
클래스에 두는가?
- 비즈니스 관점:
Order
는 핵심 엔티티로, 주문과 관련된 모든 요소(Member
,OrderItem
,Delivery
)를 관리합니다. 주문 생성 시 관련 데이터를 함께 처리하는 일이 많아Order
에 편의 메서드를 두는 것이 자연스럽습니다. - 관계의 주도권:
Order
는OrderItem
과의 관계에서 주도권을 가지며,OrderItem
은 종속적입니다. 따라서 관계 설정을Order
가 관리해야 합니다.
2. 왜 OrderItem
에 두지 않는가?
- 관계 흐름의 부자연스러움:
OrderItem
에서Order
를 추가하는 방식은 비즈니스 흐름에 어긋납니다. 보통 주문을 생성하면서 주문상품을 추가하는 흐름이 더 자연스럽습니다. - 역할의 중복:양쪽에 관계 설정 메서드가 존재하면 혼란과 데이터 무결성 문제가 발생할 수 있습니다.
3. 연관관계 편의 메서드 위치 기준
- 주도권의 방향: 관계를 주도하는 엔티티에 위치시킵니다.
- 비즈니스 흐름: 비즈니스 로직에서 더 자주 사용되는 클래스에 둡니다.
- 일관성 유지: 관계 설정을 명확히 하여 데이터 무결성을 보장합니다.
4. 결론
연관관계 편의 메서드는 Order
와 같은 주도적인 엔티티에 두는 것이 원칙입니다. 이는 비즈니스 흐름을 반영하고 코드의 일관성을 유지하는 데 가장 적합합니다.
'JPA' 카테고리의 다른 글
JPA EntityManagerFactory, EntityManager는 사용 후 꼭 닫아야 한다 (3) | 2024.12.05 |
---|---|
JPA 구동방식 (0) | 2024.12.05 |
hibernate entitymanager 의존성 (0) | 2024.12.05 |
em.persist vs em.merge (1) | 2024.11.30 |
도메인 주도설계와 OOP의 관계에 대한 chatGPT와의 질의응답 (0) | 2024.11.24 |
댓글