JPA

JPA FetchType.EAGER의 N+1 문제 & cascadeType.ALL 사용 이유 & 연관관계 편의 메서드

devJK93 2024. 11. 24.

 

 

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

동작 방식

  1. SELECT o FROM Order o를 실행하면 Order 엔티티 100건이 조회됩니다.
  2. FetchType.EAGER 설정 때문에, 각 Order에 연관된 Member 엔티티도 즉시 로드됩니다.
  3. 따라서, 각 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의 역할

@OneToManycascade = CascadeType.ALL을 설정하면, 연관된 엔티티에 대한 작업(예: persist, merge, remove 등)이 부모 엔티티(Order)에 수행될 때 자식 엔티티(OrderItem)에도 동일하게 적용된다는 뜻입니다.

예시 코드

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

주요 설정

  1. mappedBy = "order":OrderItem 엔티티가 Order를 참조하는 관계라는 것을 지정합니다. 즉, OrderItem 쪽에서 @ManyToOne으로 Order를 참조하고 있어야 합니다.
  2. cascade = CascadeType.ALL:Order 엔티티에 대해 수행되는 모든 작업(persist, merge, remove 등)이 연관된 OrderItem 엔티티에도 자동으로 적용됩니다.
  3. List<OrderItem> orderItems:Order는 여러 OrderItem과 연관되어 있으며, 이를 리스트로 관리합니다.

일반적인 동작 (cascade가 없는 경우)

cascade 설정이 없으면, 각각의 자식 엔티티를 개별적으로 persist해야 합니다.

persist(OrderItemA);
persist(OrderItemB);
persist(OrderItemC);
persist(Order);
    

Cascade 동작

cascade = CascadeType.ALL이 설정되어 있다면, persist(Order)만 호출해도 JPA가 자동으로 Order와 연관된 모든 OrderItempersist합니다.

왜 그런가?

Orderpersist할 때, 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);
    

결과

OrderOrderItemA, OrderItemB, OrderItemC가 모두 저장됩니다. JPA가 내부적으로 OrderorderItems 리스트를 순회하며 각 OrderItem에 대해 persist를 호출합니다.

Cascade의 장점

  • 코드 간결화: 부모만 처리해도 자식 엔티티가 자동으로 관리됩니다.
  • 유지보수 용이: 연관 엔티티 간의 동작을 일관되게 처리할 수 있습니다.

Cascade 사용 시 주의점

  1. 트랜잭션 관리:부모 엔티티와 자식 엔티티는 같은 트랜잭션 내에서 관리됩니다. 자식 엔티티 중 하나라도 문제가 발생하면 트랜잭션 전체가 롤백될 수 있습니다.
  2. 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. 한쪽에서만 연관관계를 관리하도록 구현:양쪽에서 관계를 설정하면 순환 참조가 발생할 수 있습니다. 편의 메서드에서 한쪽이 주인 역할을 수행하고, 다른 쪽은 참조만 하도록 제한합니다.
  2. 중복 설정 방지:같은 관계를 중복으로 설정하지 않도록 내부 로직에서 체크가 필요합니다.
  3. 일관성 유지:양방향 관계를 편의 메서드를 통해 관리하면, 데이터의 일관성을 보장할 수 있습니다.

결론

연관관계 편의 메서드를 사용하면 코드의 가독성과 유지보수성이 높아지고, 관계 설정 시 발생할 수 있는 실수를 방지할 수 있습니다.

 


 

❓질의

다음과 같은 관계가 있다:

  • 회원 - 주문: 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에 편의 메서드를 두는 것이 자연스럽습니다.
  • 관계의 주도권:OrderOrderItem과의 관계에서 주도권을 가지며, OrderItem은 종속적입니다. 따라서 관계 설정을 Order가 관리해야 합니다.

2. 왜 OrderItem에 두지 않는가?

  • 관계 흐름의 부자연스러움:OrderItem에서 Order를 추가하는 방식은 비즈니스 흐름에 어긋납니다. 보통 주문을 생성하면서 주문상품을 추가하는 흐름이 더 자연스럽습니다.
  • 역할의 중복:양쪽에 관계 설정 메서드가 존재하면 혼란과 데이터 무결성 문제가 발생할 수 있습니다.

3. 연관관계 편의 메서드 위치 기준

  • 주도권의 방향: 관계를 주도하는 엔티티에 위치시킵니다.
  • 비즈니스 흐름: 비즈니스 로직에서 더 자주 사용되는 클래스에 둡니다.
  • 일관성 유지: 관계 설정을 명확히 하여 데이터 무결성을 보장합니다.

4. 결론

연관관계 편의 메서드는 Order와 같은 주도적인 엔티티에 두는 것이 원칙입니다. 이는 비즈니스 흐름을 반영하고 코드의 일관성을 유지하는 데 가장 적합합니다.

댓글