전주호
WebSurf
전주호
전체 방문자
오늘
어제
  • 분류 전체보기 (63)
    • CS (1)
    • 프로그래밍 언어 (3)
      • JAVA (1)
      • Python (2)
    • WEB (35)
      • Spring (21)
      • FastAPI (1)
      • React (4)
    • Git (1)
    • Database (2)
    • Cloud (4)
    • Docker (0)
    • Linux (0)
    • AI (8)
      • ComputerVision (3)
      • CUDA (1)
      • Anaconda (1)
      • NLP (1)
    • ETC (9)
    • Project (0)
      • GolaBlur (0)
    • 알고리즘 문제 풀이 (0)
      • 프로그래머스 (0)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • MobileFaceSwap
  • springboot
  • AWS
  • conda
  • PYTHON
  • API
  • spring
  • Internet
  • Spring Security
  • db
  • 문제해결
  • oauth2
  • 트러블슈팅
  • 백엔드
  • http
  • AI
  • jpa
  • 클라우드
  • jwt
  • 테스트
  • 스프링
  • react
  • cloud
  • 백엔드로드맵
  • 단위테스트
  • EC2
  • websocket
  • web
  • junit
  • S3

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
전주호

WebSurf

SpringDataJpa와 QueryDsl 함께 사용하기 (feat. @DataJpaTest)
WEB/Spring

SpringDataJpa와 QueryDsl 함께 사용하기 (feat. @DataJpaTest)

2024. 12. 16. 17:19
반응형

 

SpringDataJpa를 사용하면 JpaRepository를 implements 한 레포지토리를 통해
쉽게 쿼리문을 만들어 사용합니다.
하지만 조금 더 복잡한 쿼리를 작성하려고 한다면
@Query 등을 사용하여 JPQL문을 작성해줘야 하죠.

JPQL을 작성할 때에는 텍스트(ex - "select m.id from member m")로 작성해야 합니다.
그러다 보면 자칫 오타가 나더라도 컴파일 시에는 확인할 수 없고
런타임에 발견되어 예상치 못한 문제가 발생될 수 있습니다.

QueryDsl은 모두 Java 코드로 이뤄져 있어 컴파일 시에 오타를 확인할 수 있습니다.
또한, 일부 로직을 메서드로 만들어 재사용할 수도 있죠.
이러한 장점들은 복잡한 쿼리문을 작성할 때에 매우 유리하게 작용될 수 있다 생각합니다.

 

제가 QueryDsl 없이 만들었던 쿼리를 바탕으로 QueryDsl을 적용해 보겠습니다.

public interface MemberRepository extends JpaRepository<Member, Long> {

  @Query(
      "SELECT new org.gitvest.gitvestb.member.repository.dto.MemberProfile(m.profileImageUrl, m.nickname, a.balance)"
          + " FROM Member m"
          + " JOIN Account a ON m.memberId = a.member.memberId"
          + " WHERE m.memberId = :memberId")
  Optional<MemberProfile> findMemberProfile(@Param("memberId") Long memberId);

}

간단한 조인을 하는 것인데 텍스트가 길게 늘어져 있어서 한눈에 들어오지는 않네요.
특히 dto로 조회를 할 때에는 더 심하게 느껴지는 것 같습니다.

 

QueryDsl 설정

우선 의존성을 추가해 줍시다.
저의 환경은 다음과 같습니다.
- SpringBoot 3.4.0
- Java 17
SpringBoot 버전이 3.x 이상이신 분은 동일하게 하셔도 됩니다.

build.gradle

dependencies {
    // ...
    
    //Querydsl 추가
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

// QueryDSL
sourceSets {
    main {
        java {
            srcDirs = ["$projectDir/src/main/java", "$projectDir/build/generated"]
        }
    }
}

 

이렇게 설정을 적용한 후 gradle의 compileJava를 실행시켜 봅시다.

그러면 build/generated/generated 폴더 내에 Q엔티티들이 생깁니다.
이러면 필수적인 준비는 끝났습니다.

추가적으로 QueryDsl은 JPAQueryFactory 객체를 통해 쿼리문을 작성하는데,
이 객체에 EntityManager 객체를 자동으로 주입해 주는 설정을 해줍시다.

@Configuration
public class QueryDslConfig {

  @PersistenceContext
  private EntityManager entityManager;

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

 

QueryDsl 적용

커스텀 쿼리문을 위한 레포지토리를 만들어 봅시다.

먼저 레포지토리의 인터페이스를 만들어 줍니다.

public interface MemberRepositoryCustom {

  Optional<MemberProfile> findMemberProfile(Long memberId);
}

이 인터페이스의 구현체를 만들어 우리가 원하는 쿼리를 생성해 줍시다.

import static org.프로젝트.account.entity.QAccount.account;
import static org.프로젝트.member.entity.QMember.member;

@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {

  private final JPAQueryFactory query;

  @Override
  public Optional<MemberProfile> findMemberProfile(Long memberId) {
    return Optional.ofNullable(
        query.select(Projections.constructor(
                MemberProfile.class,
                member.profileImageUrl,
                member.nickname,
                account.balance))
            .from(member)
            .join(account).on(account.member.eq(member))
            .where(member.memberId.eq(memberId))
            .fetchOne()
    );
  }
}

이렇게 작성하면 앞서 @Query를 사용해 만든 쿼리문과 동일한 쿼리문이 생성됩니다!
(이 포스트에서는 QueryDsl의 사용법에 대해서는 설명하지 않도록 하겠습니다.)

이제 이렇게 작성된 커스텀 쿼리 레포지토리를 기존 레포지토리에 상속해 주어 활용할 수 있도록 하겠습니다.

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom{

//  @Query(
//      "SELECT new org.gitvest.gitvestb.member.repository.dto.MemberProfile(m.profileImageUrl, m.nickname, a.balance)"
//          + " FROM Member m"
//          + " JOIN Account a ON m.memberId = a.member.memberId"
//          + " WHERE m.memberId = :memberId")
//  Optional<MemberProfile> findMemberProfile(@Param("memberId") Long memberId);

}

 

이를 통해서 MemberRepository를 사용하여 조회하는 기능은 유지되고,
QueryDsl을 성공적으로 적용할 수 있었습니다.

 

이제 이 기능이 잘 작동하는지 테스트 코드를 통해서 확인해 보겠습니다.

@DataJpaTest
@DisplayName("MemberRepository는")
class MemberRepositoryTest {

  @Autowired
  private EntityManager em;
  @Autowired
  private MemberRepository memberRepository;
  private Member savedMember;
  @BeforeEach
  void setUp() {
    savedMember = Member.builder()
        .memberId(null)
        .socialId(1L)
        .email("email")
        .nickname("nickname")
        .profileImageUrl("profileImageUrl")
        .build();
    em.persist(savedMember);
    em.flush();
    em.clear();
  }
  @Nested
  @DisplayName("Describe: findMemberProfile 메서드는")
  public class FindMemberProfile {
    private Account savedAccount;
    @BeforeEach
    void setUp() {
      savedAccount = Account.createNewAccount(savedMember);
      em.persist(savedAccount);
      em.flush();
      em.clear();
    }
    @Nested
    @DisplayName("Context: 존재하는 Member의 id 값을 입력받으면")
    public class ExistMemberId {
      // given
      private Long memberId;
      @BeforeEach
      void setUp(){
        memberId = savedMember.getMemberId();
      }
      @Test
      @DisplayName("It: 해당하는 Member와 Account의 데이터를 포함한 MemberProfile을 반환한다.")
      public void returnMemberProfile() {
        // when
        Optional<MemberProfile> memberProfile = memberRepository.findMemberProfile(memberId);
        // then
        assertThat(memberProfile.isPresent()).isTrue();
        assertThat(memberProfile.get().balance()).isEqualTo(savedAccount.getBalance());
      }
    }
    @Nested
    @DisplayName("Context: 존재하지 않는 Member의 id 값을 입력받으면")
    public class NonExistMemberId {
      // given
      private Long nonExistMemberId;
      @BeforeEach
      void setUp(){
        nonExistMemberId = 0L;
      }
      @Test
      @DisplayName("It: 빈 Optional 객체를 반환한다")
      public void optionalIsEmpty() {
        // when
        Optional<MemberProfile> memberProfile =
            memberRepository.findMemberProfile(nonExistMemberId);
        // then
        assertThat(memberProfile.isEmpty()).isTrue();
      }
    }
  }
}

기존 레포지토리를 테스트하는 코드입니다.
이 테스트를 실행시켜 보면 오류가 발생합니다...!

 

DataJpaTest

DataJpaTest는 JPA 관련 컨텍스트만을 로드하여 가벼운 테스트를 할 수 있게 해 줍니다.
우리는 QueryDsl이라는 추가적인 라이브러리를 사용했기 때문에, 
필요한 컨텍스트가 모두 로드되지 않아 문제가 발생합니다.

이를 해결하기 위한 방법으로는 간단하게 다음처럼 하면 됩니다.

@DataJpaTest
// QueryDslConfig를 임포트해주기!!!
@Import(QueryDslConfig.class)
@DisplayName("MemberRepository는")
class MemberRepositoryTest {
    // ...
}

 

결과적으로 모든 테스트가 성공하는 것을 확인할 수 있습니다!
성공적으로 QueryDsl로 전환했습니다!

 

반응형
저작자표시 (새창열림)

'WEB > Spring' 카테고리의 다른 글

QueryDsl에서 DTO로 조회할 때 @QueryProjection 사용하기  (0) 2024.12.18
단위 테스트 - Controller (feat. 추가적인 단위 테스트의 방향성)  (0) 2024.08.31
단위 테스트 - Service (feat. 테스트 더블)  (0) 2024.08.28
단위 테스트 - Repository (feat. 테스트 픽스쳐)  (0) 2024.08.27
Service의 메서드들이 중복해서 사용하는 로직을 테스트하기  (0) 2024.08.20
    'WEB/Spring' 카테고리의 다른 글
    • QueryDsl에서 DTO로 조회할 때 @QueryProjection 사용하기
    • 단위 테스트 - Controller (feat. 추가적인 단위 테스트의 방향성)
    • 단위 테스트 - Service (feat. 테스트 더블)
    • 단위 테스트 - Repository (feat. 테스트 픽스쳐)
    전주호
    전주호

    티스토리툴바