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 |