본문 바로가기

내배캠/TIL

JPA의 N+1 문제 해결하기

Repository를 만드는데 두 가지 방법이 있다.

 

 

1. 순수 JPA 기반 레포지토리

EntityManager를 직접 제어하며 커스텀한 데이터베이스 접근 로직 구현이 가능하다는 장점이 있지만

반복적이고 일반적인 데이터 접근 로직을 직접 구현하며 생산성이 저하된다는 단점이 있다.

 

@Repository
public class MemberJpaRepository {

    @PersistenceContext
    private final EntityManager entityManager;

    public MemberJpaRepository(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    /**
     * 회원 리스트 조회
     */
    public List<Member> findAll() {
        String jpql = "SELECT m FROM Member m";
        TypedQuery<Member> query = entityManager.createQuery(jpql, Member.class);
        return query.getResultList();
    }

    /**
     * 회원 저장
     * @param member 회원 엔티티
     * @return 저장된 엔티티
     */
    @Transactional
    public Member save(Member member) {
        entityManager.persist(member);
        return member;
    }
}

 

 

2. JPA 인터페이스 기반 레포지토리

대부분의 기본적인 데이터 접근 로직이 이미 구현되어 있기 때문에 생산성이 향상된다는 장점이 있다.

그러나 JPA의 기본 동작을 변경하거나 세밀한 제어가 어려워 유연성이 부족하다는 단점이 있다.

 

public interface CourseRepository extends JpaRepository<Course, Long> {}
public interface MemberRepository extends JpaRepository<Member, Long> {}

 

 

N + 1 문제 식별하기

 

N + 1 문제란?

하나의 쿼리를 수행한 후에 추가로 N개의 쿼리가 발생하는 상황을 말한다.

 

// 조회: 회원 엔티티 목록 조회
log.info("::: 멤버 목록 조회 :::");
List<Member> foundMemberList = memberRepository.findAll();


log.info("::: 멤버 DTO 변환 :::");
// DTO 변환: 회원 엔티티 목록 -> List<MemberDto> memberListDto
List<MemberDto> memberDtoList = foundMemberList.stream().map(member -> new MemberDto(
       member.getId(),
       member.getName(),
       member.getCourse().getName()
)).toList();

 

위의 코드에서 member가 getCourse().getName() 으로 접근하여 사용할 때 fetch type이 lazy라면 하나의 member row를 돌 때마다 course를 select하는 쿼리문이 나오게 된다. 만약 실제로 이런 상황이 일어난다면 데이터는 몇 만, 몇 십만 개가 되기 때문에 한 번의 쿼리가 몇 십만의 새로운 쿼리를 부를 수도 있다는 뜻이다. 이는 시스템 성능 저하 뿐만 아니라 잘못하면 데이터베이스 자체가 다운되어 버린다. 

 

이를 해결하기 위해선 Fecth Join을 사용하면 된다.

 

 

Fetch join

 

public interface MemberRepository extends JpaRepository<Member, Long> {


   @Query("SELECT m FROM Member m JOIN FETCH m.course")
   List<Member> findAllWithFetchJoin();
}

 

fetch join 기능을 사용하여 연관된 엔티티를 처음부터 함께 가져오도록 만든다. 이렇게 하면 추가적인 쿼리가 발생하지 않는다. 쿼리 실행 횟수가 줄어들면 데이터베이스와의 통신 횟수가 줄어들기 때문에 성능이 크게 개선된다. 특히 많은 양의 데이터를 다루는 경우 성능차이가 더욱 크게 나타난다. 

 

 

'내배캠 > TIL' 카테고리의 다른 글

[스프링] AOP  (0) 2024.09.10
카카오 로그인 개발하기  (5) 2024.09.09
데이터 베이스 정규화  (1) 2024.09.05
JWT, Filter  (0) 2024.09.04
RDBMS vs NoSQL  (0) 2024.09.03