3 분 소요


목차

  • 순수 JPA Repository와 Querydsl
  • 동적쿼리 Builder 사용
  • 동적쿼리 Where 사용
  • 조회 API 컨트롤러 개발


순수 JPA Repository와 Querydsl

순수 JPA

@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> findByUsername(String username) {
        return em.createQuery("select m from Member m where m.username = :username", Member.class)
                .setParameter("username", username)
                .getResultList();
    }
}

순수 JPA + 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();
}

[참고] JPAQueryFactory 스프링 빈 등록
다음과 같이 JPAQueryFactory를 스프링 빈으로 등록해서 주입받아 사용해도 된다.

@Bean
JPAQueryFactory jpaQueryFactory(EntityManager em) {
    return new JPAQueryFactory(em);
}

[참고]: 여기서 스프링이 주입해주는 엔티티 매니저는 실제 동작 시점에 진짜 엔티티 매니저를 찾아주는 프록시용 가짜 엔티티 매니저이기 때문에 동시성 문제는 걱정하지 않아도 된다. 이 가짜 엔티티 매니저는 실제 사용 시점에 트랜잭션 단위로 실제 엔티티 매니저(영속성 컨텍스트)를 할당해준다.


동적 쿼리와 성능 최적화 조회

MemberTeamDto - 조회 최적화용 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;
    }
}
  • @QueryProjection 추가
    • QMemberTeamDto를 생성하기 위해 compileQuerydsl 실행

[참고]: @QueryProjection을 사용하면 해당 DTO가 Querydsl을 의존하게 된다.
이런 의존이 싫다면 해당 어노테이션을 제거하고, Projection.bean(), fields(), constructor()를 사용하면 된다.

회원 검색 조건

@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(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();
}

조회 예제 테스트

// Team, Member를 넣는 과정은 생략..

MemberSearchCondition condition = new MemberSearchCondition();
condition.setAgeGoe(35);
condition.setAgeLoe(40);
condition.setTeamName("teamB")

List<MemberTeamDto> result = memberJpaRepository.searchByBuilder(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 StringUtils.hasText(username) ? member.username.eq(username) : null;
}

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

private BooleanExpression ageGoe(String ageGoe){
    return ageGoe != null ? member.age.goe(ageGoe) : null;
}

private BooleanExpression ageLoe(String ageLoe){
    return ageLoe != null ? member.age.loe(ageLoe) : null;
}

[참고]: where 절에 파라미터 방식을 사용하면 조건 재사용 가능


조회 API 컨트롤러 개발

편리한 데이터 확인을 위해 샘플 데이터를 추가.
샘플 데이터 추가가 테스트 케이스 실행에 영향을 주지 않도록 프로파일을 설정함

프로파일 설정 src/main/resources/application.yml

spring:
    profiles:
        active: local
...

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 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);
    }
}
  • Ex) http://localhost:8080/v1/members?teamName=teamB&ageGoe=31&ageLoe=35


<출처 : 인프런 - 실전! Querydsl(김영한)>

댓글남기기