JPA에서 N+1 문제

2024. 7. 19. 16:22·개발 노트/Spring

N+1 문제란?

N+1 문제는 JPA나 ORM을 사용할 때 발생할 수 있는 성능 문제 중 하나다. 간단히 말해, 하나의 쿼리를 실행한 후 관련된 N개의 항목에 대해 추가로 N개의 쿼리가 실행되는 상황을 말한다. 예를 들어, 한 번의 조회 쿼리로 여러 개의 부모 엔티티를 가져온 후, 각 부모 엔티티와 관련된 자식 엔티티를 조회하기 위해 N번의 추가 쿼리가 실행되는 경우가 이에 해당한다.

 

N+1 문제 예시

Entity Class

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

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

    // getters and setters
}

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

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

    // getters and setters
}

Repository Class

public interface ParentRepository extends JpaRepository<Parent, Long> {}

Service Class

@Service
public class ParentService {
    private final ParentRepository parentRepository;

    @Autowired
    public ParentService(ParentRepository parentRepository) {
        this.parentRepository = parentRepository;
    }

    public List<Parent> getAllParents() {
        return parentRepository.findAll();
    }
}

위 코드에서 getAllParents() 메소드를 호출하면 부모 엔티티를 조회하는 쿼리 하나와 각 부모 엔티티에 대한 자식 엔티티를 조회하는 쿼리가 N번 실행된다.

 

N+1 문제 해결 방법

1. 페치 조인(Fetch Join)

가장 일반적인 해결 방법 중 하나는 페치 조인을 사용하는 것이다. 페치 조인은 JPQL에서 join fetch를 사용하여 한 번의 쿼리로 연관된 엔티티들을 함께 조회하는 방법이다.

Repository Class

public interface ParentRepository extends JpaRepository<Parent, Long> {
    @Query("SELECT p FROM Parent p JOIN FETCH p.children")
    List<Parent> findAllWithChildren();
}

이렇게 하면 findAllWithChildren() 메소드를 호출할 때 부모와 자식 엔티티를 한 번의 쿼리로 가져올 수 있다.

예시 코드

public interface ParentRepository extends JpaRepository<Parent, Long> {
    @EntityGraph(attributePaths = {"children"})
    List<Parent> findAll();
}

@EntityGraph를 사용하여 페치 조인을 설정할 수도 있다. 이는 쿼리 메소드와 함께 사용할 수 있으며, JPQL을 직접 작성하지 않고도 연관된 엔티티를 함께 조회할 수 있다.

2. 엔티티 그래프(Entity Graph)

엔티티 그래프는 JPA 2.1부터 도입된 기능으로, 페치 타입을 명시적으로 지정하지 않고도 연관된 엔티티를 함께 로딩할 수 있는 방법이다.

Entity Class

@Entity
@NamedEntityGraph(name = "Parent.children", attributeNodes = @NamedAttributeNode("children"))
public class Parent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

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

    // getters and setters
}

Repository Class

public interface ParentRepository extends JpaRepository<Parent, Long> {
    @EntityGraph(value = "Parent.children")
    List<Parent> findAll();
}

3. Batch Size 설정

Batch Size 설정을 통해 N+1 문제를 해결할 수 있다. 이 방법은 Hibernate의 특정 기능을 사용하여 한 번에 여러 개의 엔티티를 로딩하는 방법이다.

application.yml 설정

spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 1000

이렇게 설정하면 Hibernate는 한 번에 1000개의 엔티티를 로딩하여 N+1 문제를 줄일 수 있다.

4. 서브 셀렉트(Subselect)

서브 셀렉트는 부모 엔티티를 로드할 때 한 번의 쿼리로 자식 엔티티를 로드하는 방법이다.

Entity Class

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

    @OneToMany(mappedBy = "parent")
    @BatchSize(size = 10)
    @Fetch(FetchMode.SUBSELECT)
    private List<Child> children;

    // getters and setters
}

이 방법은 많은 부모 엔티티와 관련된 자식 엔티티를 로딩할 때 유용하다.

 

Batch Size 설정 시 고려 사항

Batch Size 설정은 매우 유용하지만, 대규모 데이터를 로드할 때 메모리 사용량이 증가할 수 있는 점을 고려해야 한다. 설정된 Batch Size만큼 한꺼번에 데이터를 로드하므로, 너무 큰 값으로 설정하면 메모리 사용량이 급격히 증가할 수 있다. 적절한 Batch Size 값을 설정하여 성능과 메모리 사용 간의 균형을 맞추는 것이 중요하다.

경험담

이전 프로젝트에서 Batch Size 설정을 통해 N+1 문제를 해결한 경험이 있다. 당시 관리자 대시보드 조회가 처음에는 9초에서 11초 정도로 너무 느렸다. 대시보드뿐만 아니라 다른 조회 부분도 상당한 시간이 걸렸다. 이 문제를 해결하기 위해 default_batch_fetch_size 값을 1000으로 설정했다. 설정만으로도 쉽게 N+1 문제를 해결할 수 있었고, 조회 성능이 크게 향상되어 이제는 조회가 1초 만에 이루어지게 되었다. 메모리 이슈에 대해서는 미처 고려하지 않았지만, 이후에 이 설정이 메모리 사용에 영향을 미칠 수 있다는 점을 알게 되었다.

프로젝트에 설정한 application.yml 파일

예시 코드

Service Class

@Service
public class ParentService {
    private final ParentRepository parentRepository;

    @Autowired
    public ParentService(ParentRepository parentRepository) {
        this.parentRepository = parentRepository;
    }

    @Transactional(readOnly = true)
    public List<Parent> getAllParents() {
        return parentRepository.findAll();
    }
}

성능 최적화를 위한 권장 사항

  1. 적절한 Batch Size 설정: Batch Size를 너무 작게 설정하면 N+1 문제를 완전히 해결하지 못하고, 너무 크게 설정하면 메모리 사용량이 증가할 수 있다. 테스트를 통해 적절한 값을 찾는 것이 중요하다.
  2. 페치 조인과의 조합: 페치 조인과 Batch Size 설정을 함께 사용하여 성능을 최적화할 수 있다.
  3. 엔티티 그래프 사용: 복잡한 연관 관계가 있을 경우 엔티티 그래프를 사용하여 명시적으로 필요한 데이터를 로드할 수 있다.

 

각 방법의 이슈

1. 페치 조인(Fetch Join)

페치 조인을 사용할 때의 주의 사항은 다음과 같다:

  • 쿼리 복잡성: 여러 연관 관계를 함께 조회하려면 쿼리가 복잡해질 수 있다.
  • 페치 조인 제한: Hibernate는 페치 조인의 개수를 제한하는 경우가 있다. 너무 많은 페치 조인을 사용할 경우 예외가 발생할 수 있다.

2. 엔티티 그래프(Entity Graph)

엔티티 그래프를 사용할 때의 주의 사항은 다음과 같다:

  • JPA 2.1 이상 필요: 엔티티 그래프는 JPA 2.1부터 지원되므로, 해당 버전 이상의 JPA를 사용해야 한다.
  • 복잡한 그래프 관리: 복잡한 엔티티 관계를 모두 그래프로 정의하려면 관리가 어려울 수 있다.

3. Batch Size 설정

Batch Size 설정을 사용할 때의 주의 사항은 다음과 같다:

  • 메모리 사용량 증가: Batch Size가 클수록 메모리 사용량이 증가할 수 있다. 적절한 값을 설정하는 것이 중요하다.
  • 일괄 처리 성능: Batch Size가 작으면 일괄 처리 성능이 저하될 수 있다.

4. 조회 쿼리 직접 작성

N+1 문제를 해결하기 위해 직접 쿼리를 작성하여 필요한 데이터를 한 번에 조회하는 방법도 있다. 이 방법은 JPQL이나 네이티브 SQL을 사용하여 복잡한 쿼리를 직접 작성하는 것을 의미한다.

Repository Class

public interface ParentRepository extends JpaRepository<Parent, Long> {
    @Query("SELECT p FROM Parent p LEFT JOIN FETCH p.children WHERE p.id = :id")
    Parent findByIdWithChildren(@Param("id") Long id);
}

 

마무리

JPA를 사용하면서 발생할 수 있는 N+1 문제는 성능 저하의 주요 원인 중 하나다. 이를 해결하기 위해 다양한 방법들이 있지만, 각 방법은 상황에 따라 적절히 선택하여 사용해야 한다. 개인적으로 프로젝트에서 Batch Size 설정을 통해 N+1 문제를 해결했는데, 이는 설정만으로도 쉽게 해결할 수 있어 매우 유용했다. 다만, Batch Size 설정 시 메모리 사용량이 증가할 수 있다는 점을 유의해야 하는 것을 알았다.

'개발 노트 > Spring' 카테고리의 다른 글

JPA에서 트랜잭션 처리 및 데이터베이스 격리 수준  (0) 2024.07.19
Spring Data JPA 이해하기: 주요 구성 요소와 어노테이션  (0) 2024.07.19
JPA 이해하기: 동작 원리와 핵심 구성 요소  (0) 2024.07.19
'개발 노트/Spring' 카테고리의 다른 글
  • JPA에서 트랜잭션 처리 및 데이터베이스 격리 수준
  • Spring Data JPA 이해하기: 주요 구성 요소와 어노테이션
  • JPA 이해하기: 동작 원리와 핵심 구성 요소
악덕
악덕
우당탕탕 개발 블로그
  • 악덕
    버그와 함께 춤을
    악덕
  • 전체
    오늘
    어제
    • 전체 (23)
      • TIL (0)
      • 개발 노트 (18)
        • Java (5)
        • JavaScript (1)
        • Spring (4)
        • Linux (1)
        • etc. (7)
      • 문제 풀이 (0)
      • 삽질 로그 (3)
      • 기타 (2)
  • 링크

    • GitHub
    • project.zip
    • 밀로(millo)
  • 태그

    macos linux
    invalid character found in the request target
    프로그래밍언어
    macos ubuntu
    i/o extended
    gdg
    프로그래밍
    springboot
    웹개발
    Spring Data JPA
    자료형
    ports and adapters
    JPA
    solid 원칙
    java
    객체지향 프로그래밍
    자바기초
    OOP
    java persistence api
    ssl
  • hELLO· Designed By정상우.v4.10.3
악덕
JPA에서 N+1 문제
상단으로

티스토리툴바