JPA

QueryDSL 실무 활용

빈코 2022. 11. 18. 17:29

이전 포스팅에서는 동적 쿼리와 Bulk 연산에 대해 알아보았습니다. 이번 포스팅은 QueryDSL 시리즈의 마지막 포스팅인 실무 활용 편입니다. 사실 모든 기능들을 포스팅할 수는 없어서 시리즈 순서대로 간단하게 정리해보려 합니다😄

 

로고
실무 활용

개요

예시 관계도

이전 포스팅에서 사용했던 엔티티들을 재사용할 예정입니다. 혹여 엔티티 클래스의 코드가 궁금하신 분들은 엔티티 설계 포스팅을 참고해주세요😄

 

📌 저는 BeforeEach 어노테이션을 활용해 가 데이터가 들어간 상황에서 테스트를 진행한 것입니다.

 

@BeforeEach
public void before() {
    queryFactory = new JPAQueryFactory(em);
    Team teamA = new Team("teamA");
    Team teamB = new Team("teamB");
    em.persist(teamA);
    em.persist(teamB);

    Member member1 = new Member("member1", 10, teamA);
    Member member2 = new Member("member2", 20, teamA);

    Member member3 = new Member("member3", 30, teamB);
    Member member4 = new Member("member4", 40, teamB);
    em.persist(member1);
    em.persist(member2);
    em.persist(member3);
    em.persist(member4);
}
MEMBER_ID NAME AGE TEAM_NAME
1 member1 10 teamA
2 member2 20 teamA
3 member3 30 teamB
4 member4 40 teamB

 

📌 이전까지는 QuerydslBasicTest에서 진행했는데 이번 포스팅은 한번에 정리한 것이고, 이번 포스팅에서는 새로운 Repository를 만들고 해당 Test를 따로 구성할 예정입니다. 

 

MemberJpaRepositry 생성📘

@Repository
public class MemberJpaRepository {

    private final EntityManager em;
    private final JPAQueryFactory queryFactory;

    public MemberJpaRepository(EntityManager em) {
        this.em = em;
        this.queryFactory = new JPAQueryFactory(em);
    }

    public void save(Member member) {
        em.persist(member);
    }

    public Optional<Member> findById(Long id){
        Member findMember = em.find(Member.class, id);
        return Optional.ofNullable(findMember);
    }
 }

 

첫번째로 MemberJpaRepository를 생성하였습니다. 그리고 가장 기본적인 저장(save) 메서드와 MemberId 값을 이용하여 Member Entity를 찾는 findById 메서드도 같이 구성하였습니다.

 

Optional로 반환 받는 이유는 파라미터로 넘겨받은 memberId에 해당하는 Member Entity가 없을 경우도 있기 때문입니다. 지금까지 작성한 것은 기본적인 JPQL이고 QueryDSL과 비교해면서 다른 메서드를 작성해보겠습니다.

 

public List<Member> findAll() {
    return em.createQuery("select m from Member m", Member.class)
            .getResultList();
}

public List<Member> findAll_Querydsl() {
    return queryFactory
            .selectFrom(member)
            .fetch();
}

 

findAll() 메서드는 JPQL을 이용해서 직접적으로 쿼리문을 작성한 것이며, findAll_Querydsl() 메서드는 QueryDSL을 사용한 메서드입니다. 이전 포스팅까지 거듭 강조했던 것처럼 QueryDSL을 사용하면 Java 코드만 사용해서 반환 값을 리턴 받을 수 있습니다. 또한, 컴파일시에 오류를 발견할 수 있다는 큰 장점이 있습니다.

 

📌 QueryDSL의 QMemberstatic으로 구성했습니다.

import static study.querydsl.entity.QMember.member;

 

이번에는 파라미터가 있을 경우의 로직 구성을 살펴볼게요. 만약 회원 이름을 이용하여 회원을 찾는다면 JPQL과 QueryDSL에서는 어떻게 로직을 구성할까요?

 

public List<Member> findByUsername(String username) {
    return em.createQuery("select m from Member m where m.username = :username"
    ,Member.class)
            .setParameter("username", username)
            .getResultList();
}

public List<Member> findByUsername_Querydsl(String username) {
    return queryFactory
            .selectFrom(member)
            .where(member.username.eq(username))
            .fetch();
}

 

기본적인 구성은 같습니다. 하지만 검색 조건을 적용시키는 부분에 차이가 있는 것을 볼 수 있습니다. JPQL은 쿼리문에 직접 세팅을 하고 setParameter()를 이용해서 값을 세팅하는 것을 볼 수 있습니다.

 

QueryDSLwhere() 절을 이용해서 간단하게 구현되는 것을 확인할 수 있습니다. 만약 파라미터 조건이 많아진다면 가독성 면에서도 QueryDSL 사용이 더 좋아 보이네요😊

 

테스트📒

@SpringBootTest
@Transactional
class MemberJpaRepositoryTest {

    @Autowired
    EntityManager em;

    @Autowired
    MemberJpaRepository memberJpaRepository;

    @Test
    public void basicTest() {
        Member member = new Member("member1", 10);
        memberJpaRepository.save(member);

        List<Member> result = memberJpaRepository.findAll_Querydsl();
        assertThat(result).containsExactly(member);

        List<Member> result2 = memberJpaRepository.findByUsername_Querydsl("member1");
        assertThat(result2).containsExactly(member);
    }
 }

 

JPQL 로직은 제외하고 QueryDSL로 작성한 메서드들만 테스트를 진행했어요. 신규 회원을 등록하고 findAll_Querydsl() 메서드와 findByUsername_Querydsl() 메서드를 각각 실행한 후에 해당 Member가 컬렉션에 담겨 있는지 containsExactly()로 확인해 보았습니다.

 

테스트 결과
테스트 결과

테스트 결과 성공한 것을 확인할 수 있었고, 원했던 대로 쿼리문이 전달된 것을 확인할 수 있었네요😀

 

Builder VS Where 📚

사실 builder 패턴where 패턴은 상황에 맞게 사용하는 것이지 뭐가 더 좋다고 정의할 순 없을 것 같네요. 이번 예시는 클라이언트 요구사항이 회원의 이름과 나이, 회원의 id, 소속된 팀의 이름과 팀 id를 필요로 한다는 전제조건이 있다고 가정해볼게요. 첫 번째로 해야 할 것은 위의 5개 필드를 리턴 받는 DTO를 구성해야 합니다.

 

MemberTeamDto 생성📙

@Data
public class MemberTeamDto {
    private Long memberId;
    private String username;
    private int age;
    private Long teamId;
    private String teamName;

    @QueryProjection
    public MemberTeamDto(Long memberId, String username, int age, Long teamId, String teamName) {
        this.memberId = memberId;
        this.username = username;
        this.age = age;
        this.teamId = teamId;
        this.teamName = teamName;
    }
}

 

@QueryProjection은 프로젝션을 사용할 때 가장 권장되는 방법입니다. 생성자를 통해 DTO를 조회하는 방법에 사용하는데 위의 코드처럼 생성자 위에 선언해주시면 됩니다😊

 

MemberSearchConditon 생성📗

@Data
public class MemberSearchCondition {
    private String username;
    private String teamName;
    private Integer ageGoe; 
    private Integer ageLoe; 
}

MemberSearchCondition은 말 그대로 검색 조건을 넘겨받을 때 사용하는 클래스입니다. View에서 Controller로, Controller에서 Service로 등 많은 곳에 쓰일 수 있습니다😀

 

ageGoe 필드는 나이가 크거나 같을 때 사용할 예정이고, ageLoe는 나이가 작거나 같을 때 사용할 예정입니다

 

Builder 패턴📔

public List<MemberTeamDto> searchByBuilder(MemberSearchCondition condition) {

    BooleanBuilder builder = new BooleanBuilder();
    if(StringUtils.hasText(condition.getUsername())) {
        builder.and(member.username.eq(condition.getUsername()));
    }
    if(StringUtils.hasText(condition.getTeamName())) {
        builder.and(team.name.eq(condition.getTeamName()));
    }
    if(condition.getAgeGoe() != null) {
        builder.and(member.age.goe(condition.getAgeGoe()));
    }
    if(condition.getAgeLoe() != null) {
        builder.and(member.age.loe(condition.getAgeLoe()));
    }

    return queryFactory
            .select(new QMemberTeamDto(
                    member.id.as("memberId"),
                    member.username,
                    member.age,
                    team.id.as("teamId"),
                    team.name.as("teamName")))
            .from(member)
            .leftJoin(member.team, team)
            .where(builder)
            .fetch();
}

첫 번째로 이전에 만든 MemberSearchCondition을 넘겨받습니다. 이후에, BooleanBuilder를 생성하고 해당 값들을 and 조건으로 builder에 차곡차곡 쌓아줍니다.

 

String으로 넘겨받는 필드들은 null이나 "" 값으로 들어올 수 있기 때문에, StringUtils에서 제공하는 hasText() 메서드를 활용하였습니다. 

 

이후에 QueryDSL 문법을 이용하는데, 이전에 만든 MemberTeamDto와 필드명을 맞춰야 하기 때문에 as() 별칭 기법을 사용해서 맞추었습니다. 검색 조건이 쌓인 builder는 where() 절에 넣어주시면 끝납니다😄

 

확실히 JPQL보다 가독성이 좋지만 한 가지 단점으로는 해당 Builder를 다른 곳에서 재사용이 불가능하다는 점이 있습니다. 그러면 Where 패턴을 사용해볼까요?

 

Where 패턴📖

public List<MemberTeamDto> search(MemberSearchCondition condition) {
    return queryFactory
            .select(new QMemberTeamDto(
                    member.id.as("memberId"),
                    member.username,
                    member.age,
                    team.id.as("teamId"),
                    team.name.as("teamName")))
            .from(member)
            .leftJoin(member.team, team)
            .where(
                    usernameEq(condition.getUsername()),
                    teamNameEq(condition.getTeamName()),
                    ageGoe(condition.getAgeGoe()),
                    ageLoe(condition.getAgeLoe())
            )
            .fetch();
}

private BooleanExpression usernameEq(String username) {
    if(StringUtils.hasText(username)) {
        return member.username.eq(username);
    }
    return null;
}

private BooleanExpression teamNameEq(String teamName) {
    if(StringUtils.hasText(teamName)) {
        return team.name.eq(teamName);
    }
    return null;
}

private BooleanExpression ageGoe(Integer ageGoe) {
    if(ageGoe != null) {
        return member.age.goe(ageGoe);
    }
    return null;
}

private BooleanExpression ageLoe(Integer ageLoe) {
    if(ageLoe != null) {
        return member.age.loe(ageLoe);
    }
    return null;
}

사실 코드 내용이 길어졌을 뿐이지 맥락은 Builder 패턴과 비슷하게 작동합니다. Builder를 여러 곳에 분산시켜서 적용했다고 생각하시면 이해하기 편하실 것 같네요!

 

Where 패턴의 가장 큰 장점은 여러 메서드로 나누어서 각각의 메서드를 혼합하여 사용할 수 있고, 그만큼 재사용성이 높아지는 장점을 가집니다😊

 


테스트 작성📝

@Test
public void searchTest() {
    MemberSearchCondition condition = new MemberSearchCondition();
    condition.setAgeGoe(35);
    condition.setAgeLoe(40);
    condition.setTeamName("teamB");

    List<MemberTeamDto> result = memberJpaRepository.search(condition);

    assertThat(result).extracting("username").containsExactly("member4");
}

 

테스트 결과
테스트 결과

개요에서 미리 등록한 회원 중에 테스트 코드에 부합하는 회원은 "member4"에 해당합니다. 테스트 결과 쿼리 문도 검색 조건에 맞게 들어간 걸 확인할 수 있었고, "member4"가 포함되어 있는지의 여부도 성공적으로 결과가 나온 것을 확인할 수 있었습니다.

 

 


👨‍👩‍👦‍👦 오픈채팅방 운영

취업을 준비하는 예비 개발자분들을 위한 질문&답변할 수 있는 공간을 만들었습니다. 취업과 이직을 하기 위해서 어떤 걸 중점적으로 준비해야 하는지부터 포트폴리오&이력서 작성법 등 다양한 질문들을 받고 답변을 드립니다. 참여하셔서 다양한 정보 얻고 가시면 좋을 것 같네요😁

 

참여코드 : 456456

https://open.kakao.com/o/gVHZP8dg

 

비전공 개발자 취업 준비방(질문&답변)

#비전공 #개발자 #취업 #멘토링 #부트캠프 #국비지원 #백엔드 #프론트엔드 #중소기업 #중견기업 #자바 #Java #sql

open.kakao.com

 


👨‍💻 전자책 출간

아울러 제가  🌟비전공자에서 2년만에 보안 전문 중견기업으로 이직 한 방법들을 정리한 전자책을 출간 하게 되었습니다. 어떤 걸 공부해야 하는지, 이직을 위해서 무엇을 준비해야 하는지, 제가 받았던 기술 면접 리스트 등 다양한 목차로 구성되어 있습니다. 또한, 구매 시 1:1 채팅을 이용하여 포트폴리오 첨삭을 도와드리고 있습니다. 🐕전자책으로 얻은 모든 수익은 유기견 센터 '팅*벨 입양센터'에 후원될 예정입니다. 관심 있으신 분들은 아래 링크를 참고해주세요😁

https://kmong.com/gig/480954

 

비전공개발자 2년만에 중견기업 들어간 방법 | 14000원부터 시작 가능한 총 평점 0점의 전자책, 취

0개 총 작업 개수 완료한 총 평점 0점인 Binco의 전자책, 취업·이직 전자책 서비스를 0개의 리뷰와 함께 확인해 보세요. 전자책, 취업·이직 전자책 제공 등 14000원부터 시작 가능한 서비스

kmong.com


 

마치며

지금까지 QueryDSL의 실무 활용 포스팅이자 시리즈를 마치게 되었습니다. 새로운 기술을 접하고 테스트 코드를 작성하면서 익숙해지려고 많이 노력했네요. JPA 기술을 사용하는 실무 프로젝트가 잡히면 꼭 활용해 보고 싶네요. 독자님들도 꼭 그래 보시길 바랍니다! 다음 포스팅에서 뵐게요😊

 

QueryDSL Series

1. QueryDSL 적용 및 예제 시리즈 시작

2. QueryDSL 조회 예제

3. QueryDSL 정렬 & 페이징 & 집합

4. QueryDSL 조인 예제

5. QueryDSL Projection

6. QueryDSL 동적 쿼리 & Bulk 연산

7. QueryDSL 실무 활용(현재)

 

Reference

실전! Querydsl

 

반응형