개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

공부 › JPA › 쿼리

동적 쿼리와 Querydsl

검색 조건이 있을 때만 where에 붙이려면

회원 검색 화면에서 이름과 나이는 입력할 수도 있고 비워 둘 수도 있습니다. 입력된 조건만 where에 넣어야 하므로 쿼리의 모양이 요청마다 달라집니다. 이런 쿼리를 동적 쿼리라고 합니다.

일부만 실행으로 확인 · 문자열 방식과 Criteria API의 열여섯 조합은 Hibernate 7.4.5와 H2에서 실행해 대조. Querydsl의 네 조합은 라이브러리가 없어 공식 문서 기준

이 실습에 나오는 개념

동적 쿼리
실행할 때의 입력에 따라 문장의 모양이 달라지는 쿼리다. 검색 화면처럼 입력된 조건만 where에 넣어야 할 때 필요하다. 문자열을 이어 붙여 만들면 조건이 하나도 없을 때 where만 남거나, and가 엉뚱한 자리에 가거나, 띄어쓰기가 빠지는 실수가 생기고, 이런 오류는 컴파일이 아니라 그 조합의 요청이 들어왔을 때에야 드러난다. 그래서 조건을 객체로 조립하는 Criteria API나 Querydsl을 쓴다. 어떤 방식이든 값은 문자열에 직접 넣지 않고 파라미터로 바인딩한다.
JPQL
테이블이 아니라 엔티티와 그 필드를 대상으로 작성하는 JPA의 쿼리 언어다. SQL과 문법이 비슷하며, 실행할 때 JPA 구현체가 사용 중인 데이터베이스에 맞는 SQL로 변환한다. 특정 데이터베이스의 문법에 의존하지 않는다는 장점이 있다.
Criteria API
JPA 표준에 들어 있는, 쿼리를 문자열이 아니라 자바 객체로 조립하는 API다. CriteriaBuilder로 조건 객체(Predicate)를 만들어 목록에 모은 뒤 where에 넘기면, 조건의 개수에 맞게 where와 and가 만들어진다. 조건이 0개이면 where 절이 생기지 않는다. 별도 라이브러리 없이 쓸 수 있지만 코드가 길어 어떤 쿼리인지 읽기 어렵다. 프로퍼티 이름을 문자열로 적으면 이름이 틀려도 컴파일은 되고 실행 중에 예외가 난다.
Querydsl
쿼리를 자바 코드로 조립하게 해 주는 라이브러리다. 엔티티 클래스를 보고 Q클래스(예: QMember)를 미리 만들어 두고, m.name.eq("김철수")처럼 프로퍼티와 조건을 메서드로 적는다. 프로퍼티 이름이나 타입이 틀리면 컴파일 오류가 나므로 실행 전에 잘못을 찾을 수 있다. where에 여러 조건을 넘기면 and로 묶이고 null인 조건은 건너뛰므로, 값이 없을 때 null을 돌려주는 조건 메서드를 만들어 동적 쿼리를 짧게 쓸 수 있다. BooleanBuilder에 조건을 하나씩 쌓는 방법도 있다. JPA 표준이 아니므로 의존성과 Q클래스를 만드는 빌드 설정을 따로 추가해야 한다.
SQL Injection
사용자 입력이 SQL 문의 일부로 해석되어 공격자가 의도한 쿼리가 실행되는 공격이다. 입력값을 문자열로 이어 붙여 SQL을 만들 때 발생한다. SQL 구조와 값을 분리하는 바인드 파라미터(PreparedStatement)를 쓰면 입력이 항상 값으로만 처리되어 막을 수 있다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. 검색 조건: 이름은 입력하지 않아 null, 나이는 20입니다. null이 아닌 조건만 쿼리의 where에 넣어야 합니다.
  2. 1번째 줄: 기본 문장을 만듭니다. 조건이 붙을 것이라 보고 끝에 where를 미리 붙여 두었습니다.
  3. 2번째 줄: 이름 조건이 null이므로 건너뜁니다. 문자열은 그대로입니다.
  4. 3번째 줄: 나이 조건이 있으므로 뒤에 " and m.age >= :age" 조각을 붙입니다. 이 줄은 앞에 이름 조건이 이미 붙어 있다고 보고 and로 시작합니다. 그런데 이번에는 이름 조건이 붙지 않았습니다.
  5. em.createQuery(jpql, Member.class): Hibernate가 문자열을 처음부터 읽어 문법을 검사합니다. where 바로 뒤에 and가 왔습니다. and 앞에 있어야 할 첫 조건이 없습니다. Hibernate의 문법에서는 and 같은 낱말도 이름으로 쓸 수 있어서 and까지는 이름으로 읽고 넘어가고, 그다음 낱말 m에서 멈춥니다.
  6. 예외: createQuery가 IllegalArgumentException을 던집니다(원인은 Hibernate의 SyntaxException). 메시지에 나오는 "At 1:33"이라는 글자는 1번째 줄의 33번 글자(0부터 셈)에서 멈췄다는 뜻입니다. SQL은 DB로 나가지 않았고, 검색 요청은 실패합니다.

실행 결과

코드

List<Member> search(String name, Integer age) {
    String jpql = "select m from Member m where";
    if (name != null) jpql += " m.name = :name";
    if (age != null)  jpql += " and m.age >= :age";

    TypedQuery<Member> query = em.createQuery(jpql, Member.class);
    if (name != null) query.setParameter("name", name);
    if (age != null)  query.setParameter("age", age);
    return query.getResultList();
}

search(null, 20);   // 이름은 입력 안 함, 나이는 20 이상

이 모형의 전제

문자열을 이어 붙이는 세 가지 방식과 JPA Criteria API 방식의 열여섯 가지 조합은 이 PC의 Hibernate 7.4.5와 H2 데이터베이스에서 화면과 같은 구조의 코드를 실제로 실행해, 조립된 문자열, 성공 여부, 예외의 종류와 오류 위치와 걸린 낱말, DB로 나간 SQL의 where, 조회된 회원 수를 화면과 대조했습니다. Querydsl의 네 가지 조합은 이 PC에 Querydsl 라이브러리가 없어 실행으로 확인하지 못했습니다. 그 부분은 Querydsl 공식 문서를 따른 설명입니다: FilteredClause.where(Predicate...)의 API 문서("Adds the given filter conditions. Skips null arguments"), 참조 문서 Querying JPA 장의 여러 필터를 and로 묶는 예, Complex predicates 절의 BooleanBuilder 설명. Querydsl의 결과 칸에 적힌 where와 회원 수는 문서대로 동작한다고 볼 때의 값이며 실행 결과가 아닙니다. 문법 오류의 위치와 걸린 낱말은 Hibernate 7.4.5가 낸 메시지 기준이고, 다른 버전이나 다른 JPA 구현에서는 다르게 나올 수 있습니다. "where and"에서 Hibernate가 and를 이름으로 읽고 넘어간다는 설명은, 오류가 and가 아니라 그다음 낱말에서 났다는 실행 결과에 근거한 해석입니다. 화면의 SQL where는 테이블 별칭을 걷어 내고 읽기 쉽게 줄인 것입니다. Criteria API에서 조건이 0개일 때 where 절이 생기지 않는다는 것, m.get에 없는 프로퍼티 이름을 주면 컴파일은 되고 실행 중에 예외(Hibernate 7.4.5에서는 PathElementException)가 난다는 것도 실행으로 확인했습니다. 실제 서비스에서는 조건이 훨씬 많고 or, like, 정렬이 섞이므로 문자열 방식의 실수가 여기보다 생기기 쉽습니다.

앞 실습: 스프링 데이터 JPA · 다음 실습: 커넥션 풀과 OSIV

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요