새소식

두부이모저모

면접 질문

  • -

Lazy Loading

Lazy Loading은 연관된 엔티티를 실제로 필요할 때까지 로드하지 않는 전략입니다. 이는 주로 연관된 엔티티가 많은 경우, 데이터베이스의 불필요한 로드를 방지하기 위해 사용됩니다.

작동 원리:

  • 연관된 엔티티는 처음에 프록시(proxy) 객체로 로드됩니다.
  • 프록시 객체는 실제 데이터에 접근하는 순간 데이터베이스를 조회하여 데이터를 로드합니다.

코드 예제:

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

    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
    private List<Child> children;
}

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

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

장점:

  1. 초기 로딩 성능: 초기 쿼리 시점에 불필요한 데이터 로드를 피하여 성능을 최적화할 수 있습니다.
  2. 메모리 효율성: 실제로 필요할 때만 데이터를 로드하여 메모리 사용량을 줄입니다.

단점:

  1. N+1 문제: 연관된 엔티티를 루프에서 접근할 때, 각 엔티티에 대해 추가 쿼리가 실행되어 성능이 크게 저하될 수 있습니다.
  2. 복잡성 증가: 데이터 접근 시점이 예측하기 어려워져 코드가 복잡해질 수 있습니다.

 

 

Eager Loading

Eager Loading은 연관된 엔티티를 즉시 로드하는 전략입니다. 엔티티를 조회할 때, 연관된 모든 엔티티도 함께 조회합니다.

작동 원리:

  • 엔티티가 로드될 때 연관된 모든 엔티티를 함께 조회합니다.
  • 이는 하나의 조인 쿼리로 구현됩니다.

코드 예제:

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

    @OneToMany(mappedBy = "parent", fetch = FetchType.EAGER)
    private List<Child> children;
}

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

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

장점:

  1. 간단한 코드: 데이터 접근 시점이 명확하고 예측 가능하여 코드가 단순해집니다.
  2. 쿼리 최적화: 연관된 엔티티를 한 번의 쿼리로 로드하여 N+1 문제를 방지할 수 있습니다.

단점:

  1. 초기 로딩 비용: 처음부터 모든 연관 데이터를 로드하기 때문에 초기 로딩 시간이 길어질 수 있습니다.
  2. 메모리 사용량: 불필요한 데이터를 로드하여 메모리 사용량이 증가할 수 있습니다.

 

 

 

N+1 문제란?

N+1 문제는 하나의 엔티티를 조회한 후, 연관된 N개의 엔티티를 각각 별도의 쿼리로 조회할 때 발생하는 성능 문제입니다. 예를 들어, 한 번의 쿼리로 부모 엔티티를 조회한 다음, 각 부모 엔티티에 대해 자식 엔티티를 개별 쿼리로 조회하는 경우를 생각해 보세요.

예시:

예를 들어, Parent와 Child라는 두 개의 엔티티가 있고, Parent 엔티티는 여러 Child 엔티티를 가질 수 있다고 가정합니다.

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

    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
    private List<Child> children;
}

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

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

다음과 같은 코드로 모든 부모와 그들의 자식을 로드하려고 한다고 가정해봅시다:

List<Parent> parents = entityManager.createQuery("SELECT p FROM Parent p", Parent.class).getResultList();

for (Parent parent : parents) {
    List<Child> children = parent.getChildren(); // 이 시점에서 Lazy Loading에 의해 데이터베이스 쿼리가 실행됨
}​

문제 발생:

1. 첫 번째 쿼리: 모든 Parent 엔티티를 로드하기 위해 실행됩니다.

SELECT * FROM Parent;

2. N개의 추가 쿼리: 각 Parent 엔티티의 children 필드를 접근할 때마다 실행됩니다.

SELECT * FROM Child WHERE parent_id = ?;

결과적으로, 부모 엔티티가 10개 있다면, 총 11개의 쿼리(1개의 부모 쿼리 + 10개의 자식 쿼리)가 실행됩니다. 이처럼 불필요하게 많은 쿼리가 실행되면서 성능 문제가 발생합니다. 이 현상이 바로 N+1 문제입니다.

 

N+1 문제 해결 방법

N+1 문제를 해결하기 위한 몇 가지 주요 방법을 소개하겠습니다.

1. Fetch Join 사용

JPQL에서 FETCH JOIN을 사용하여 연관된 엔티티를 한 번의 쿼리로 함께 로드할 수 있습니다.

List<Parent> parents = entityManager.createQuery(
    "SELECT p FROM Parent p JOIN FETCH p.children", Parent.class).getResultList();

이 쿼리는 부모와 자식을 한 번의 쿼리로 로드합니다.

SELECT p.*, c.* FROM Parent p JOIN Child c ON p.id = c.parent_id;
 

 

2. Entity Graph 사용

JPA 2.1 이상에서는 엔티티 그래프(Entity Graph)를 사용하여 특정 엔티티를 로드할 때 연관된 엔티티를 명시적으로 지정할 수 있습니다. 

EntityGraph<Parent> graph = entityManager.createEntityGraph(Parent.class);
graph.addAttributeNodes("children");
Map<String, Object> hints = new HashMap<>();
hints.put("javax.persistence.fetchgraph", graph);
List<Parent> parents = entityManager.createQuery("SELECT p FROM Parent p", Parent.class)
    .setHint("javax.persistence.fetchgraph", graph)
    .getResultList();

 

3. Batch Fetching 사용

Hibernate에서는 Batch Fetching을 사용하여 한 번에 여러 연관된 엔티티를 로드할 수 있습니다. 이는 한 번의 쿼리로 여러 부모의 자식들을 로드하여 쿼리 횟수를 줄입니다.

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

    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
    @BatchSize(size = 10)
    private List<Child> children;
}

Hibernate는 부모 엔티티를 조회한 후 배치 사이즈만큼 자식 엔티티를 한 번에 조회합니다.

 

4. Hibernate의 @Fetch Annotation 사용

Hibernate의 @Fetch(FetchMode.SUBSELECT) 어노테이션을 사용하여 서브셀렉트를 통한 배치 로드를 지원합니다.

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

    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
    @Fetch(FetchMode.SUBSELECT)
    private List<Child> children;
}

Hibernate는 첫 번째 쿼리에서 부모 엔티티를 로드하고, 두 번째 쿼리에서 서브셀렉트를 사용하여 연관된 자식 엔티티를 한 번에 로드합니다.

SELECT * FROM Parent;
SELECT * FROM Child WHERE parent_id IN (SELECT id FROM Parent);
 

결론

N+1 문제는 JPA를 사용할 때 발생할 수 있는 성능 문제 중 하나입니다. 이를 해결하기 위해 Fetch Join, Entity Graph, Batch Fetching, Hibernate의 @Fetch 어노테이션 등 다양한 방법을 사용할 수 있습니다. 각 방법의 장단점을 이해하고, 상황에 맞게 적절한 방법을 선택하는 것이 중요합니다.

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.