Today I Learned

241216 TIL / JPA와 QueryDSL

shinelee26 2024. 12. 16. 22:17

오늘 진행한 학습 요약

1. JPA 심화 5주차 ( SpringData JPA 기본 )

  • 좀 더 멋지게 쿼리 생성하기 (QueryDSL)
  • 테이블 객체 방명록 설정하기 (Auditing)
  • 필요한 부분만 갱신하기 (Dynamic Insert/Update)
  • 필요한 부분만 효율적으로 조회하기 (Projection)

2. JPA QueryDSL 사용하는 법

  • QueryDSL 사용법
  • QueryDSL의 장점
  • QueryDSL의 단점
  • QueryDSL을 사용할 때 적합한 상황
  • 주요 메서드
  • 실제 사용해보기

학습 정리

1. JPA 심화 5주차 ( SpringData JPA 기본 )

 

좀 더 멋지게 쿼리 생성하기 (QueryDSL)

  • QueryDSL 사용법
    • QueryDSL이란?
      • 보통 데이터베이스에서 데이터를 가져올 때 SQL 같은 쿼리를 사용합니다. 하지만 SQL은 문자열로 작성되다 보니 오타가 나거나 실행 중에만 오류가 확인되는 문제가 있습니다.
      • QueryDSL은 SQL 대신 자바 객체를 이용해 쿼리를 작성할 수 있도록 도와주는 기술입니다.
      • Q클래스: 엔티티 정보를 이용해 쿼리를 쉽게 작성할 수 있도록 만든 전용 클래스.
    • 왜 사용하나요?
      • SQL처럼 복잡한 쿼리를 작성할 수 있으면서도, 문법 오류를 컴파일 시점에 잡을 수 있습니다.
      • 읽기 쉽고 유지보수가 편리합니다.
    • 적용 방법
      • 설정 파일 작성:
        • JPAQueryFactory를 Bean으로 등록하여 사용할 준비를 합니다.
        • @Configuration
          public class JPAConfiguration {
              @PersistenceContext
              private EntityManager entityManager;
          
              @Bean
              public JPAQueryFactory jpaQueryFactory() {
                  return new JPAQueryFactory(entityManager);
              }
          }
      • Q클래스 활용
        • 생성된 Q클래스를 사용하여 데이터를 조회합니다.
        • JPAQueryFactory jqf = new JPAQueryFactory(em);
          QUser user = QUser.user;
          
          List<User> userList = jqf.selectFrom(user)
                                   .where(user.username.eq("name"))
                                   .fetch();
    • 활용 예시
      • Slack 멘션된 쓰레드 조회
          • 특정 사용자가 멘션된 채팅 데이터를 가져오고 싶을 때.
          • 가져올 데이터: 채널 이름, 쓰레드 작성자, 본문, 댓글, 이모지 등.
          • 정렬 기준: 멘션된 시간 순서대로 정렬.

 

테이블 객체 방명록 설정하기 (Auditing)

  • Auditing이란?
    • 데이터를 다룰 때 언제, 누가 데이터를 만들었고 수정했는지를 자동으로 기록하는 기능입니다.
    • 마치 방명록처럼 누가 다녀갔는지 기록을 남길 수 있습니다.
  • 주요 기능
    1. 시간 기록: 데이터가 생성된 시점과 수정된 시점을 자동으로 기록.
      • @CreatedDate: 생성 시간.
      • @LastModifiedDate: 수정 시간.
    2. 사용자 기록: 데이터를 생성/수정한 사람을 자동으로 기록.
      • @CreatedBy: 데이터를 만든 사람.
      • @LastModifiedBy: 데이터를 수정한 사람.
  • 적용 방법
    1. 프로젝트에 @EnableJpaAuditing를 추가하여 Auditing 기능 활성화.
    2. 엔티티 클래스에 @EntityListeners(AuditingEntityListener.class) 추가.
    3. 인증된 사용자 정보를 반환하는 AuditorAware를 구현
      1. @Service public class UserAuditorAware implements AuditorAware<User> { @Override public Optional<User> getCurrentAuditor() { return Optional.of(authenticatedUser); } }

 

필요한 부분만 갱신하기 (Dynamic Insert/Update)

  • Dynamic Insert
    • 보통 데이터를 데이터베이스에 추가할 때, 모든 필드를 포함해서 Insert 쿼리가 만들어집니다.
    • 하지만 Dynamic Insert를 사용하면, null인 값은 제외하고 필요한 데이터만 추가할 수 있습니다.
    • 효과: 불필요한 데이터 처리를 줄여 성능을 높입니다.
    • 적용 방법: 엔티티 클래스에 @DynamicInsert 어노테이션 추가.
  • Dynamic Update
    • 데이터를 수정할 때도, 모든 필드를 업데이트하는 대신 변경된 데이터만 처리합니다.
    • 효과: 수정이 필요한 데이터만 업데이트해 성능을 최적화.
    • 적용 방법: 엔티티 클래스에 @DynamicUpdate 어노테이션 추가.
  • 적용 전/후 비교
    1. 적용 전: 모든 필드가 포함된 쿼리가 실행됩니다.
    2. 적용 후: 변경된 필드만 포함된 쿼리가 실행됩니다.
       
      • -- 적용 전:
        UPDATE users SET username=?, password=? WHERE id=?;
        
        -- 적용 후:
        UPDATE users SET password=? WHERE id=?;

필요한 부분만 효율적으로 조회하기 (Projection)

  • Projection이란?
    • 데이터베이스에서 모든 데이터를 가져오는 것이 아니라, 필요한 데이터만 선택적으로 가져오는 기술.
    • 예를 들어, 사용자 정보에서 이름과 이메일만 필요할 때, 해당 데이터만 가져와서 성능을 최적화할 수 있습니다.
  • 유형
    1. 인터페이스 기반: 인터페이스를 통해 필요한 데이터를 가져옵니다.
    2. 클래스 기반: 특정 클래스를 생성해 필요한 데이터를 조회합니다.

2. JPA QueryDSL 사용하는 법

 

QueryDSL 사용법

  • QueryDSL 사용하는 법
    • QueryDSL은 Java에서 타입 세이프(Typed-Safe)하고 동적 쿼리를 작성하기 위한 프레임워크입니다. 주로 JPA와 함께 사용하며, JPQL이나 SQL 쿼리를 코드로 작성할 수 있습니다.
  • 1. 설정 방법
    • Gradle 의존성 추가
      • implementation 'com.querydsl:querydsl-jpa:5.x.x'
        annotationProcessor 'com.querydsl:querydsl-apt:5.x.x:jpa'
    • Q클래스 생성
      • @Entity로 선언된 엔티티 클래스의 Q클래스를 생성하려면 querydsl-apt를 사용해 컴파일 시 자동 생성
      • IntelliJ: Preferences > Annotation Processors > Enable annotation processing 활성화
      • 명령어로 생성
      • ./gradlew clean compileQuerydsl
  • 2. 사용 방법
    • 1) 기본적인 QueryDSL 사용
      • QUser user = QUser.user;
        
        List<User> users = queryFactory.selectFrom(user)
                                       .where(user.name.eq("John"))
                                       .fetch();
    • 2) 동적 쿼리 작성
      • BooleanBuilder builder = new BooleanBuilder();
        if (name != null) {
            builder.and(user.name.eq(name));
        }
        if (age != null) {
            builder.and(user.age.goe(age));
        }
        
        List<User> result = queryFactory.selectFrom(user)
                                        .where(builder)
                                        .fetch();
    • 3) 조인
      • QOrder order = QOrder.order;
        QUser user = QUser.user;
        
        List<Tuple> results = queryFactory.select(order.id, user.name)
                                          .from(order)
                                          .join(order.user, user)
                                          .where(user.name.eq("John"))
                                          .fetch();
    • 4) 페이징 및 정렬
      • List<User> users = queryFactory.selectFrom(user)
                                       .orderBy(user.age.desc())
                                       .offset(0)     // 시작 위치
                                       .limit(10)     // 최대 결과 수
                                       .fetch();
    • 5) 서브쿼리
      • QOrder order = QOrder.order;
        
        JPQLQuery<Double> subQuery = JPAExpressions.select(order.price.avg())
                                                   .from(order);
        
        List<User> result = queryFactory.selectFrom(user)
                                        .where(user.age.gt(subQuery))
                                        .fetch();

QueryDSL의 장점

  • 1. 타입 세이프 (Type-Safe)
    • 컴파일 시점에 쿼리의 정확성을 보장합니다.
    • JPQL에서 문자열로 작성한 쿼리는 런타임에 오류가 발생할 수 있으나, QueryDSL은 이를 방지합니다.
  • 2. 가독성 향상
    • 쿼리를 코드처럼 작성할 수 있어 가독성이 높습니다.
    • 코드에 IDE의 자동 완성을 사용할 수 있습니다.
  • 3. 동적 쿼리 작성 용이
    • BooleanBuilder와 같은 기능을 사용해 동적 조건을 간편하게 추가할 수 있습니다.
  • 4. JPA, SQL 모두 지원
    • JPQL 뿐만 아니라 네이티브 SQL도 지원합니다.
  • 5. 모듈 간 의존성 감소
    • JPA 엔티티를 기반으로 쿼리를 작성하기 때문에 SQL 쿼리문이 변경되어도 코드의 수정이 최소화됩니다.

 


QueryDSL의 단점

  • 1. 러닝 커브
    • 초반 학습이 필요합니다. 특히, JPQL을 처음 사용하는 사람에게 QueryDSL의 문법은 복잡하게 느껴질 수 있습니다.
  • 2. Q 클래스 의존
    • QueryDSL은 자동 생성된 Q 클래스를 사용해야 하므로, 이를 잘못 관리하면 프로젝트 구조가 복잡해질 수 있습니다.
  • 3. 코드 증가
    • 간단한 쿼리도 코드가 길어질 수 있습니다. (e.g., 단순 조회 쿼리)
  • 4. 추가적인 빌드 단계
    • Q 클래스 생성을 위해 추가적인 빌드 단계가 필요합니다. (예: compileQuerydsl)
  • 5. SQL 쿼리에 비해 성능 최적화가 어려움
    • JPQL 기반으로 동작하기 때문에 복잡한 SQL을 최적화하기에는 한계가 있을 수 있습니다.

 


QueryDSL을 사용할 때 적합한 상황

  • 동적 쿼리가 많을 때
    • 조건이 다양하고 동적으로 변경되는 쿼리 작성이 필요한 경우 적합합니다.
  • 타입 세이프가 중요한 프로젝트
    • 컴파일 시점에 쿼리 오류를 발견해야 하는 경우.
  • 가독성을 중시하는 팀
    • 유지보수와 협업에서 쿼리의 가독성을 높이고 싶을 때.

 


주요 메서드

 

QueryDSL에서 사용하는 주요 메서드 | Notion

QueryDSL에서 사용하는 주요 메서드들은 크게 JPQL-like 메서드와 BooleanExpression 메서드로 나눌 수 있습니다. 각각의 메서드는 QueryDSL의 주요 기능을 수행하며, JPQL과 유사한 방식으로 조건을 생성하거

shinelee26.notion.site

 

 


실제 사용해보기

  • ReservationRepository에 QueryDSL 사용을하기 위해 아래와 같이설정해야 한다.
    • 1. JPAConfiguration 작성
    • 2. ReservationRepositoryCustom -> Interface 생성
    • 3. ReservationRepositoryCusomImpl -> 클래스를 생성 ->  implements ReservationRepositoryCustom
    • 4. ReservationRepository -> extends JpaRepository, ReservationRepositoryCustom

 

 

  •  1. JPAConfiguration 코드 작성 내용

 

  • 2. ReservationRepositoryCustom 코드 작성 내용
    •  

    • 인터페이스로 커스텀 리포지토리 메서드를 정의한다.
    • 이 인터페이스는 구현체(ReservationRepositoryCustomImpl)에 의해 구현된다!
      • searchReservations: 특정 사용자(userId)와 아이템(itemId)을 기준으로 예약 정보를 검색
      • searchReservationIdAndStatus: 예약 ID(reservationId)를 기준으로 예약 정보를 검색

 

  • 3. ReservationRepositoryCusomImpl 코드 작성 내용

    • QueryDSL을 사용하여 실제 구현을 제공한다.
    • @RequiredArgsConstructor: final 필드인 jpaQueryFactory를 생성자 주입한다
      • electFrom(reservation): Reservation 엔티티를 조회 대상으로 설정.
      • leftJoin:
        • 예약 엔티티(reservation)와 사용자(user), 아이템(item)을 LEFT JOIN
        • fetchJoin: 연관된 엔티티를 한 번에 가져옴(지연 로딩 방지).
        • fetchJoin으로 연관된 엔티티를 한번에 가져와서 N+1 문제를 방지하려고 했는데 다른 좋은 방법이 있는지 더 찾아봐야겠다..
      • where: 조건을 지정.
        • reservation.user.id.eq(userId): 사용자 ID가 userId와 일치하는 조건.
        • reservation.item.id.eq(itemId): 아이템 ID가 itemId와 일치하는 조건.
      • fetch(): 결과를 리스트로 반환.