이전 포스팅에서는 동적 쿼리와 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의 QMember는 static으로 구성했습니다.
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()를 이용해서 값을 세팅하는 것을 볼 수 있습니다.
QueryDSL은 where() 절을 이용해서 간단하게 구현되는 것을 확인할 수 있습니다. 만약 파라미터 조건이 많아진다면 가독성 면에서도 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
👨💻 전자책 출간
아울러 제가 🌟비전공자에서 2년만에 보안 전문 중견기업으로 이직 한 방법들을 정리한 전자책을 출간 하게 되었습니다. 어떤 걸 공부해야 하는지, 이직을 위해서 무엇을 준비해야 하는지, 제가 받았던 기술 면접 리스트 등 다양한 목차로 구성되어 있습니다. 또한, 구매 시 1:1 채팅을 이용하여 포트폴리오 첨삭을 도와드리고 있습니다. 🐕전자책으로 얻은 모든 수익은 유기견 센터 '팅*벨 입양센터'에 후원될 예정입니다. 관심 있으신 분들은 아래 링크를 참고해주세요😁
마치며
지금까지 QueryDSL의 실무 활용 포스팅이자 시리즈를 마치게 되었습니다. 새로운 기술을 접하고 테스트 코드를 작성하면서 익숙해지려고 많이 노력했네요. JPA 기술을 사용하는 실무 프로젝트가 잡히면 꼭 활용해 보고 싶네요. 독자님들도 꼭 그래 보시길 바랍니다! 다음 포스팅에서 뵐게요😊
QueryDSL Series
Reference
'JPA' 카테고리의 다른 글
QueryDSL 동적 쿼리 및 Bulk 연산 (1) | 2022.11.15 |
---|---|
QueryDSL Projection 결과 반환하는 방법 (0) | 2022.11.14 |
QueryDSL 조인 사용 방법 (0) | 2022.11.01 |
QueryDSL 정렬과 페이징 처리 및 집합 예시 (2) | 2022.10.31 |
QueryDSL 사용하는 조회 예제 (0) | 2022.10.28 |