개인 공부/JPA

Fetch Join시 생기는 문제

hanyugyeong 2024. 4. 28. 20:15
반응형
SMALL

JPA의 fetch join을 사용할 때 발생할 수 있는 몇가지 일반적인 문제에 대해서 

1. 데이터 부하: Fetch Join은 관련 엔티티를 함께 가져오므로 데이터베이스에서 불필요한 데이터를 로드할 수 있습니다 

특히 엔티티 간에 많은 관계가 있는 경우 이 문제가 발생할 수 있습니다. 불필요한 데이터를 가져오는 것은 성능에 부정적인 영향을 줄 수 있습니다. 

예시) 

주문(Order)과 주문상세(OrderDetail) 엔티티가 있습니다. 주문을 가져올 때 fetch join을 사용하여 주문 상세 정보를 함께 가져올 수 있습니다. 하지만 만약 주문에 연결된 주문 상세가 많고, 이를 모두 함께 로드하면 데이터베이스에서 불필요한 데이터를 가져오게 됩니다. 

 

@Entity
public class Order {
    @Id
    private Long id;

    @OneToMany(mappedBy = "order", fetch = FetchType.EAGER)
    private List<OrderDetail> orderDetails;
}

@Entity
public class OrderDetail {
    @Id
    private Long id;

    @ManyToOne
    private Order order;

    // other fields
}

2.N+1 문제

FetchType이 LAZY로 설정된 경우, fetch join을 사용하여 연관된 엔티티를 가져오더라도 여전히 N+1 문제가 발생할 수 있습니다. 이는 엔티티를 하나씩 가져오고 각각에 대해 추가적인 쿼리를 실행함으로써 발생합니다.

예시) 

위의 예시에서 주문(Order) 엔티티의 주문 상세(OrderDetail)를 FetchType이 LAZY로 설정했다고 가정합니다. 이때 fetch join을 사용하여 모든 주문과 연관된 주문 상세를 가져올 수 있지만, 실제로는 주문 상세가 필요한 시점에 N+1 문제가 발생할 수 있습니다.

List<Order> orders = entityManager.createQuery(
    "SELECT o FROM Order o JOIN FETCH o.orderDetails",
    Order.class
).getResultList();

for (Order order : orders) {
    // 여기서 order.getOrderDetails()를 호출할 때마다 추가적인 쿼리가 실행됨 (N+1 문제)
}

3.메모리 부하 

Fetch join을 사용하여 대량의 데이터를 가져올 경우 메모리 부하가 발생할 수 있습니다. 모든 데이터가 메모리에 로드되므로 JVM 메모리 사용량이 증가할 수 있습니다.

예시)

대량의 데이터를 처리할 때 fetch join을 사용하면 메모리 부하가 발생할 수 있습니다. 예를 들어, 수천 개의 주문을 가져올 때 각 주문에 대한 모든 주문 상세가 함께 로드되면 메모리 사용량이 급격히 증가할 수 있습니다.

List<Order> orders = entityManager.createQuery(
    "SELECT o FROM Order o JOIN FETCH o.orderDetails",
    Order.class
).getResultList();

4. 데이터 불일치 

Fetch join은 관련 엔티티를 함께 가져오지만, 이 엔티티가 이미 변경된 경우 데이터 불일치가 발생할 수 있습니다. 즉, 메인 엔티티와 연관된 엔티티가 서로 다른 트랜잭션에서 변경된 경우 문제가 발생할 수 있습니다.

예시)

데이터 불일치 문제는 여러 트랜잭션이 동시에 동일한 데이터를 변경할 때 발생할 수 있습니다. 예를 들어, 주문(Order)과 주문 상세(OrderDetail) 엔티티가 있고, 동일한 주문에 대한 주문 상세가 여러 개인 경우를 가정해봅시다. 이때 한 트랜잭션에서 주문에 연결된 주문 상세를 변경하고, 다른 트랜잭션에서는 fetch join을 사용하여 동일한 주문을 가져오면 데이터 불일치가 발생할 수 있습니다.

 

트랜잭션 간의 격리 수준 때문에 일어날 수 있습니다

대부분의 데이터베이스에서는 여러 트랜잭션이 동시에 데이터를 읽고 쓸 수 있습니다. 이때 한 트랜잭션이 변경한 데이터가 다른 트랜잭션에서 읽을 때 데이터의 일관성이 깨질 수 있습니다. 

fetch join을 사용하여 연관된 엔티티를 함께 로드할 때, 이 엔티티들은 영속 상태로 관리됩니다. 따라서 한 트랜잭션에서 엔티티를 변경하고 커밋한 후에는, 다른 트랜잭션에서 fetch join을 사용하여 동일한 엔티티를 로드하더라도 변경된 상태가 반영되지 않을 수 있습니다.

이는 일관된 데이터베이스 상태를 보장하기 위해 트랜잭션 격리 수준에 따라 발생할 수 있는 문제입니다. 대표적인 격리 수준 중 하나인 "Read Committed"에서는 커밋된 데이터만 읽을 수 있으므로, 한 트랜잭션이 변경한 데이터는 다른 트랜잭션에서 조회하지 못할 수 있습니다. 이러한 이유로 데이터 불일치 문제가 발생할 수 있습니다.

// Transaction 1
Order order = entityManager.find(Order.class, orderId);
OrderDetail orderDetail = order.getOrderDetails().get(0);
orderDetail.setQuantity(10);

// Transaction 2
Order order = entityManager.createQuery(
    "SELECT o FROM Order o JOIN FETCH o.orderDetails WHERE o.id = :orderId",
    Order.class
).setParameter("orderId", orderId)
.getSingleResult();
// 여기서 가져온 order.getOrderDetails()는 Transaction 1에서 변경된 상태와 일치하지 않을 수 있음
  1. 트랜잭션 A: 주문(Order) 엔티티와 그에 속한 주문 상세(OrderDetail) 엔티티를 변경하는 트랜잭션.
  2. 트랜잭션 B: 동일한 주문을 fetch join을 사용하여 가져오는 트랜잭션.

여기서 트랜잭션 A가 주문 상세를 변경하고 커밋한 후에, 트랜잭션 B가 같은 주문을 가져온다면 데이터 불일치 문제가 발생할 수 있습니다. 이는 트랜잭션 A의 변경이 커밋되기 전에는 다른 트랜잭션에서는 변경된 상태를 읽을 수 없기 때문입니다.

따라서 동시에 실행되는 트랜잭션 간에 데이터 일관성을 유지하기 위해서는 적절한 트랜잭션 격리 수준과 동시성 제어 기법을 사용해야 합니다. 일반적으로 격리 수준을 높이고, 트랜잭션을 충분히 빨리 완료하여 트랜잭션 간의 충돌 가능성을 최소화하는 것이 좋습니다.

 

. 여러 동시성 제어 기법 중에서 가장 일반적으로 사용되는 두 가지 기법인 Pessimistic Locking과 Optimistic Locking에 대한 예시를 들어보겠습니다.

1. Pessimistic Locking (비관적 잠금)

예를 들어, 은행 애플리케이션에서 두 명의 사용자가 동시에 동일한 계좌에서 돈을 인출하려고 할 때, Pessimistic Locking을 사용하여 충돌을 방지할 수 있습니다. 각 사용자는 인출하는 동안 해당 계좌를 잠그고, 다른 사용자는 해당 계좌가 잠겨있는 동안 대기하게 됩니다.

 

public void withdrawMoney(Account account, BigDecimal amount) {
    EntityManager entityManager = // EntityManager를 얻어옴
    entityManager.getTransaction().begin();
    
    // 해당 계좌를 잠금
    entityManager.lock(account, LockModeType.PESSIMISTIC_WRITE);
    
    // 계좌에서 돈을 인출하는 작업 수행
    account.withdraw(amount);
    
    // 커밋
    entityManager.getTransaction().commit();
}

2. Optimistic Locking (낙관적 잠금)

예를 들어, 온라인 쇼핑 애플리케이션에서 두 명의 사용자가 동시에 동일한 상품을 구매하려고 할 때, Optimistic Locking을 사용하여 충돌을 방지할 수 있습니다. 각 사용자는 상품을 구매하기 전에 해당 상품의 버전을 확인하고, 구매하는 동안 다른 사용자가 상품을 변경하지 않았는지 확인합니다.

public void purchaseProduct(Product product) {
    EntityManager entityManager = // EntityManager를 얻어옴
    entityManager.getTransaction().begin();
    
    // 상품의 현재 버전을 가져옴
    Product currentProduct = entityManager.find(Product.class, product.getId());
    
    // 현재 버전과 요청한 버전을 비교하여 충돌을 감지
    if (!currentProduct.getVersion().equals(product.getVersion())) {
        throw new OptimisticLockException("상품 정보가 최신이 아닙니다.");
    }
    
    // 상품을 구매하는 작업 수행
    product.purchase();
    
    // 커밋
    entityManager.getTransaction().commit();
}

이러한 예시에서 Pessimistic Locking은 트랜잭션을 동시에 실행하는 경우에도 각 트랜잭션을 순차적으로 실행되도록 보장합니다. 반면에 Optimistic Locking은 트랜잭션이 충돌할 경우, 충돌이 발생했음을 알리고 다시 시도하도록 합니다.

반응형
LIST