[QueryDSL] 3. 순수 JPA 와 QueryDSL

HJ·2024년 3월 13일
0

QueryDSL

목록 보기
3/4
post-thumbnail

김영한 님의 실전! Querydsl 강의를 보고 작성한 내용입니다.


1. 순수 JPA Repository

1-1. 생성하기

@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 List<Member> findByUsername_QueryDSL(String username) {
        return queryFactory
                .selectFrom(member)
                .where(member.username.eq(username))
                .fetch();
    }
}

순수 JPA 를 이용한 Repository 이기 때문에 JPA 에 접근하기 위해 EntityManger 가 필요합니다. 또 QueryDSL 을 사용하려면 JpaQueryFactory 가 필요한데 EntityManager 를 파라미터로 넘겨주어 생성합니다.


1-2. 스프링 빈 등록하기

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

생성자를 사용하지 않고, JpaQueryFactory 를 스프링 빈으로 등록해서 사용할 수도 있습니다. 이때 Repository 에서는 @RequiredArgsConstructor 를 사용해서 주입받으면 됩니다.

스프링 빈은 싱글톤이기 때문에 같은 객체를 모든 멀티스레드에서 사용하기 때문에 동시성 문제에 대해 걱정할 수 있는데 전혀 문제가 되지 않습니다.

JpaQueryFactory 에 대한 모든 동시성 문제는 엔티티 매니저에 의존하는데, 엔티티 매니저를 스프링과 함께 사용하면 동시성 문제랑 전혀 관계 없이 트랜잭션 단위로 분리돼서 동작합니다.

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




2. 준비하기

2-1. 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 을 생성자에 붙여 Q 클래스를 생성합니다. 해당 어노테이션을 사용하면 select 에서 new 를 통해 DTO 를 바로 생성할 수 있지만 DTO 가 QueryDSL 라이브러리에 의존하게 된다는 단점이 있습니다.


2-2. 검색 조건 생성

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

회원명, 팀명, 나이를 검색 조건에 사용하기 위해 생성합니다.




3. 동적 쿼리와 DTO 조회

3-1. Builder 사용

public List<MemberTeamDto> searchByBuilder(MemberSearchCond condition) {

    BooleanBuilder builder = new BooleanBuilder();

    if (StringUtils.hasText(condition.getUsername())) {
        builder.and(member.username.eq(condition.getUsername()));
    }


    if (condition.getAgeGoe() != null) {
        builder.and(member.age.goe(condition.getAgeGoe()));
    }

    return queryFactory
            .select(new QMemberTeamDto(
                    member.id,
                    member.username,
                    member.age,
                    team.id,
                    team.name))
            .from(member)
            .leftJoin(member.team, team)
            .where(builder)
            .fetch();
}
  1. 검색조건에서 문자는 null 이거나 빈 문자열이 들어올 수 있는데 StringUtils.hasText() 가 그런 것을 다 체크해줍니다.

  2. @QueryProjection 를 사용했기 때문에 new 키워드와 생성자를 통해 바로 DTO 를 조회할 수 있습니다.

  3. 어노테이션을 사용하지 않는다면 현재 사용하는 DTO 와 엔티티들의 필드명이 다르기 때문에 as 를 사용해서 DTO 와 엔티티의 필드명을 맞춰주어야 합니다.


참고로 검색 조건이 없다면 모든 데이터를 가져오게 됩니다. 하지만 데이터가 많이 쌓이다보면 모든 것을 가져오는게 성능 상 좋지 않습니다. 그래서 기본 조건을 넣거나, limit 를 설정하는 것이 좋습니다.


3-2. where 절 파라미터 사용

public List<MemberTeamDto> search(MemberSearchCond 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 ageGoe(Integer ageGoe) {
    return ageGoe == null ? null : member.age.goe(ageGoe);
}
  1. where 절 다중 파라미터에 각 검색 조건들에 대한 메서드를 사용합니다.

  2. 각 메서드는 null 과 빈 문자열에 주의해서 검색 조건을 작성합니다. 이때 hasText 는 StringUtils 의 메서드입니다.

  3. 작성된 검색 조건 메서드들은 재사용이 가능하며, Predicate 가 아닌 BooleanExpression 을 사용하면 검색 조건들을 조합할 수 있습니다.




참고> 프로파일 분리

[ 설정 파일 작성 ]

spring:
  profiles:
    active: local    # main/resources/application.yml

spring:
  profiles:
    active: test    # test/resources/application.yml

로컬에서의 실행과 테스트에서의 실행 프로파일을 분리하기 위해 test/resources 에 application.yml 파일을 추가합니다. 추가 후에는 위처럼 로컬과 테스트 프로파일을 각각 설정합니다.


[ 프로파일 적용 ]

@Profile("local")
@Component
@RequiredArgsConstructor
public class InitMember {
    ...
}

위처럼 @Profile("local") 을 붙여주면 local 프로파일이 실행될 때만 동작하게 됩니다.

profile
공부한 내용을 정리해서 기록하고 다시 보기 위한 공간

0개의 댓글