[Java/JPA] JPQL - 기본 문법과 쿼리 API

daheenamic·2025년 12월 1일

Java

목록 보기
43/48

JPQL (Java Persistence Query Language)

JPQL은 JPA가 제공하는 객체 지향 쿼리 언어이다. SQL과 비슷하지만 결정적인 차이가 있다.

  • JPQL: 엔티티 객체를 대상으로 쿼리
  • SQL: 데이터베이스 테이블을 대상으로 쿼리

JPQL은 SQL을 추상화했기 때문에 특정 데이터베이스에 종속되지 않는다. MySQL을 쓰든 Oracle을 쓰든 같은 JPQL로 작성할 수 있고, JPA가 각 데이터베이스에 맞는 SQL로 변환해준다.


JPQL 기본 문법

JPQL은 SQL과 유사한 구조를 가진다

select_문 ::= 
    select_절
    from_절
    [where_절]
    [groupby_절]
    [having_절]
    [orderby_절]

update_문 ::= update_절 [where_절]
delete_문 ::= delete_절 [where_절]

기본적인 조회뿐만 아니라 UPDATE, DELETE도 지원한다.
여러 건을 한 번에 수정하거나 삭제하는 벌크 연산도 가능하다.

문법 규칙

select m from Member as m where m.age > 18
  • 엔티티와 속성은 대소문자 구분: Member, age (정확히 써야 함)
  • JPQL 키워드는 대소문자 구분 안함: SELECT, select, SeLeCt 다 가능
  • 엔티티 이름 사용: 테이블 이름이 아닌 엔티티 클래스 이름 (Member)
  • 별칭은 필수: m (as는 생략 가능)

집합 함수와 정렬

SQL처럼 집합 함수도 사용할 수 있다.

select
    COUNT(m),      // 회원수
    SUM(m.age),    // 나이 합
    AVG(m.age),    // 평균 나이
    MAX(m.age),    // 최대 나이
    MIN(m.age)     // 최소 나이
from Member m

GROUP BY, HAVING, ORDER BY도 당연히 지원된다.
SQL의 집계 쿼리를 거의 그대로 사용할 수 있다고 보면 된다.


TypedQuery vs Query

JPQL로 쿼리를 작성할 때 반환 타입에 따라 두 가지 방식을 사용할 수 있다.

TypedQuery - 반환 타입이 명확할 때

// Member 엔티티 전체 조회
TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class);

// String 타입 단일 컬럼 조회
TypedQuery<String> query2 = em.createQuery("select m.username from Member m", String.class);

반환 타입을 제네릭으로 지정하기 때문에 타입 캐스팅이 필요 없고, 컴파일 시점에 타입 체크가 가능하다.

Query - 반환 타입이 명확하지 않을 때

// 여러 컬럼을 조회하는 경우
Query query3 = em.createQuery("select m.username, m.age from Member m");

여러 컬럼을 조회하거나, 타입이 혼합되어 있을 때는 Query를 사용한다. 다만 실무에서는 이런 경우 DTO로 직접 조회하는 방식을 더 많이 쓴다.


결과 조회 API

JPQL 쿼리를 실행한 후 결과를 받아오는 방법은 두 가지다.

getResultList() - 결과가 여러 건

TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
List<Member> resultList = query.getResultList();

for (Member member : resultList) {
    System.out.println("member = " + member);
}
  • 결과가 하나 이상일 때 사용
  • 결과가 없으면 빈 리스트 반환 (예외 발생 안 함)

getSingleResult() - 결과가 정확히 하나

TypedQuery<Member> query = em.createQuery(
    "select m from Member m where m.id = 10L", 
    Member.class
);

Member result = query.getSingleResult();
System.out.println("result: " + result);
  • 결과가 정확히 1개일 때만 사용
  • 결과가 없으면: NoResultException 발생
  • 결과가 2개 이상이면: NonUniqueResultException 발생

주의사항

getSingleResult()는 예외 처리가 까다롭다. NPE(NullPointerException)를 걱정해야 하고, 결과가 없거나 여러 건이면 무조건 예외가 터진다. 그래서 진짜 결과가 1개인 게 확실할 때만 써야 한다.

Spring Data JPA에서는 이 문제를 개선했다. Optional로 감싸서 반환하기 때문에 예외가 발생하지 않는다. 순수 JPA를 쓸 때와 Spring Data JPA를 쓸 때의 동작이 다르니 헷갈리지 않도록 주의해야 한다.


파라미터 바인딩

동적으로 조건값을 넣어야 할 때는 파라미터 바인딩을 사용한다. 두 가지 방식이 있다.

이름 기준 파라미터 (권장)

Member result = em.createQuery(
    "select m from Member m where m.username = :username", 
    Member.class
)
.setParameter("username", "member1")
.getSingleResult();
  • :파라미터명 형식으로 사용
  • 메서드 체이닝 방식으로 깔끔하게 작성 가능
  • 실무에서는 거의 이 방식만 사용

위치 기준 파라미터 (비권장)

Member result = em.createQuery(
    "select m from Member m where m.username = ?1", 
    Member.class
)
.setParameter(1, "member1")
.getSingleResult();
  • ?1, ?2 형식으로 위치 지정
  • 실무에서는 사용하지 않는 게 좋다.

왜 위치 기준을 쓰면 안되는가?

중간에 파라미터가 하나 추가되면 모든 위치가 밀린다. 예를 들어

// 원래 쿼리
"select m from Member m where m.username = ?1 and m.age = ?2"

// 중간에 조건 하나 추가
"select m from Member m where m.team = ?1 and m.username = ?2 and m.age = ?3"

username?1에서 ?2로, age?2에서 ?3으로 밀렸다. 이런 식으로 순서가 꼬이면 버그가 발생하고, 심하면 장애로 이어진다.

반면 이름 기준 파라미터는 위치가 바뀌어도 문제없다. 파라미터 이름만 맞으면 되기 때문이다. 그래서 실무에서는 이름 기준만 사용한다.

실무 코드 스타일

보통은 TypedQuery 변수를 따로 선언하지 않고 메서드 체이닝으로 바로 작성한다

// 변수 선언 방식 (잘 안 씀)
TypedQuery<Member> query = em.createQuery(
    "select m from Member m where m.username = :username", 
    Member.class
);
query.setParameter("username", "member1");
Member result = query.getSingleResult();

// 메서드 체이닝 방식 (실무 스타일)
Member result = em.createQuery(
    "select m from Member m where m.username = :username", 
    Member.class
)
.setParameter("username", "member1")
.getSingleResult();

코드가 훨씬 간결하고 가독성도 좋다.


정리

  • JPQL은 엔티티 객체를 대상으로 쿼리를 작성한다
  • TypedQuery는 반환 타입이 명확할 때, Query는 불명확할 때 사용
  • getResultList()는 여러 건, getSingleResult()는 정확히 1건일 때만 사용
  • 파라미터 바인딩은 반드시 이름 기준으로 작성 (위치 기준은 장애 위험)
  • Spring Data JPA는 Optional로 예외를 감싸주지만, 순수 JPA는 예외가 그대로 터진다

다음 포스팅에서는 프로젝션, 페이징, 조인 같은 JPQL의 고급 기능을 배울 예정이다.

0개의 댓글