Chapter 6. JPA 활용: 즉시 로딩, N+1 문제, 영속성 컨텍스트, JPQL, QueryDSL

김지민·2024년 12월 26일

UMC springboot

목록 보기
5/9

이번 시간에는 저번 포스팅에서 배운 JPA를 더 자세히 배워보는 시간을 갖겠습니다!


🌟 학습 목표

  1. 즉시 로딩과 지연 로딩의 전략 차이를 이해하고, 지연 로딩을 추천하는 이유를 살펴본다.
  2. JPQLQueryDSL의 차이를 파악하고, 실제 프로젝트에서의 활용 방법을 익힌다.
  3. JPA 사용 방법 및 반복 작업을 줄이는 JPA의 장점을 이해한다.

1) Spring Data JPA 영속성 컨텍스트

영속성 컨텍스트(Persistence Context)란?

영속성 컨텍스트는 JPA가 엔티티 객체를 관리하고 데이터베이스와 연동하는 가상의 메모리 공간입니다. ORM, 즉 객체와 RDB 데이터를 자동으로 매핑해주는 기능을 제공하는 JPA에서 아주 핵심적인 녀석이죠.

일종의 캐시 메모리라고 생각하시면 됩니다.

먼저 최초로 엔티티에 접근하면, DB에서 끌어온 엔티티를 영속성 컨텍스트에 등록해줍니다.
이후에 Spring Data JPA에서 Repository 인터페이스 를 통하여 같은 엔티티에 접근하면, DB에 쿼리를 날리는 게 아닌 영속성 컨텍스트에 있는 데이터를 조회하게 되는 거죠.
JPA에서는 테이블과 매핑해주는 엔티티 객체를 영속성 컨텍스트를 통해 애플리케이션 내부에서 오래 지속되도록 돕습니다.
JPA를 통해 데이터베이스에서 조회한 엔티티는 영속성 컨텍스트에 저장되고, 그 안에서 문맥적(contextual)으로 하나의 상태를 유지하면서, 여러 기능을 할 수 있게 하죠.
다시 말해, 애플리케이션과 데이터베이스 사이에서 데이터를 저장하는 가상의 데이터베이스 같은 역할을 합니다.

@EntityManager을 이용하여 접근합니다 ‼️

영속성 컨텍스트는 이처럼 데이터베이스 요청을 최소화해 성능을 높이고, 엔티티 변경 사항을 감지하는 등 다양한 이점을 제공합니다.

영속성 컨텍스트의 주요 장점

객체가 ‘영속’ 상태이면, 다음과 같이 크게 3가지 장점을 지닙니다.

  • 1차 캐시
  • 변경 감지(Dirty Checking)
  • 지연 로딩(FetchType.LAZY)
기능설명예시
1차 캐시동일한 엔티티를 조회할 때, DB 쿼리 생략 후 캐시 데이터를 반환합니다.em.find(Member.class, 1L); 캐시에 저장된 데이터 반환
변경 감지(Dirty Checking)엔티티 변경 사항을 감지하고 트랜잭션 종료 시 자동으로 DB에 반영합니다.member.setName("New Name");UPDATE member 쿼리 실행
지연 로딩연관 데이터를 필요할 때만, 분리하여 DB가 아닌 프록시에서 데이터를 가져옵니다‼️FetchType.LAZY 설정 시, 연관 데이터 접근 시점에 쿼리 실행

즉시 로딩(FetchType.EAGER) VS 지연 로딩(FetchType.LAZY)

즉시 로딩(FetchType.EAGER)

즉시 로딩은 연관된 모든 데이터를 한 번에 가져오는 로딩 전략입니다.

  • 사용 방법: @ManyToOne(fetch = FetchType.EAGER)
  • 장점: 필요한 데이터를 미리 로딩해, 추가적인 쿼리 없이 사용할 수 있습니다.
  • 단점: 연관된 데이터가 많을 경우 N+1 문제가 발생할 가능성이 높습니다.

지연 로딩(FetchType.LAZY)

지연 로딩은 실제로 데이터가 필요할 때 DB에서 가져오는 로딩 전략입니다.

  • 사용 방법: @ManyToOne(fetch = FetchType.LAZY)
  • 장점: 초기 쿼리 수를 최소화해 성능 최적화를 이룰 수 있습니다.
  • 단점: 데이터 접근 시마다 추가적인 쿼리가 발생할 수 있어 주의가 필요합니다.
    🌟 지연 로딩은 DB가 아닌 프록시에서 데이터를 가져옵니다‼️

지연 로딩은 MemberPreferMember, FoodCategory이 각각 분리되어 MemberPrefer은 DB를, MemberFoodCategory 은 프록시를 조회합니다.

@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class MemberPrefer extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    **@ManyToOne(fetch = FetchType.LAZY)**
    @JoinColumn(name = "member_id")
    private Member member;

    **@ManyToOne(fetch = FetchType.LAZY)**
    @JoinColumn(name = "category_id")
    private FoodCategory foodCategory;

}

즉, MemberPrefer을 조회할 때, Member과 FoodCategory 를 굳이 조회할 필요가 없다면 분리하여 조회하는 지연 로딩 방법을 사용합시다👍

즉시 로딩과 지연 로딩의 비교

로딩 전략특징장점단점
즉시 로딩(FetchType.EAGER)연관된 데이터를 한 번에 가져옴연관된 데이터 접근 시 추가 쿼리가 발생하지 않음불필요한 데이터 로딩, N+1 문제 발생 가능
지연 로딩(FetchType.LAZY)데이터 접근 시점에 DB에서 가져옴초기 로딩 속도가 빠르고, 필요 시점에만 데이터를 로딩데이터 접근 시마다 추가 쿼리 발생

N+1 문제란?

즉시 로딩에서 발생할 수 있다는 N+1 문제는 뭘까요?

문제 정의

N+1 문제는 하나의 메인 쿼리로 N개의 데이터를 가져온 뒤, 각 데이터에 대해 N개의 추가 쿼리가 실행되는 문제입니다.
RDB와 객체 지향의 패러다임 간극에 의해 발생하는 문제인데, 1개의 쿼리를 실행한 후에, 관련된 N개의 데이터를 각각 가져오기 위해 추가적으로 N번의 불필요한 쿼리가 실행되어 성능 저하가 발생합니다.

해결 방법

방법설명장점단점
Fetch Join연관 데이터를 한 번의 쿼리로 로딩N+1 문제 해결 가능, 성능 최적화복잡한 쿼리에선 성능 저하 가능
Batch Size지연 로딩 시 여러 데이터를 한 번에 로딩추가 쿼리 수를 줄여 N+1 문제 해결설정에 따라 불필요한 데이터 로딩 가능
@EntityGraph특정 필드만 즉시 로딩으로 변경간결한 설정, 필요 시점에만 즉시 로딩 가능JPQL 사용 시 비효율적
QueryDSL동적 쿼리를 통해 필요한 데이터만 로딩타입 안전성 보장, 동적 쿼리 작성 가능설정이 다소 복잡

2) JPQL: 객체 지향 쿼리 언어

JPQL이란?

JPQL(Java Persistence Query Language)은 JPA 엔티티 객체를 대상으로 작성하는 쿼리 언어입니다.

  • SQL과 유사하지만, 데이터베이스 테이블이 아닌 엔티티 객체를 사용하여 쿼리를 작성합니다.
  • 즉, JPQL은 데이터베이스와 상호작용한다는 기본 컨셉은 유지하되, 객체 지향적인 특성을 반영하여 작성할 수 있는 것이 가장 큰 특징입니다.

JPQL 사용 방법

JPQL을 사용하는 방법에는 두 가지가 있습니다:

[1] EntityManager 인터페이스
[2] Spring Data JPA의 Repository 인터페이스

[1] EntityManager 인터페이스를 통한 JPQL 사용

EntityManager란?

  • JPA의 핵심 인터페이스로, 엔티티를 관리하고 JPQL 쿼리를 실행하는 데 사용됩니다.
  • Native SQL과 병행 사용이 가능하며, 트랜잭션 관리와 엔티티 작업 관리에 탁월합니다.
기본 사용 예시
// EntityManager 생성 및 트랜잭션 시작
EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();

// JPQL 쿼리 작성
String jpql = "SELECT m FROM Member m WHERE m.name = :name";
TypedQuery<Member> query = em.createQuery(jpql, Member.class);
query.setParameter("name", "Alice");

// 결과 리스트로 받기
List<Member> members = query.getResultList();
for (Member member : members) {
    System.out.println("Member Name: " + member.getName());
}

// 트랜잭션 종료
em.getTransaction().commit();
em.close();
주요 메서드
  • createQuery(String jpql, Class<T> resultClass): JPQL 쿼리를 작성하고 실행합니다.
  • setParameter(String name, Object value): JPQL 쿼리에서 사용할 파라미터를 설정합니다.
  • getResultList(): 결과를 리스트로 반환합니다.
  • getSingleResult(): 단일 결과를 반환합니다.
업데이트/삭제 쿼리 예시

JPQL을 사용하면 업데이트나 삭제 작업도 가능합니다.

em.getTransaction().begin();

// JPQL 업데이트 쿼리
String jpql = "UPDATE Member m SET m.age = :age WHERE m.name = :name";
Query query = em.createQuery(jpql);
query.setParameter("age", 30);
query.setParameter("name", "Alice");

int rowsUpdated = query.executeUpdate(); // 업데이트된 행 수 반환
System.out.println("Rows updated: " + rowsUpdated);

em.getTransaction().commit();

[2] Repository 인터페이스를 통한 JPQL 사용

Spring Data JPA는 Repository 인터페이스를 통해 JPQL 쿼리를 간편하게 작성할 수 있습니다.
이 방법은 더 직관적이고 간결하며, Spring 환경과 잘 통합됩니다.

메서드 이름 기반 쿼리 생성

Spring Data JPA는 메서드 이름을 기반으로 JPQL 쿼리를 자동 생성합니다.

  • 예시: 특정 조건에 맞는 회원을 조회하는 메서드
public interface MemberRepository extends JpaRepository<Member, Long> {
    List<Member> findByNameAndStatus(String name, MemberStatus status);
}

// 호출
List<Member> activeMembers = memberRepository.findByNameAndStatus("Alice", MemberStatus.ACTIVE);

Spring 내부 동작:
위 메서드 호출은 다음 JPQL 쿼리로 변환됩니다.

SELECT m FROM Member m WHERE m.name = 'Alice' AND m.status = 'ACTIVE';
@Query 어노테이션을 통한 JPQL 작성

더 복잡한 쿼리가 필요할 경우, @Query 어노테이션을 사용해 직접 JPQL을 작성할 수 있습니다.

public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query("SELECT m FROM Member m WHERE m.name = :name AND m.status = :status")
    List<Member> findByNameAndStatus(@Param("name") String name, @Param("status") MemberStatus status);
}

장점

  • 간결한 코드로 복잡한 JPQL 작성 가능.
  • 메서드 이름 기반 쿼리 생성보다 더 높은 유연성 제공.

JPQL과 SQL의 차이점

구분JPQLSQL
대상엔티티 객체를 대상으로 작성테이블을 대상으로 작성
언어객체 지향적 문법 반영관계형 데이터베이스 중심의 문법
예시SELECT m FROM Member mSELECT * FROM member
사용 환경JPA 환경에서 사용JPA 외에 모든 SQL 환경에서 사용 가능

JPQL 사용 시 주의사항

  1. 엔티티 필드 이름을 사용

    • JPQL은 엔티티의 필드 이름을 사용하므로, 테이블 컬럼 이름이 아니라 엔티티의 필드 이름과 일치해야 합니다.
    • 예: m.name은 엔티티의 name 필드를 참조.
  2. Lazy Loading과의 연관성

    • 지연 로딩이 설정된 엔티티를 JPQL로 조회할 경우, 필요 시점에 추가 쿼리가 발생할 수 있습니다.
  3. 동적 쿼리 작성

    • 복잡한 조건을 가진 동적 쿼리를 작성해야 할 경우, QueryDSL과 같은 도구를 사용하는 것이 더 효율적일 수 있습니다.

3) QueryDSL: 동적 쿼리를 위한 강력한 도구

JPQL 사용으로는 작성하기 너무 복잡한 동적 쿼리를 위해서는 QueryDSL를 사용하는 게 효과적인 방법입니다.

QueryDSL이란?

QueryDSL은 코드 기반의 쿼리 빌더 라이브러리로, 타입 안전성동적 쿼리 작성에 강점을 가진 도구입니다.

  • 타입 안전성 보장: 컴파일 시점에 쿼리 오류를 잡을 수 있어 안전합니다.
  • 동적 쿼리 작성: 조건이나 구조가 실행 시점에 유동적으로 변경될 수 있는 쿼리를 쉽게 작성할 수 있습니다.
  • 메서드 체이닝: 조건과 동작을 체계적으로 연결해 직관적이고 간결한 쿼리 작성이 가능합니다.

QueryDSL 사용법

1️⃣ QueryDSL 기본 설정

QueryDSL을 사용하려면 Q 클래스를 자동 생성해야 합니다. 이를 위해 build.gradle 파일에 다음 설정을 추가합니다.

plugins {
    id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}

dependencies {
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
}
sourceSets {
    main.java.srcDir 'src/main/generated'
}

2️⃣ Q 클래스 생성

Q 클래스는 각 엔티티를 기준으로 자동 생성됩니다.
src/main/generated 폴더에 생성된 QMember 또는 QStore 등을 사용하여 QueryDSL로 쿼리를 작성합니다.

3️⃣ JPAQueryFactory 설정

QueryDSL은 JPAQueryFactory를 사용해 쿼리를 작성합니다.
JPAQueryFactory를 Bean으로 등록하려면 다음과 같은 설정이 필요합니다.

@Configuration
@RequiredArgsConstructor
public class QueryDSLConfig {
    private final EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

4️⃣ QueryDSL로 동적 쿼리 작성

BooleanBuilder 활용

BooleanBuilder를 사용해 조건을 동적으로 추가할 수 있습니다.

private List<Store> dynamicQueryWithBooleanBuilder(String name, Float score) {
    BooleanBuilder predicate = new BooleanBuilder();

    if (name != null) {
        predicate.and(store.name.eq(name));
    }
    if (score != null) {
        predicate.and(store.score.goe(score));
    }

    return jpaQueryFactory
        .selectFrom(store)
        .where(predicate)
        .fetch();
}
  • .and(): 조건 추가
  • .or(): 조건 선택적으로 추가
Repository 인터페이스 구성
  1. Custom Interface 생성

    public interface StoreRepositoryCustom {
        List<Store> dynamicQueryWithBooleanBuilder(String name, Float score);
    }
  2. Custom 구현체 작성

    @Repository
    @RequiredArgsConstructor
    public class StoreRepositoryImpl implements StoreRepositoryCustom {
        private final JPAQueryFactory jpaQueryFactory;
        private final QStore store = QStore.store;
    
        @Override
        public List<Store> dynamicQueryWithBooleanBuilder(String name, Float score) {
            BooleanBuilder predicate = new BooleanBuilder();
    
            if (name != null) predicate.and(store.name.eq(name));
            if (score != null) predicate.and(store.score.goe(score));
    
            return jpaQueryFactory
                .selectFrom(store)
                .where(predicate)
                .fetch();
        }
    }
  3. Repository 연결

    public interface StoreRepository extends JpaRepository<Store, Long>, StoreRepositoryCustom {
    }
  4. Service Layer에서 호출

    @Service
    @RequiredArgsConstructor
    public class StoreQueryServiceImpl implements StoreQueryService {
        private final StoreRepository storeRepository;
    
        public List<Store> findStoresByNameAndScore(String name, Float score) {
            return storeRepository.dynamicQueryWithBooleanBuilder(name, score);
        }
    }

QueryDSL의 장단점

장점단점
타입 안전성 보장초기 설정 및 학습 곡선 필요
동적 쿼리 작성 간편QueryDSL 라이브러리 의존성 추가 필요
메서드 체이닝으로 직관적 쿼리 작성 가능설정 과정이 다소 복잡할 수 있음

실습을 통한 QueryDSL 요약

  1. QueryDSL 설정

    • build.gradle에 종속성 추가
    • Q 클래스 자동 생성 설정
  2. JPAQueryFactory Bean 등록

    • EntityManager와 연동
  3. Repository 구성

    • Custom InterfaceCustom 구현체 작성
    • 동적 쿼리를 작성해 비즈니스 로직 적용
  4. Service Layer에서 활용

    • 동적 조건에 따라 유연하게 데이터를 조회하고 비즈니스 로직에 적용

정리

  1. 즉시 로딩(FetchType.EAGER)은 N+1 문제를 일으킬 수 있으므로, 지연 로딩(FetchType.LAZY)을 기본으로 설정.
  2. JPQL은 객체 지향 쿼리 언어로 SQL보다 객체 매핑에 적합.
  3. QueryDSL은 동적 쿼리를 작성할 때 강력한 도구로 활용 가능.
profile
열혈개발자~!!

0개의 댓글