https://www.inflearn.com/course/Querydsl-%EC%8B%A4%EC%A0%84/
실전! Querydsl - 인프런 | 강의
Querydsl의 기초부터 실무 활용까지 한번에 해결, 본 강의는 자바 백엔드 개발의 실전 코스를 완성하는 마지막 강의 입니다. 스프링 부트와 JPA 실무 완전 정복 로드맵을 우선 확인해주세요. 로드
www.inflearn.com
강의를 들으며 생각 정리
순수 JPA 리포지토리와 Querydsl
순수 JPA 리포지토리
</repository/MemberJPARepository>
@Repository
public class MemberJPARepository {
private final EntityManager em;
public MemberJPARepository(EntityManager em) {
this.em = 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")
.setParameter("username", username)
.getResultList();
}
}
JPA로 만든 일반적인 리포지토리 형태이다. 여기에 Querydsl을 사용해보자.
@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);
}
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();
}
public List<Member> findByUsername(String username) {
return em.createQuery("select m from Member m where m.username = :username")
.setParameter("username", username)
.getResultList();
}
public List<Member> findByUsername_Querydsl(String username) {
return queryFactory
.selectFrom(member)
.where(member.username.eq(username))
.fetch();
}
}
1. JPAQueryFactory 주입
2. JPQL을 사용하는 메서드(findAll(), findByUsername()): Querydsl 버전 추가
-> Querydsl 장점: 자바 코드 어시스턴트가 편리하고 문법 오류에 대해 컴파일 오류로 잡을 수 있다.
+) JPAQueryFactory 스프링 빈 등록
다음과 같이 JPAQueryFactory를 스프링 빈으로 등록해서 주입 받아 사용해도 된다.
@Bean
JPAQueryFactory jpaQueryFactory(EntityManager em) {
return new JPAQueryFactory(em);
}
이 방식을 사용하면 생성자 주입을 다음과 같이 수정하면 된다.
@Repository
public class MemberJPARepository {
private final EntityManager em;
private final JPAQueryFactory queryFactory;
public MemberJPARepository(EntityManager em, JPAQueryFactory queryFactory) {
this.em = em;
this.queryFactory = queryFactory;
}
}
+) 동시성 문제는 걱정하지 않아도 된다. 스프링이 주입해주는 엔티티 매니저는 가짜 프록시 매니저이다. 이 가짜 엔티티 매니저는 실제 사용 시점에 트랜잭션 단위로 실제 엔티티 매니저(영속성 컨텍스트)를 할당해준다.
동적 쿼리와 성능 최적화 조회 - Builder 사용
<조회용 DTO 추가>
@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;
}
}
<검색 조건용 DTO 추가>
@Data
public class MemberSearchCondition {
//회원명, 팀명, 나이(ageGoe, ageLoe)
private String username;
private String teamName;
private Integer ageGoe;
private Integer ageLoe;
}
<동적 쿼리 - Builder 사용>
public List<MemberTeamDto> searchByBuilder(MemberSearchCondition condition) {
BooleanBuilder builder = new BooleanBuilder();
if (hasText(condition.getUsername())) {
builder.and(member.username.eq(condition.getUsername()));
}
if (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();
}
+) username, teamName과 같은 String 타입의 경우 null, "" 모두 고려해주기 위해 StringUtils.hasText()를 사용한다.
(예제에서는 StringUtils를 static import 하였다)
테스트
@Test
public void searchTest() {
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);
MemberSearchCondition condition = new MemberSearchCondition();
condition.setAgeGoe(35);
condition.setAgeLoe(40);
condition.setTeamName("teamB");
List<MemberTeamDto> result = memberJPARepository.searchByBuilder(condition);
assertThat(result).extracting("username").containsExactly("member4");
}
+) 만약 MemberSearchCondition 필드가 전부 null이어서 condition이 비어있다면 동적 쿼리는 모든 데이터를 불러오게 된다. 이는 트래픽이 많은 서버에서는 부담이 될 수 있다. 따라서, 동적 쿼리는 기본 초기 조건이나 페이징 처리를 해주는 것이 좋다.
동적 쿼리와 성능 최적화 조회 - Where절 파라미터 사용
public List<MemberTeamDto> search(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 BooleanExpression usernameEq(String username) {
return hasText(username) ? member.username.eq(username) : null;
}
private BooleanExpression teamNameEq(String teamName) {
return hasText(teamName) ? team.name.eq(teamName) : null;
}
private BooleanExpression ageGoe(Integer ageGoe) {
return ageGoe != null ? member.age.goe(ageGoe) : null;
}
private BooleanExpression ageLoe(Integer ageLoe) {
return ageLoe != null ? member.age.loe(ageLoe) : null;
}
동적 쿼리는 where절 파라미터 방식을 권장한다.
1. 메서드를 재사용할 수 있다.
2. 메서드를 조합할 수 있다.
ex)
private BooleanExpression ageBetween(int ageLoe, int ageGoe) {
return ageGoe(ageGoe).and(ageLoe(ageLoe));
}
+) 메서드 조합시 null 처리 주의
위 예시에서 만약 ageGoe()가 null을 반환한다면 NullPointException이 발생하게 된다.
메서드를 조합할 때는 이와 같은 null 처리를 주의해야 한다. 공부하면서 알게 된 두 가지 방법을 소개한다.
1. 메서드 조합시 BooleanBuilder 사용
private BooleanBuilder ageBetween(Integer ageGoe, Integer ageLoe) {
BooleanBuilder booleanBuilder = new BooleanBuilder();
return booleanBuilder
.and(ageGoe(ageGoe))
.and(ageLoe(ageLoe));
}
ageGoe()가 null이더라도 무시하기 때문에 안전하게 조합할 수 있다.
2. 각 메서드마다 BooleanBuilder 모두 사용
private BooleanBuilder usernameEq(String username) {
return nullSafeBuilder(() -> member.username.eq(username));
}
private BooleanBuilder teamNameEq(String teamName) {
return nullSafeBuilder(() -> team.name.eq(teamName));
}
private BooleanBuilder ageGoe(Integer ageGoe) {
return nullSafeBuilder(() -> member.age.goe(ageGoe));
}
private BooleanBuilder ageLoe(Integer ageLoe) {
return nullSafeBuilder(() -> member.age.loe(ageLoe));
}
private BooleanBuilder nullSafeBuilder(Supplier<BooleanExpression> f) {
try {
return new BooleanBuilder(f.get());
} catch (Exception e) {
return new BooleanBuilder();
}
}
private BooleanBuilder ageBetween(Integer ageLoe, Integer ageGoe) {
return ageLoe(ageLoe).and(ageGoe(ageGoe));
}
모든 메서드의 반환 타입을 BooleanBuilder로 한다. null 조건은 nullSafeBuilder() 메서드로 검증한다.
조회 API 컨트롤러 개발
편리한 데이터 확인을 위해 샘플 데이터를 추가하자.
샘플 데이터 추가가 테스트 케이스 실행에 영향을 주지 않도록 다음과 같이 프로파일을 설정하자.
<src/main/resources/application.yml>
spring:
profiles:
active: local
테스트는 기존 application.yml을 다음 경로로 복사하고, 프로파일을 test로 수정하자.
<src/test/resources/application.yml>
spring:
profiles:
active: test
이렇게 분리하면 main 소스코드와 테스트 소스 코드 실행시 프로파일을 분리할 수 있다.
<샘플 데이터 추가>
@Profile("local")
@Component
@RequiredArgsConstructor
public class InitMember {
private final InitMemberService initMemberService;
@PostConstruct
public void init() {
initMemberService.init();
}
@Component
static class InitMemberService {
@PersistenceContext
private EntityManager em;
@Transactional
public void init() {
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
for (int i = 0; i < 100; i++) {
Team selectedTeam = i % 2 == 0 ? teamA : teamB;
em.persist(new Member("member" + i, i, selectedTeam));
}
}
}
}
<조회 컨트롤러>
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberJPARepository memberJPARepository;
@GetMapping("/v1/members")
public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition) {
return memberJPARepository.search(condition);
}
}
http://localhost:8080/v1/members?teamName=teamB&ageGoe=31&ageLoe=35 처럼 쿼리 파라미터로 조건을 추가해서 테스트할 수 있다.
-> postman을 사용하면 편리하다.
'java > querydsl' 카테고리의 다른 글
[Querydsl] 스프링 데이터 JPA가 제공하는 Querydsl 기능 (2) | 2021.06.07 |
---|---|
[Querydsl] 실무 활용 - 스프링 데이터 JPA와 Querydsl (0) | 2021.06.06 |
[Querydsl] 중급 문법 (1) | 2021.06.03 |
[Querydsl] 기본 문법 (0) | 2021.06.02 |
[Querydsl] 예제 도메인 모델 (1) | 2021.06.01 |