Java & Spring

QueryDSL

nippycloud 2026. 4. 30. 13:09

환경 : Spring 4.x, Java 21, H2 DB 

 

QueryDSL : JPA 환경에서 동적 쿼리 작성에 강한 라이브러리 이름이다.

 

Spring Boot가 아닌 일반 Spring 환경(MVC, Legacy..) 에서는 MyBatis가 동적 쿼리에 강한 장점을 가지고 있어 별도 라이브러리 추가가 필요하지 않았다.

Spring Boot 환경에서는 JPA Criteria 라는 것을 기반으로 동적 쿼리를 작성할 수 있지만 Criteria 자체가 가독성이 떨어지기 때문에 별도 외부 라이브러리로 QueryDSL을 사용하는 추세이다.

 

QueryDSL 의존성 (build.gradle)

dependencies {
    ...
    
    // QueryDSL dependency
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

clean {
	delete file('src/main/generated')
}

 

QueryDSL은 JPA 엔티티에 대해 먼저 Q 타입을 추출하여 Q 타입 엔티티로 쿼리를 작성하고 동작한다. 

 

Q 타입 엔티티 : QueryDSL에서 사용하는 메타 데이터 클래스이다.

실제 JPA 엔티티를 기반으로 자동 생성되며, 편리한 동적 쿼리 작성과 타입 안전성이 높다는 장점을 가진다.

 

clean {
    delete file('src/main/generated')
}

- 스프링 구 버전에서는 빌드 도구가 gradle이 아닌 intelliJ로 설정되어있을 경우 Q 타입 엔티티가 src/main/generated 하위에 생성될 때도 있다.

- 일반적으로 최신 스프링 부트는 Q 타입 엔티티를 build - generated - sources - annotationProcessor - java - main 하위에서 생성한다.

- ./gradlew clean 명령어 입력 시 기본적으로 build와 그 하위의 파일들을 모두 삭제하는데, clean { delete file ('src/main/generated')} 추가 시 build 뿐만이 아니라 src - main - generated 하위의 Q 타입 엔티티 파일까지 모두 삭제한다.

 

- JPA 엔티티 구조가 변경되거나 Spring Boot 실행 시 원인을 알 수 없는 오류들이 발생할 경우 ./gradlew clean을 입력하면 빌드 결과가 모두 삭제된다.

- 스프링 부트를 재실행하거나 ./gradlew compileJava 실행 시 build 결과가 현재 프로그램에 맞게 정확한 build 결과물을 산출한다.

 

gradlew : gradle 빌드 도구를 wrapping한 실행 파일

 

Q 타입 엔티티는 컴파일 시점에 자동으로 생성되기 때문에 Git에 포함하지 않도록 .gitignore에서 관리하는 것이 일반적이다.

.gitignore에는 기본적으로 build/가 작성되어 있어서 build 디렉토리와 그 하위의 모든 것들을 git의 관리 대상으로 두지 않는다.

만약 구 버전 스프링 사용 시 Q 타입 엔티티가 src 하위에 들어가고 있다면 별도로 .gitignore에 등록이 필요하다.

 

 

@SpringBootTest
@Transactional
class QueryDslApplicationTests {

    @Autowired
    EntityManager em;

    // contextLoads : 해당 스프링 부트 프로젝트가 오류 없이 제대로 실행(로딩)되는지를 확인하는 가장 기초적인 테스트
    @Test
    void contextLoads() {

       TestEntity t = new TestEntity();
       em.persist(t); // 쓰기 지연 저장소에 insert 쿼리 저장, 영속화 (1차 캐시에 저장)

       JPAQueryFactory queryFactory = new JPAQueryFactory(em);

       // Q 타입 엔티티는 기존 변수 이름과 동일한 Q 타입 엔티티를 가진다.
       // public static final QTestEntity testEntity = new QTestEntity("testEntity");
       QTestEntity qTestEntity = QTestEntity.testEntity;


       // qTestEntity 를 이용하여 TestEntity 테이블에 있는 데이터를 select (모든 데이터 select : fetch())
       // select 쿼리가 동작하기 전 먼저 쓰기 지연 저장소의 insert 쿼리가 flush 된 후 select 동작
       TestEntity result = queryFactory
             .selectFrom(qTestEntity)
             .fetchOne();
       // fetch() : 리스트 조회, fetchOne() : 결과가 반드시 하나 (2개 이상이라면 NonUniqueResultException)

       // 하나의 트랜잭션 안에서는 JPA 영속성 컨텍스트에서 동일성(== 비교)을 보장한다. (1차 캐시의 주솟값을 같이 공유) 
       Assertions.assertThat(result).isEqualTo(t);
       Assertions.assertThat(result.getId()).isEqualTo(t.getId());
    }
}

 

TestEntity result = queryFactory

             .selectFrom(qTestEntity)
             .fetchOne();

 

Q 타입 엔티티 (qTestEntity) 를 이용하여 TestEntity 테이블의 데이터를 조회한다. 

fetch() 로 조회 시 List<TestEntity> 로 TestEntity 테이블의 모든 데이터를 조회하고, fetchOne() 으로 조회 시 반드시 결과값이 하나라는 것을 가정하고 조회한다.

 

// fetch() : 리스트 조회, 데이터가 없을 경우 빈 리스트 반환
List<Member> members = queryFactory
    .selectFrom(qMember)
    .fetch();

// fetchOne() : 단 건 조회
Member member = queryFactory
    .selectFrom(qMember)
    .fetch();

/* 
하나도 없을 경우 : null
1개일 경우 : 정상 조회 
2개 이상일 경우 : NonUniqueResultException 예외 발생
*/

// fetchFirst() : 단 건 조회, limit 조건 추가, limit(1).fetchOne()
Member memberFirst = queryFactory
    .selectFrom(qMember)
    .fetchFirst();

// fetchResults() : 페이징 정보 + 리스트 조회 (total count 쿼리를 추가적으로 실행 => 2번의 쿼리가 실행)
QueryResults<Member> results = queryFactory
    .selectFrom(qMember)
    .fetchResults();

// fetchCount() : count 쿼리
long count = queryFactory
    .selectFrom(qMember)
    .fetchCount();

                                                                                                                                                                                                                                   

 

 

 

QueryDSL은 JPQL의 빌더 역할을 수행하며 실행 시 JPQL로 변환되어 동작한다.

 

JPQL을 예시로 @Query("select m from member m where m.name = :name") 에서 m.name은 varchar 타입이지만 파라미터 name이 int 타입일 경우 타입 불일치 문제가 생기는데, JPQL은 String 문자열로 작성되기 때문에 컴파일 시점에서 타입 불일치 문제를 잡지 않고, 런타임 환경에서 실제 JPQL 쿼리를 실행할 때 런타임 오류가 발생한다.

 

QueryDSL은 JPQL에 비해 Q 타입 엔티티가 컴파일 시점에 타입 오류를 검증하기 때문에 타입 안정성이 높다는 장점을 가진다.

 

@BeforeEach

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    @Autowired
    EntityManager em;
    JPAQueryFactory queryFactory;

    @BeforeEach
    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(10, "member1", teamA);
        Member member2 = new Member(20, "member2", teamA);
        Member member3 = new Member(30, "member3", teamB);
        Member member4 = new Member(40, "member4", teamB);

        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);

        // 쓰기 지연 저장소에 insert 쿼리 저장 (team A, B, member 1, 2, 3, 4) 이후 flush
        }
        
        @Test
        void JPQL() { ... }
        
        @Test
        void queryDsl() { ... }
}

 

 

JPQL 동작 방식 : 문자열로 직접 작성, 오타 또는 타입 검증에 취약하다.

@Test
void JPQL() {
    // given
    String qlString = "select m from Member m where m.username = :username";

    // when
    Member findMember = em.createQuery(qlString, Member.class)
            .setParameter("username", "member1")
            .getSingleResult();

    // then
    Assertions.assertThat(findMember.getUsername()).isEqualTo("member1");
}

 

 

QueryDSL 동작 방식 : 빌더 방식, 런타임에 JPQL로 변경되어 실행된다, 타입 안정성, 오타 대비 가능

@Test
void queryDsl() {
    // given
    QMember qMember = QMember.member; 
    // 기본 인스턴스 사용 방식
    // 셀프 조인이 필요하지 않다면 기본 인스턴스 방식을 사용하는 것을 권장
    // QMember qMember = new QMember("m") 처럼 alias를 직접 지정해서 사용할 수도 있다.

    // when
    Member findMember = queryFactory
            .select(qMember)
            .from(qMember) // selectFrom(qMember) 로 합칠 수도 있다.
            .where(qMember.username.eq("member1")
                    .and(qMember.age.eq(10))) // .and(), .or() 로 메서드 체이닝 가능
            .fetchOne(); // 결과값이 반드시 하나일 때 fetchOne() 사용

    // then
    Assertions.assertThat(findMember.getUsername()).isEqualTo("member1");
}

 

.where(...) , ... : and 조건과 동일 (,)

@Test
void searchByQueryDsl() {
    QMember q = QMember.member;
    List<Member> members = queryFactory
            .selectFrom(q)
            .where(q.username.eq("member1"), q.age.eq(10))
            // .where(조건1, 조건 2); 로 and 생략 가능
            .fetch();
}

 

 

QueryDSL에서의 where 조건 처리

.where(qMember.username.eq("member1") : where m.username = 'member1', eq : SQL의 = 로 치환
.where(qMember.username.ne("member1")
.where(qMember.username.eq("member1").not
 => where m.username != 'member1', ne & eq().not : SQL의 != 로 치환

.where(qMember.username.isNotNull()) : IS NOT NULL 조건

.where (qMember.age.in(10, 20))
.where (qMember.notIn(10, 20))
.where (age.between(10, 30))

.where (qMember.age.goe(30)) : age >= 30, greater or equal
.where (qMember.age.gt(30)) : age > 30, greater then
.where (qMember.age.loe(30)) : age <= 30, less or equal
.where (qMember.age.lt(30)) : age < 30, less then

.where (qMember.username.like("member%")
.where (qMember.username.contains("member") : LIKE '%member%'
.where (qMember.username.startWith("member") : LIKE 'member%'

 

 

QueryDSL에서의  조건절 - NULL일 경우 무시 (pass) : 동적 쿼리 작성에서 큰 장점

.where(조건 1), (조건2), (조건3) ...

// 조건 1이 true라면 where절에 추가
// 조건 2가 true라면 where절에 and로 추가
// 조건 3이 true라면 where절에 and로 추가

조건 1, 2, 3 중 false인 조건절이 있다면 where문에 false인 조건절은 추가하지 않는다.

 

 

 

QueryDSL에서의  정렬, 페이징

@Test
void sortAndPaging() {
    em.persist(new Member(100, null)); // username = null
    em.persist(new Member(100, "memberA")); // username = "memberA"
    em.persist(new Member(100, "memberB")); // username = "memberB"

    QMember qMember = QMember.member;

    // select 쿼리 전달 => 쓰기 지연 저장소의 insert가 먼저 flush된 뒤 select 쿼리 동작
    // 정렬
    List<Member> members = queryFactory
            .selectFrom(qMember)
            .where(qMember.age.eq(100))
            .orderBy(qMember.age.desc(), qMember.username.asc().nullsFirst())
            // ORDER BY m.age DESC, m.username ASC
            // .nullsLast() : 회원 이름이 null 이라면 마지막에 출력
            // .nullsFirst() : 회원 이름이 null 이라면 처음에 먼저 출력
            .fetch();
    Assertions.assertThat(members.get(2).getUsername()).isEqualTo("memberB");

    // 페이징
    // 데이터 자체만 조회하는 방식, 실무 권장
    List<Member> memberList = queryFactory
            .selectFrom(qMember)
            .orderBy(qMember.username.desc())
            .offset(1)  // 두 번째 데이터부터 시작 (인덱스 기준 1 = 실제 두 번째 데이터)
            .limit(2)   // 데이터 2개 조회
            .fetch();
    Assertions.assertThat(memberList.size()).isEqualTo(2);

    // 데이터 전체 개수 조회 쿼리
    long totalCount = queryFactory
            .select(qMember.count())
            .from(qMember)
            .where(qMember.age.eq(100))
            .fetchOne();
    Assertions.assertThat(totalCount).isEqualTo(3);

    // 데이터 & 데이터 전체 개수 함께 조회, 매 번 count 쿼리 비효율적으로 동작 => 권장하지 X
    QueryResults<Member> queryResults = queryFactory
            .selectFrom(qMember)
            .orderBy(qMember.username.desc())
            .offset(1)
            .limit(2)
            .fetchResults();
    Assertions.assertThat(queryResults.getResults().size()).isEqualTo(2);
}

 

 

QueryDSL에서의  집계 함수, GroupBy와 간단한 inner join

@Test
void aggregationAndGroupBy() throws Exception {
    QMember qMember = QMember.member;
    QTeam qTeam = QTeam.team;

    List<Tuple> aggregationResult = queryFactory
            .select(qMember.count(), // count 쿼리에서도 사용
                    qMember.age.sum(),
                    qMember.age.avg(),
                    qMember.age.max(),
                    qMember.age.min())
            .from(qMember)
            .fetch();

    // Tuple : 여러 다른 타입의 조회 결과를 하나로 담아내기 위한 QueryDSL 전용 객체, Long, Integer, Double ..
    Tuple tuple = aggregationResult.get(0);
    Assertions.assertThat(tuple.get(qMember.count())).isEqualTo(4);

    List<Tuple> groupByResult = queryFactory
            .select(qTeam.name, qMember.age.avg())
            .from(qMember)
            .join(qMember.team, qTeam) // inner join, qMember의 team 필드와 qTeam
            .groupBy(qTeam.name)
            .fetch();

    Tuple teamA = groupByResult.get(0);
    Tuple teamB = groupByResult.get(1);

    Assertions.assertThat(teamA.get(qTeam.name)).isEqualTo("teamA");
    Assertions.assertThat(teamB.get(qTeam.name)).isEqualTo("teamB");
}

 

 

 

QueryDSL에서의 Join (내부 조인, 외부 조인, 세타 조인, On 절, fetch Join)

@SpringBootTest
@Transactional
public class QuerydslJoinTest {
    @Autowired
    EntityManager em;
    JPAQueryFactory queryFactory;

    @BeforeEach
    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(10, "member1", teamA);
        Member member2 = new Member(20, "member2", teamA);
        Member member3 = new Member(30, "member3", teamB);
        Member member4 = new Member(40, "member4", teamB);

        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);

        // 쓰기 지연 저장소에 insert 쿼리 저장 (team A, B, member 1, 2, 3, 4), flush
    }

    // inner join
    // select m from member m join team t on m.team_id = t.id where t.name = 'teamA';
    @Test
    void innerJoin() throws Exception {
        QMember qMember = QMember.member;
        QTeam qTeam = QTeam.team;

        List<Member> result = queryFactory
                .selectFrom(qMember)
                .join(qMember.team, qTeam) // join (Q 타입 엔티티의 조인 대상 필드, join할 Q 타입 엔티티), 기본적으로 Team 필드는 가지고 오지 않는다 (n + 1 발생 가능)
                // .leftjoin, .rightjoin 사용 시 left, right join 동작
                .where(qTeam.name.eq("teamA"))
                .fetch();

        Assertions.assertThat(result).extracting("username").containsExactly("member1", "member2");
    }

    // 세타 조인 - SELECT m.* FROM member m, team t WHERE m.username = t.name;
    // 세타 조인 : 연관관계가 없는 데이터끼리 특정 조건으로 연관지어 join
    @Test
    void thetaJoin() throws Exception {
        em.persist(new Member("teamA"));
        em.persist(new Member("teamB"));

        QMember qMember = QMember.member;
        QTeam qTeam = QTeam.team;

        // on 절을 사용하지 않으면 세타 조인 시 외부 조인이 불가능하다.
        // join 조건 : 회원 이름이 팀 이름과 같은 회원 조회
        List<Member> result = queryFactory
                .select(qMember)
                .from(qMember, qTeam) // from 절에 Q 타입 엔티티를 2개 둔다.
                .where(qMember.username.eq(qTeam.name))
                .fetch();

        Assertions.assertThat(result)
                .extracting("username")
                .containsExactly("teamA", "teamB");
    }

    // SELECT m.*, t.* FROM member m LEFT OUTER JOIN team t
    // ON m.team_id = t.id AND t.name = 'teamA';
    // left outer join - member은 모두 조회, team 에만 조건
    @Test
    void on_filtering() throws Exception {
        QMember qMember = QMember.member;
        QTeam qTeam = QTeam.team;

        // join 조건 : 회원, 팀을 join 하며 team 이름이 teamA인 팀만 필터링하여 조회, 회원은 전부 조회
        List<Tuple> result = queryFactory
                .select(qMember, qTeam)
                .from(qMember)
                .leftJoin(qMember.team, qTeam)
                .on(qTeam.name.eq("teamA")) // on 절 : join 대상 필터링 가능, 연관 관계가 없는 엔티티에 대해 외부 조인 가능
                .fetch();

        for (Tuple t : result)
            System.out.println("tuple = " + t);
    }

    // SELECT m.*, t.* FROM member m LEFT OUTER JOIN team t
    // ON m.username = t.name
    // left outer join - member은 모두 조회, team 에만 조건
    @Test
    void on_no_relation() throws Exception {
        em.persist(new Member("teamA"));
        em.persist(new Member("teamB"));

        QMember qMember = QMember.member;
        QTeam qTeam = QTeam.team;

        // join 조건 : 연관관계가 없는 엔티티 외부 조인 (회원의 이름과 팀 이름이 같은 대상 외부 조인)
        // 세타 조인 - on 절을 쓰지 않고 where 절을 쓸 경우 내부 조인만 가능
        // 세타 조인 - 외부 조인이 필요할 경우 on 절 사용
        List<Tuple> result = queryFactory
                .select(qMember, qTeam)
                .from(qMember)
                .leftJoin(qTeam)
                .on(qMember.username.eq(qTeam.name)) // on 절 : 연관 관계가 없는 엔티티에 대해 외부 조인 가능
                .fetch();

        for (Tuple t : result)
            System.out.println("tuple = " + t);
    }

    @Autowired
    EntityManagerFactory emf;

    // Join 시 fetch join 으로 연관관계 필드도 즉시로딩
    @Test
    void fetchJoin() throws Exception {
        QMember qMember = QMember.member;
        QTeam qTeam = QTeam.team;

        em.flush();
        em.clear();
        // 영속성 컨텍스트 먼저 초기화

        Member findMember = queryFactory
                .selectFrom(qMember)
                // .join(qMember.team, qTeam).fetchJoin() : 생략 시 내부 조인, n + 1 지연 로딩
                .join(qMember.team, qTeam).fetchJoin() // join 절 추가 시 fetch join, 즉시 로딩
                .where(qMember.username.eq("member1"))
                .fetchOne();

        boolean isLoaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
        Assertions.assertThat(isLoaded).as("fetch join success").isTrue();
    }
}

 

 

QueryDSL에서의 서브쿼리 (where절, select절) // from절 서브쿼리(인라인뷰)는 지원하지 않는다. (JPQL 자체가 지원 x)

@SpringBootTest
@Transactional
public class QuerydslSubQueryTest {
    @Autowired
    EntityManager em;
    JPAQueryFactory queryFactory;

    @BeforeEach
    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(10, "member1", teamA);
        Member member2 = new Member(20, "member2", teamA);
        Member member3 = new Member(30, "member3", teamB);
        Member member4 = new Member(40, "member4", teamB);

        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);

        // 쓰기 지연 저장소에 insert 쿼리 저장 (team A, B, member 1, 2, 3, 4), flush
    }

    // 서브 쿼리 : JPAExpressions 사용
    @Test
    void subQuery() throws Exception {
        QMember qMember = QMember.member;
        QTeam qTeam = QTeam.team;

        QMember qMemberSub = new QMember("memberSub");

        // age 가 가장 많은 회원 조회
        // select m.* from Member m where m.age = (select Max(age) from Member)
        List<Member> result = queryFactory
                .selectFrom(qMember)
                .where(qMember.age.eq(  // qMember.age.goe( ... ) 도 가능,  goe : great or equal >=
                        JPAExpressions
                        .select(qMemberSub.age.max())
                        .from(qMemberSub))
                        // 서브 쿼리로 나이가 가장 많은 회원의 나이 조회
                )
                .fetch();

        Assertions.assertThat(result).extracting("age").containsExactly(40);
    }

    // 서브 쿼리 : IN 절 사용
    @Test
    void subQuery_IN() throws Exception {
        QMember qMember = QMember.member;
        QTeam qTeam = QTeam.team;

        QMember qMemberSub = new QMember("memberSub");

        // age 가 가장 많은 회원 조회
        // select m.* from Member m where m.age in (select age from Member where age > 10)
        List<Member> result = queryFactory
                .selectFrom(qMember)
                .where(qMember.age.in(  // in 조건절 - 서브쿼리에서 10보다 큰 나이를 모두 조회
                                JPAExpressions
                                    .select(qMemberSub.age)
                                    .from(qMemberSub)
                                    .where(qMemberSub.age.gt(10))
                ))
                .fetch();

        Assertions.assertThat(result).extracting("age").containsExactly(20, 30, 40);
    }

    // select 절에 사용하는 서브쿼리
    @Test
    void subQuery_Select() throws Exception {
        QMember qMember = QMember.member;
        QTeam qTeam = QTeam.team;

        QMember qMemberSub = new QMember("memberSub");

       // select m.username, (select avg(age) from Member) from Member m
        List<Tuple> result = queryFactory
                .select(qMember.username, JPAExpressions.select(qMemberSub.age.avg()).from(qMemberSub))
                .from(qMember)
                .fetch();

        for (Tuple t : result) {
            System.out.println("username = " + t.get(qMember.username));
            System.out.println("age = " + t.get(JPAExpressions.select(qMemberSub.age.avg()).from(qMemberSub)));
        }
    }
}

 

 

QueryDSL - Repository에서 DTO 바로 조회 : Projections.constructor

List<MemberDto> result = queryFactory
    .select(Projections.constructor(MemberDto.class, qMember.username, qMember.age))
    .from(qMember)
    .fetch();
    
@Data
@AllArgsConstructor
@NoArgsConstructor
class MemberDto {
    private String username;
    private int age;
}

 

DB에서 데이터 조회 후 DTO로 리턴하는 방법

- 일반 데이터 조회 후 DTO 객체로 조립 (단일 타입) : List<String> result = queryFactory.select(qMember.username) .. 

- 일반 데이터 조회 후 DTO 객체로 조립(복수 타입) : List<Tuple> result = ueryFactory.select(qMember.username, qMember.age) .. 

- DTO 객체로 바로 조회 : Projections.constructor, Projections.bean, Projections.field 3가지 방식이 있다.

그러나 Projections.constructor 같은 방식은 실제 코드가 동작하는 시점에서 런타임 오류가 발생할 수 있기에 좋은 코드는 아니다.

실무에서는 DTO를 바로 조회 시 @QueryProjection 사용

 

@Data
public class MemberDto {
    private String username;
    private int age;

    @QueryProjection
    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}


// 사용
List<MemberDto> result = queryFactory
    .select(new QMemberDto(qMemberDto.username, qMemberDto.age))
    .from(qMember)
    .fetch();

 

./gradlew clean compileJava 명령어 터미널 입력 : QMemberDto 가 빌드 결과로 생성 (Dto에 대한 Q 타입 엔티티가 생성된다)

Projections.constructor 방식들처럼 런타임에 오류가 발생하지 않고 컴파일 시점에 오류 발생 - 안정적 
DTO가 QueryDSL annotation 의존, DTO Q 타입 엔티티까지 Q 파일을 생성한다는 단점이 있다.

DTO Q 타입 엔티티

 

 

QueryDSL - 동적 쿼리 (Boolean Builder, Where 다중 파라미터)

 

1. Boolean Builder : where 절에 and, or 조건을 동적으로 추가 가능

@Test
void 동적쿼리_BooleanBuilder() throws Exception {
    QMember qMember = QMember.member;

    // select 조건 (condition)
    String usernameCond = "member1";
    int ageCond = 10;

    List<Member> result = searchMemberByBooleanBuilder(usernameCond, ageCond, qMember); // member1, 10인 회원 조회
    Assertions.assertThat(result.size()).isEqualTo(1);
}

private List<Member> searchMemberByBooleanBuilder(String usernameCond, Integer ageCond, QMember qMember) {
    BooleanBuilder booleanBuilder = new BooleanBuilder();
    if (usernameCond != null) booleanBuilder.and(qMember.username.eq(usernameCond)); // and qMember.username = usernameCond 조건 추가
    if (ageCond != null) booleanBuilder.and(qMember.age.eq(ageCond)); // and qMember.age = ageCond 조건 추가

    return queryFactory.selectFrom(qMember)
            .where(booleanBuilder)
            .fetch();
}

 

 

2. Where 다중 파라미터 (실무 권장)

private List<Member> searchMemberByWhere(String usernameCond, int ageCond, QMember qMember) {

    // BooleanExpression : Java 코드로 쓴 SQL의 WHERE 절 조건 
    BooleanExpression userNameEq = usernameCond != null ? qMember.username.eq(usernameCond) : null;
    BooleanExpression ageEq = usernameCond != null ? qMember.age.eq(ageCond) : null;

    return queryFactory.selectFrom(qMember)
            .where(userNameEq, ageEq) // where 조건에 null이 있다면 무시
            .fetch();

}

 

 

 

QueryDSL - 수정, 삭제 벌크 연산 

- 영속성 컨텍스트 (1차 캐시)를 거치지 않고 DB에 직접 쿼리를 전달하는 방식 (where id In (1, 2, 3 ... ))

- 단 하나의 쿼리로 수 만 건의 데이터를 동시에 수정 => 변경 감지로 여러 번 update 쿼리를 DB에 전달하는 것보다 빠르다.

- 벌크 연산은 영속성 컨텍스트를 거치지 않기 때문에 벌크 연산 수행 후 바로 em.flush(), em.clear()를 호출해서 영속성 컨텍스트를 비워야한다.

 

long count = queryFactory
    .update(qMember)
    .set(qMember.username, "비회원")
    .where(qMember.age.lt(28)) // lt(28) : less then, 28살 미만일 경우 비회원으로 변경
    .execute(); // fetch가 아니라 벌크 연산 시에는 execute
    
long count = queryFactory
    .delete(qMember)
    .where(qMember.age.gt(18))
    .execute();

 

.set(qMember.username, "비회원")

.set(qMember.age, qMember.age.add(1)) : age에 1을 더한 값으로 update

.set(qMember.age, qMember.age.multiply(2)) : age에 2를 곱한 값으로 update

 

'Java & Spring' 카테고리의 다른 글

[Spring] JPA 6  (0) 2026.03.16
[Java] Map 정렬  (0) 2026.03.03
[Spring] ResponseEntity  (0) 2026.02.22
[Servlet] WebSocket  (0) 2026.02.16
[Spring] Security  (0) 2026.02.15