내가 공부한 것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼 겸 상세히 기록하고 얕은 부분들은 가볍게 포스팅하겠습니다.
이번 글은 다음과 같은 순서로 이어질 것 이다.
- 순수 JPA 리포지토리와 Querydsl
- 동적쿼리 Builder 적용
- 동적쿼리 Where 적용
- 조회 API 컨트롤러 개발
1. 순수 JPA 리포지토리와 Querydsl
우선 순수 JPA를 기반으로 repository를 하나 만들자.
Repository
public class MemberJpaRepository {
private final EntityManager em;
private final JPAQueryFactory jpaQueryFactory;
public MemberJpaRepository(EntityManager em) {
this.em = em;
this.jpaQueryFactory = 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);
}
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
public List<Member> findByUsername(String username) {
return em.createQuery("select m from Member m where m.username = :username", Member.class)
.setParameter("username", username)
.getResultList();
}
}
위 코드를 QueryDsl를 사용하는 방식으로 일부 변경해 보자!
public List<Member> findAll_Querydsl() {
return queryFactory
.selectFrom(member)
.fetch();
}
public List<Member> findByUsername_Querydsl(String username) {
return queryFactory
.selectFrom(member)
.where(member.username.eq(username))
.fetch();
}
java 코드로 작성하기 때문에 매우 간단하게 끝나버렸다.
▶ JPAQueryFactory 스프링 빈 등록
기존에는 다음과 같이 생성자를 통해 EntityManager를 받고, 이를 이용하여 JPAQueryFactory를 만들었다.
@Repository
public class MemberJpaRepository {
private final EntityManager em;
private final JPAQueryFactory queryFactory;
public MemberJpaRepository(EntityManager em) {
this.em = em;
this.queryFactory = new JPAQueryFactory(em);
}
// ... 생략
}
하지만 그냥 다음과 같이 Bean으로 만들어서 등록해두고 사용해도 된다.
@SpringBootApplication
public class QuerydslApplication {
public static void main(String[] args) {
SpringApplication.run(QuerydslApplication.class, args);
}
// Bean으로 등록하기
@Bean
JPAQueryFactory jpaQueryFactory(EntityManager em) {
return new JPAQueryFactory(em);
}
}
Repository에서는 @RequiredArgsContructor를 사용하면 편하게 DI 받을 수 있다.
참고로 동시성 문제는 걱정하지 않아도 된다.
왜냐하면 Spring이 주입해주는 EntityManager는 실제 동작 시점에 진짜 EntityManager를 찾아주는 proxy EntityManager이다.
이 가짜 EntityManager는 실제 사용 시점에 트랜잭션 단위로 실제 EntityManager(영속성 컨텍스트)를 할당해준다.
2. 동적 쿼리와 성능 최적화 조회 - Builder 사용
이번시간에는 동적쿼리를 통해서 DTO를 조회해올 것 이다. 우선 사용할 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을 사용하고 있기 때문에 QMemberTeamDto 를 생성하기 위해 ./gradlew compileQuerydsl 을 한번 실행하자.
또한 검색 조건으로 사용할 class를 하나 만들자.
- MemberSearchCondition
@Data
public class MemberSearchCondition {
// 회원명, 팀명, 나이(goe, loe)
private String username;
private String teamName;
private Integer ageGoe;
private Integer ageLoe;
}
기존의 repository에 다음과 같이 검색조건을 받아서 검색한 후, 반환해주는 메서드를 만들어보자.
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,
member.username,
member.age,
team.id,
team.name
)
)
.from(member)
.leftJoin(member.team, team)
.where(builder)
.fetch();
}
이를 테스트 코드를 통해서 검증해 보자.
@Test
public void search_builder_test() {
// given
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);
// when
MemberSearchCondition searchCondition1 = new MemberSearchCondition();
searchCondition1.setAgeGoe(35);
searchCondition1.setAgeLoe(40);
searchCondition1.setTeamName("teamB");
MemberSearchCondition searchCondition2 = new MemberSearchCondition();
searchCondition2.setTeamName("teamB");
// then
List<MemberTeamDto> result = memberJpaRepository.searchByBuilder(searchCondition1);
assertThat(result).extracting("username").containsExactly("member4");
List<MemberTeamDto> result2 = memberJpaRepository.searchByBuilder(searchCondition2);
assertThat(result2).extracting("username").containsExactly("member3", "member4");
}
35살 이상 40살 이하의 회원을 찾는 조건을 searchCondition으로 만들었다.
이를 searchByBuilder에 넘겨 결과를 전달받고 있다.
만약 검색 조건이 하나도 없이 전달됬다면 어떻게 될까?
모든 회원에 대한 정보를 다 조회해오게 된다. 이게 문제점이다!
회원이 적어서 지금은 다행이지만, 회원이 점차 늘어나 3만명 정도만 된다 쳐도, 조건이 없을때마다 3만건을 조회해오는 것은 무리이다.
=> 기본 조건을 지정해 주거나, limit을 걸어주는것이 좋다.
(Intellij tip : command + 1 -> 화면 왼쪽 디렉토리 목록리스트 숨김, 보이기 기능)
(Intellij tip : command + shift + enter -> if 조건문 까지만 입력한 후 {} 중괄호를 만들면서 커서가 중괄호 안으로 이동하는 기능)
3. 동적 쿼리와 성능 최적화 조회 - Where절 파라미터 사용
이번시간에는 직전에 builder를 사용하여 만든 동적 쿼리를 where절을 활용하여 만들어보자.
원래 BooleanExpression을 반환해야 하지만, null-safe한 코드를 만들기 위해 BooleanBuilder를 이용하였다.
public List<MemberTeamDto> searchByWhereParams(MemberSearchCondition condition) {
return queryFactory
.select(new QMemberTeamDto(
member.id,
member.username,
member.age,
team.id,
team.name
)
)
.from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
)
.fetch();
}
private BooleanBuilder usernameEq(String username) {
if(StringUtils.hasText(username)) {
return new BooleanBuilder(member.username.eq(username));
}
return new BooleanBuilder();
}
private BooleanBuilder teamNameEq(String teamName) {
if(StringUtils.hasText(teamName)) {
return new BooleanBuilder(team.name.eq(teamName));
}
return new BooleanBuilder();
}
private BooleanBuilder ageGoe(Integer ageGoe) {
if(ageGoe != null) {
return new BooleanBuilder(member.age.goe(ageGoe));
}
return new BooleanBuilder();
}
private BooleanBuilder ageLoe(Integer ageLoe) {
if(ageLoe != null) {
return new BooleanBuilder(member.age.loe(ageLoe));
}
return new BooleanBuilder();
}
직전 builder를 이용했던 test 코드를 그대로 사용해도 동일하게 테스트를 통과한다.
추가로 다음과 같은 메서드를 만든다고 해보자.
private BooleanBuilder ageBetween(Integer ageLoe, Integer ageGoe) {
return ageLoe(ageLoe).and(ageGoe(ageGoe));
}
기존의 ageLoe() 와 ageGoe() 메서드를 조립하여 새로운 메서드를 만들게 되었다.
또한 BooleanBuilder를 반환하도록 하여 null-safe하게 만들었기 때문에 안전한다.
ageLoe(ageLoe)가 null을 반환할 일이 없어졌기 때문에 .and 를 적용할떄 NullPointException이 발생할 일이 없다.
위와 같이 다양한 함수를 만들어 조립할수 있다는 점이 매우 큰 장점이다.
'BackEnd > JPA' 카테고리의 다른 글
[JPA] Spring Data JPA가 제공하는 QueryDsl 기능 (0) | 2022.05.16 |
---|---|
[JPA] 실무 활용 - Spring Data JPA와 Querydsl (0) | 2022.05.15 |
[JPA] QueryDSL 중급문법 - 2 (0) | 2022.05.14 |
[JPA] QueryDSL 중급문법 - 1 (0) | 2022.05.14 |
[JPA] QueryDSL 기본문법 - 4 (0) | 2022.05.14 |
댓글