개발인생/Backend

[ORM] Lazy Loading vs Eager Loading

forri 2025. 3. 19. 19:17

📍Lazy Loading 과 Eager Loading는

ORM에서 연관 데이터를 가져오는 방법을 결정하는 전략으로 JPA에서 연관된 엔티티를 가져오는 방식

1. Lazy Loading (지연 로딩)

  • 기본적으로 필요할 때만 연관된 데이터를 가져옴
  • 처음에는 프록시 객체를 반환하고 실제 데이터가 필요할 때 쿼리가 실행
  • 성능 최적화에 좋지만 LazyInitializationException(영속성 컨텍스트가 닫힌 후 조회 시 발생)을 주의해야함

2. Eager Loading (즉시 로딩)

  • 연관된 엔티티를 즉시 조회하여 한 번의 쿼리로 가져옴
  • 필요 없는 데이터도 미리 가져오기 때문에 성능 저하가 발생할 수 있음
ORM(Object-Relational Mapping)
객체와 데이터베이스를 매핑하여 SQL 없이 데이터 조작을 가능하게 하는 기술
ex. JPA (Hibernate), TypeORM, Sequelize

📍예제

  • User, Order, OrderItem, Product 테이블 간의 1:N, N:1 관계를 포함

1️⃣ 데이터 모델

  • User(사용자) → 여러 개의 Order(주문)을 가질 수 있음 (1:N)
  • Order(주문) → 여러 개의 OrderItem(주문 상세)을 가짐 (1:N)
  • OrderItem(주문 상세) → Product(상품)을 참조 (N:1)

2️⃣ 엔티티 설계

(1) User 엔티티 (Lazy Loading)

import jakarta.persistence.*;
import java.util.List;

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) // Lazy Loading
    private List<Order> orders;

    // Getter, Setter
}

 

(2) Order 엔티티 (Eager Loading)

import jakarta.persistence.*;
import java.util.List;

@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String orderDate;

    @ManyToOne(fetch = FetchType.LAZY) // Lazy Loading
    @JoinColumn(name = "user_id")
    private User user;

    @OneToMany(mappedBy = "order", fetch = FetchType.EAGER) // Eager Loading
    private List<OrderItem> orderItems;

    // Getter, Setter
}

 

(3) OrderItem 엔티티 (Lazy Loading)

import jakarta.persistence.*;

@Entity
public class OrderItem {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private int quantity;

    @ManyToOne(fetch = FetchType.LAZY) // Lazy Loading
    @JoinColumn(name = "order_id")
    private Order order;

    @ManyToOne(fetch = FetchType.LAZY) // Lazy Loading
    @JoinColumn(name = "product_id")
    private Product product;

    // Getter, Setter
}

 

(4) Product 엔티티 (Eager Loading)

import jakarta.persistence.*;

@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private int price;

    // Getter, Setter
}

 

 

3️⃣ Lazy vs Eager Loading 실행 예제

(1) Lazy Loading 사용 시

User user = entityManager.find(User.class, 1L);
System.out.println(user.getName()); // ✅ User 정보만 가져옴

List<Order> orders = user.getOrders(); // ❌ 아직 Orders 조회 안 됨
System.out.println(orders.size()); // ✅ 이 시점에서 Order 조회 쿼리 실행됨

Order order = orders.get(0);
List<OrderItem> orderItems = order.getOrderItems(); // ✅ Eager Loading이므로 즉시 조회됨

 

실행되는 SQL

SELECT * FROM User WHERE id = 1; -- User만 조회

SELECT * FROM Order WHERE user_id = 1; -- 이 시점에서 Order 조회됨

SELECT * FROM OrderItem WHERE order_id = 1; -- Eager로 OrderItem 즉시 조회됨

 

(2) Eager Loading 사용 시

Order order = entityManager.find(Order.class, 1L);
System.out.println(order.getOrderDate()); // ✅ Order 즉시 로드됨

List<OrderItem> items = order.getOrderItems(); // ✅ Eager Loading이므로 이미 가져옴
System.out.println(items.size());

 

실행되는 SQL

SELECT o.*, oi.* FROM Order o
LEFT JOIN OrderItem oi ON o.id = oi.order_id
WHERE o.id = 1;
  • 장점: 한 번의 SQL로 조회 가능
  • 단점: 필요 없는 데이터까지 불러올 가능성 있음

 

4️⃣ Lazy Initialization Exception 방지 방법

Lazy Loading을 사용할 때 트랜잭션이 끝난 후 객체를 조회하면 LazyInitializationException이 발생

@Transactional
public void loadUserData() {
    User user = entityManager.find(User.class, 1L);
    System.out.println(user.getOrders().size()); // ✅ 트랜잭션 내에서 실행해야 함
}

 

JOIN FETCH를 사용하여 미리 가져올 수도 있음

List<User> users = entityManager.createQuery(
    "SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = :id", User.class)
    .setParameter("id", 1L)
    .getResultList();

'개발인생 > Backend' 카테고리의 다른 글

JSON 파싱이란?  (1) 2025.03.25
[ORM] JPA, JPQL, QueryDSL - Spring과의 관계  (1) 2025.03.19
Spring vs Spring Boot  (2) 2025.03.19
[Spring] @Transactional  (0) 2025.03.19
[Spring] Spring Data JPA  (0) 2025.03.19