[Spring] JPQL

배창민·2025년 10월 27일
post-thumbnail

JPQL

JPA를 엔티티 중심으로 사용하면서, 복잡한 조회·집계·조인을 DB 벤더에 독립적으로 작성하게 해 주는 객체지향 쿼리 언어. EntityManager가 JPQL → SQL로 변환해 DB에 항상 질의(find()와 달리 1차 캐시 우선 조회 X).


1) 기본 문법과 실행 흐름

  • 키워드: SELECT / UPDATE / DELETE (INSERT는 persist() 사용)

  • 별칭 필수: SELECT m FROM Menu m

  • 대소문자: 키워드 무관, 엔티티/필드명은 구분

  • 실행 절차

    1. createQuery(String jpql, Class<T> type)TypedQuery<T>

    2. getSingleResult() 혹은 getResultList()로 실행

      • getSingleResult()는 없거나 여러 건이면 예외
        (NoResultException, NonUniqueResultException)
      • getResultList()는 결과 없으면 빈 컬렉션
// 단일 컬럼 (TypedQuery)
String jpql = "SELECT m.menuName FROM Section01Menu m WHERE m.menuCode = 8";
String name = em.createQuery(jpql, String.class).getSingleResult();

// 단일 행 엔티티
Menu menu = em.createQuery("SELECT m FROM Section01Menu m WHERE m.menuCode=8", Menu.class)
              .getSingleResult();

2) 파라미터 바인딩

문자열 더하기 대신 바인딩으로 안전하게.

  • 이름 기준: :name
  • 위치 기준: ?1 (1부터 시작)
em.createQuery("SELECT m FROM Section02Menu m WHERE m.menuName = :name", Menu.class)
  .setParameter("name", "한우딸기국밥")
  .getResultList();

em.createQuery("SELECT m FROM Section02Menu m WHERE m.menuName = ?1", Menu.class)
  .setParameter(1, "한우딸기국밥")
  .getResultList();

3) 프로젝션(SELECT 대상)

  1. 엔티티: 영속성 컨텍스트가 관리 → 조회 후 변경 시 자동 반영
  2. 임베디드(값 타입): 관리 대상 아님
  3. 스칼라(기본 타입): 관리 대상 아님
  4. DTO(new 명령어): 바로 DTO로 매핑
// 엔티티
List<Menu> menus = em.createQuery("SELECT m FROM Section03Menu m", Menu.class).getResultList();

// 임베디드
List<MenuInfo> infos = em.createQuery("SELECT m.menuInfo FROM EmbeddedMenu m", MenuInfo.class)
                         .getResultList();

// 스칼라
List<String> names = em.createQuery("SELECT c.categoryName FROM Section03Category c", String.class)
                       .getResultList();

// DTO
List<CategoryInfo> dto = em.createQuery(
  "SELECT new com.example.CategoryInfo(c.categoryCode, c.categoryName) " +
  "FROM Section03Category c", CategoryInfo.class
).getResultList();

4) 페이징

DBMS별 페이징을 JPA가 추상화. 정렬 필수로 일관된 결과 보장.

List<Menu> page = em.createQuery(
    "SELECT m FROM Section04Menu m ORDER BY m.menuCode DESC", Menu.class)
  .setFirstResult(10)  // offset (0부터)
  .setMaxResults(5)    // limit
  .getResultList();

5) 그룹 함수·집계

  • 지원: COUNT / SUM / AVG / MAX / MIN
  • 반환 타입: COUNT→Long, 그 외는 보통 Long/Double
  • 결과 없음: COUNT는 0, 그 외는 null (언박싱 주의)
Long cnt = em.createQuery(
  "SELECT COUNT(m.menuPrice) FROM Section05Menu m WHERE m.categoryCode=:c", Long.class)
  .setParameter("c", 4)
  .getSingleResult();

List<Object[]> rows = em.createQuery(
  "SELECT m.categoryCode, SUM(m.menuPrice) " +
  "FROM Section05Menu m GROUP BY m.categoryCode HAVING SUM(m.menuPrice) >= :min")
  .setParameter("min", 50_000L)
  .getResultList();

6) 조인

6-1. 일반 조인

  • 내부조인: JOIN
  • 외부조인: LEFT JOIN / RIGHT JOIN
  • 컬렉션 조인: c.menuList 처럼 컬렉션에 조인
  • 세타 조인: FROM A a, B b (비권장, 조건 누락 주의)
// 내부조인
List<Menu> list = em.createQuery(
  "SELECT m FROM Section06Menu m JOIN m.category c", Menu.class).getResultList();

// 외부조인 (스칼라)
List<Object[]> rows = em.createQuery(
  "SELECT m.menuName, c.categoryName FROM Section06Menu m RIGHT JOIN m.category c " +
  "ORDER BY m.category.categoryCode").getResultList();

// 컬렉션 조인
List<Object[]> rows2 = em.createQuery(
  "SELECT m.menuName, c.categoryName FROM Section06Category c LEFT JOIN c.menuList m")
  .getResultList();

6-2. 페치 조인 (성능 최적화)

연관 엔티티/컬렉션을 한 번에 로딩. 지연 로딩 대신 즉시 로딩처럼 동작.

List<Menu> list = em.createQuery(
  "SELECT m FROM Section06Menu m JOIN FETCH m.category", Menu.class)
  .getResultList();

주의

  • 컬렉션 페치 조인과 페이징 동시 사용 불가
  • 중복 로우가 생길 수 있으니 필요 시 SELECT DISTINCT로 엔티티 중복 제거

7) 서브쿼리

WHERE / HAVING에서만 사용 가능(SELECT/FROM 불가).

List<Menu> list = em.createQuery(
  "SELECT m FROM Section07Menu m " +
  "WHERE m.categoryCode = (" +
    "SELECT c.categoryCode FROM Section07Category c WHERE c.categoryName = :name" +
  ")", Menu.class).setParameter("name", "한식").getResultList();

8) NamedQuery

미리 정의된 정적 쿼리. 애플리케이션 기동 시 파싱되어 안전하고 재사용성 높음.

8-1. 애노테이션 기반

@NamedQueries({
  @NamedQuery(name="Section08Menu.selectMenuList",
              query="SELECT m FROM Section08Menu m")
})
// 사용
List<Menu> list = em.createNamedQuery("Section08Menu.selectMenuList", Menu.class)
                    .getResultList();

8-2. XML 기반

spring:
  jpa:
    mapping-resources:
      - section08/namedquery/menu-query.xml
<named-query name="Section08Menu.selectMenuByCode">
  <query>
    SELECT m FROM Section08Menu m WHERE m.menuCode = :menuCode
  </query>
</named-query>
Menu m = em.createNamedQuery("Section08Menu.selectMenuByCode", Menu.class)
           .setParameter("menuCode", 20)
           .getSingleResult();

9) 실전 팁과 주의사항

  • 엔티티명/필드명을 사용한다(테이블/컬럼명 X).
  • getSingleResult() 예외 처리 습관화.
  • 페이징은 정렬 필수, setFirstResult(0) 기준.
  • 그룹 함수 결과의 null/Long/Double 타입 주의.
  • N+1 피하려면: 페치 조인, 엔티티그래프, 배치 사이즈 등 고려.
  • 컬렉션 페치 조인 + 페이징 금지. 필요하면 DTO 조회로 전환.
  • 동적 쿼리는 Criteria / QueryDSL / MyBatis와 병행 검토.
  • LIKE '%…%'는 인덱스 활용이 어려움. 접두 매칭이나 검색 전용 저장소 고려.

10) 핵심 요약

  • 비교/조건: =, <, >, <=, >=, <>, BETWEEN, IN, LIKE, IS NULL
  • 논리: AND, OR, NOT
  • 정렬: ORDER BY a.prop ASC|DESC
  • 그룹: GROUP BY, HAVING
  • 조인: JOIN, LEFT JOIN, JOIN FETCH
  • 함수: COUNT, SUM, AVG, MAX, MIN, DISTINCT
profile
개발자 희망자

0개의 댓글