BackEND/Java

JPA의 기본 조회 성능 최적화 기법

mingmingIT 2025. 4. 2. 10:35

JPA를 사용할 때 성능을 최적화하는 것이 중요합니다. 잘못된 설정이나 무분별한 쿼리 실행은 애플리케이션의 속도를 저하시킬 수 있습니다. 이번 포스팅에서는 JPA의 기본적인 조회 성능 최적화 기법을 소개합니다.

1. FetchType 설정 (EAGER vs LAZY)

JPA에서는 연관된 엔티티를 조회할 때 FetchType을 설정할 수 있습니다.

1) 즉시 로딩 (EAGER)

  • 연관된 엔티티를 즉시 조회
  • 필요하지 않은 데이터를 불필요하게 로딩하여 성능 저하 가능
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;
    
    @ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩
    private Team team;
}

2) 지연 로딩 (LAZY, 기본값 추천)

  • 연관된 엔티티를 실제 사용할 때 조회 (프록시 객체 사용)
  • 필요할 때만 데이터를 가져오므로 성능 최적화 가능
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;
    
    @ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
    private Team team;
}

💡 성능 최적화를 위해 가능하면 LAZY 로딩을 기본으로 설정하는 것이 좋습니다.


2. N+1 문제 해결 (Fetch Join & EntityGraph 활용)

1) N+1 문제란?

연관된 엔티티를 조회할 때 추가적인 SELECT 쿼리가 반복적으로 실행되는 문제입니다.

List<Member> members = em.createQuery("SELECT m FROM Member m", Member.class).getResultList();
for (Member member : members) {
    System.out.println(member.getTeam().getName()); // Team 조회 시 추가 쿼리 발생 (N번 실행)
}

2) Fetch Join을 활용한 최적화

JOIN FETCH를 사용하여 한 번의 쿼리로 연관된 데이터를 가져올 수 있습니다.

List<Member> members = em.createQuery(
    "SELECT m FROM Member m JOIN FETCH m.team", Member.class).getResultList();

 

💡 Fetch Join을 사용하면 불필요한 추가 쿼리를 방지할 수 있습니다.

3) @EntityGraph 활용

@EntityGraph를 사용하면 Fetch Join과 유사한 효과를 낼 수 있습니다.

@EntityGraph(attributePaths = {"team"})
@Query("SELECT m FROM Member m")
List<Member> findAllWithTeam();

💡 @EntityGraph는 JPQL을 직접 작성하지 않고도 Fetch Join 효과를 낼 수 있는 강력한 방법입니다.


3. Batch Size 설정을 통한 최적화

컬렉션을 조회할 때 @BatchSize를 설정하면 IN 절을 활용하여 한 번에 데이터를 가져올 수 있습니다.

@Entity
public class Team {
    @OneToMany(mappedBy = "team")
    @BatchSize(size = 10)
    private List<Member> members;
}

또는 글로벌 설정으로 적용할 수도 있습니다.

spring.jpa.properties.hibernate.default_batch_fetch_size=100

💡 @BatchSize 설정을 활용하면 N+1 문제를 해결할 수 있습니다.


4. JPQL 대신 Criteria API 또는 QueryDSL 활용

JPQL은 문자열 기반이라 동적 쿼리 작성이 어렵습니다. 이를 해결하기 위해 QueryDSL을 사용할 수 있습니다.

QMember member = QMember.member;
List<Member> members = queryFactory
    .selectFrom(member)
    .where(member.age.gt(20))
    .fetch();

💡 QueryDSL을 사용하면 성능 최적화뿐만 아니라 가독성과 유지보수성도 향상됩니다.


5. 인덱스 및 페이징 최적화

1) 인덱스 활용

  • 조회 성능을 높이기 위해 @Index를 추가할 수 있습니다.
@Entity
@Table(indexes = {@Index(name = "idx_member_name", columnList = "name")})
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;
}

2) 페이징 시 최적화

setFirstResult()setMaxResults()를 활용하면 페이징 쿼리를 최적화할 수 있습니다.

TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m ORDER BY m.id", Member.class);
query.setFirstResult(0); // 시작 위치
query.setMaxResults(10); // 최대 조회 개수
List<Member> members = query.getResultList();

💡 인덱스와 페이징을 함께 활용하면 대량 데이터 조회 시 성능을 크게 개선할 수 있습니다.


결론

JPA를 활용할 때 성능 최적화를 위해 다음과 같은 전략을 사용할 수 있습니다.

FetchType.LAZY 사용하여 불필요한 데이터 로딩 방지
Fetch Join과 @EntityGraph 활용하여 N+1 문제 해결
Batch Size 설정을 통해 다중 조회 최적화
QueryDSL 활용하여 가독성과 성능 개선
인덱스와 페이징 최적화를 통해 대량 데이터 조회 개선