말귀 알아듣기.zip

[JPA] JPQL 문법 완전정복 - 실전 예제 30가지 총정리

zippy 2026. 4. 19. 01:09
반응형

📘 JPQL 실전 예제 30선: 문법 완전 정복 가이드

JPQL(Java Persistence Query Language)은 JPA에서 데이터를 조회하기 위한 객체지향 쿼리 언어입니다. SQL과 유사하지만 테이블이 아닌 엔티티(Entity)와 속성(Field)을 대상으로 동작하는 것이 핵심 차이점입니다. 이 글에서는 실무에서 바로 사용할 수 있는 30가지 실전 예제를 통해 JPQL 문법을 완전히 정복해 봅니다.


📑 목차

  1. JPQL 기본 문법 (SQL과 비교)
  2. 기본 SELECT 쿼리 (예제 1~10)
  3. WHERE 조건 쿼리 (예제 11~20)
  4. JOIN 및 관계 쿼리 (예제 21~25)
  5. 정렬 및 그룹 쿼리 (예제 26~30)
  6. JPQL vs Spring Data JPA 메서드 비교
  7. 마치며: JPQL 핵심 정리

1. JPQL 기본 문법 (SQL과 비교)

JPQL은 SQL과 비슷하지만 엔티티와 속성을 대상으로 합니다. 아래 표에서 주요 차이점을 확인하세요.

JPQL 특징 SQL JPQL 설명
테이블 → 엔티티 user User u 테이블명 대신 엔티티 클래스명 사용
컬럼 → 속성 name u.name 컬럼명 대신 엔티티 필드명 사용
별칭 user u User u AS 생략 가능, 필수는 아님
와일드카드 SELECT * SELECT u * 대신 별칭 사용
조인 JOIN dept JOIN u.department 객체 관계를 따라 조인

2. 기본 SELECT 쿼리 (예제 1~10)

📌 예제 1: 전체 조회

상황: 모든 사용자를 조회하고 싶을 때

🔍 JPQL: SELECT u FROM User u

@Query("SELECT u FROM User u")
List<User> findAllUsers();
  • ⚙️ SQL 변환: SELEC * ROM user
  • 결과: 모든 User 엔티티 리스트
  • 🚀 언제 사용: 관리자 화면에서 전체 회원 목록을 보여줄 때

📌 예제 2: 특정 컬럼만 조회

상황: 이름과 이메일만 가져오고 싶을 때

🔍 JPQL: SELECT u.name, u.email FROM User u

@Query("SELECT u.name, u.email FROM User u")
List<Object[]> findNamesAndEmails();
  • ⚙️ SQL 변환: SELECT name, email FROM user
  • 결과: Object[] 리스트, [0]=name, [1]=email
  • 🚀 언제 사용: 통계 화면에서 최소한의 데이터만 빠르게 조회할 때

📌 예제 3: NEW DTO로 조회

상황: DTO로 바로 변환해서 받고 싶을 때

🔍 JPQL: SELECT NEW com.example.UserDto(u.id, u.name, u.email) FROM User u

@Query("SELECT NEW com.example.UserDto(u.id, u.name, u.email) FROM User u")
List<UserDto> findUserDtos();
  • ⚙️ SQL 변환: SELECT id, name, email FROM user
  • 결과: UserDto 리스트
  • 🚀 언제 사용: API 응답용 DTO를 JPQL에서 바로 생성할 때

📌 예제 4: DISTINCT로 중복 제거

상황: 중복된 이메일을 제거하고 싶을 때

🔍 JPQL: SELECT DISTINCT u.email FROM User u

@Query("SELECT DISTINCT u.email FROM User u")
List<String> findDistinctEmails();
  • ⚙️ SQL 변환: SELECT DISTINCT email FROM user
  • 결과: 중복 없는 이메일 문자열 리스트
  • 🚀 언제 사용: 중복 데이터 제거 후 통계 분석할 때

📌 예제 5: TOP/LIMIT (페이징)

상황: 최근 가입한 5명만 조회하고 싶을 때

🔍 JPQL: SELECT u FROM User u ORDER BY u.createdDate DESC

@Query("SELECT u FROM User u ORDER BY u.createdDate DESC")
List<User> findRecentUsers(Pageable pageable);

// 사용: findRecentUsers(PageRequest.of(0, 5))
  • ⚙️ SQL 변환: SELEC * ROM user ORDER BY created_date DESC LIMIT 5
  • 결과: 최근 5명의 User 리스트
  • 🚀 언제 사용: 대시보드에서 최신 가입자를 보여줄 때

📌 예제 6: COUNT 집계

상황: 전체 사용자 수를 세고 싶을 때

🔍 JPQL: SELECT COUNT(u) FROM User u

@Query("SELECT COUNT(u) FROM User u")
Long countAllUsers();
  • ⚙️ SQL 변환: SELECT COUNT(*) FROM user
  • 결과: 전체 사용자 수 (Long)
  • 🚀 언제 사용: 관리자 대시보드에서 통계 표시할 때

📌 예제 7: SUM 집계

상황: 모든 사용자 나이의 합을 구하고 싶을 때

🔍 JPQL: SELECT SUM(u.age) FROM User u

@Query("SELECT SUM(u.age) FROM User u")
Long sumAllAges();
  • ⚙️ SQL 변환: SELECT SUM(age) FROM user
  • 결과: 나이 총합 (Long)
  • 🚀 언제 사용: 평균 나이 계산 전 단계로 사용할 때

📌 예제 8: AVG, MAX, MIN 집계

상황: 나이의 평균, 최대, 최소를 한 번에 구하고 싶을 때

🔍 JPQL: SELECT AVG(u.age), MAX(u.age), MIN(u.age) FROM User u

@Query("SELECT AVG(u.age), MAX(u.age), MIN(u.age) FROM User u")
List<Object[]> findAgeStats();
  • ⚙️ SQL 변환: SELECT AVG(age), MAX(age), MIN(age) FROM user
  • 결과: [평균, 최대, 최소]가 담긴 Object[]
  • 🚀 언제 사용: 통계 대시보드에서 한 번에 여러 지표를 보여줄 때

📌 예제 9: 별칭(AS) 사용

상황: 결과 컬럼명을 명시적으로 지정하고 싶을 때

🔍 JPQL: SELECT u.name AS userName, u.email AS userEmail FROM User u

@Query("SELECT u.name AS userName, u.email AS userEmail FROM User u")
List<Object[]> findUserNameAndEmailWithAlias();
  • ⚙️ SQL 변환: SELECT name AS userName, email AS userEmail FROM user
  • 결과: Object[] 리스트, 별칭으로 접근 가능
  • 🚀 언제 사용: 결과 매핑 시 명확한 컬럼명이 필요할 때

📌 예제 10: INDEX 함수 (컬렉션)

상황: List 타입 컬렉션의 특정 인덱스 값을 조회할 때

🔍 JPQL: SELECT u FROM User u WHERE INDEX(u.phones) = 0

// User 엔티티에 List<String> phones 필드가 있다고 가정
@Query("SELECT u FROM User u WHERE INDEX(u.phones) = 0")
List<User> findUsersWithFirstPhone();
  • ⚙️ SQL 변환: DB에 따라 다름 (일부 DB는 지원 안 함)
  • 결과: 첫 번째 전화번호가 있는 User 리스트
  • 🚀 언제 사용: 컬렉션 필드의 특정 위치 값을 검색할 때

3. WHERE 조건 쿼리 (예제 11~20)

📌 예제 11: 단순 동등 조건 (=)

상황: 이름이 정확히 "홍길동"인 사용자 찾기

🔍 JPQL: SELECT u FROM User u WHERE u.name = :name

@Query("SELECT u FROM User u WHERE u.name = :name")
List<User> findByNameExact(@Param("name") String name);
  • ⚙️ SQL 변환: SELEC * ROM user WHERE name = ?
  • 결과: 이름이 정확히 일치하는 User 리스트
  • 🚀 언제 사용: 로그인 ID나 정확한 값으로 검색할 때

📌 예제 12: 부분 문자열 검색 (LIKE)

상황: 이름에 "길동"이 포함된 사용자 찾기

🔍 JPQL: SELECT u FROM User u WHERE u.name LIKE %:keyword%

@Query("SELECT u FROM User u WHERE u.name LIKE %:keyword%")
List<User> findByNameContaining(@Param("keyword") String keyword);
  • ⚙️ SQL 변환: SELEC * ROM user WHERE name LIKE '%길동%'
  • 결과: 이름에 키워드가 포함된 User 리스트
  • 🚀 언제 사용: 검색창에서 부분 검색할 때

📌 예제 13: 시작/끝 문자열 검색

상황: "김"으로 시작하는 이름 찾기

🔍 JPQL: SELECT u FROM User u WHERE u.name LIKE :keyword%

@Query("SELECT u FROM User u WHERE u.name LIKE :keyword%")
List<User> findByNameStartingWith(@Param("keyword") String keyword);
  • ⚙️ SQL 변환: SELEC * ROM user WHERE name LIKE '김%'
  • 결과: "김"으로 시작하는 이름의 User 리스트
  • 🚀 언제 사용: 초성 검색이나 성씨로 필터링할 때

📌 예제 14: 범위 조건 (BETWEEN)

상황: 나이가 20세에서 30세 사이인 사용자 찾기

🔍 JPQL: SELECT u FROM User u WHERE u.age BETWEEN :minAge AND :maxAge

@Query("SELECT u FROM User u WHERE u.age BETWEEN :minAge AND :maxAge")
List<User> findByAgeBetween(@Param("minAge") int minAge, @Param("maxAge") int maxAge);
  • ⚙️ SQL 변환: SELEC * ROM user WHERE age BETWEEN 20 AND 30
  • 결과: 해당 나이 범위의 User 리스트
  • 🚀 언제 사용: 나이, 가격, 날짜 범위 필터에서 사용할 때

📌 예제 15: 목록 조건 (IN)

상황: 특정 도시 목록에 속한 사용자 찾기

🔍 JPQL: SELECT u FROM User u WHERE u.city IN :cities

@Query("SELECT u FROM User u WHERE u.city IN :cities")
List<User> findByCityIn(@Param("cities") List<String> cities);
  • ⚙️ SQL 변환: SELEC * ROM user WHERE city IN ('서울', '부산', '대구')
  • 결과: 해당 도시에 사는 User 리스트
  • 🚀 언제 사용: 체크박스 다중 선택 필터에서 사용할 때

📌 예제 16: NULL 조건 (IS NULL / IS NOT NULL)

상황: 이메일이 등록되지 않은 사용자 찾기

🔍 JPQL: SELECT u FROM User u WHERE u.email IS NULL

@Query("SELECT u FROM User u WHERE u.email IS NULL")
List<User> findByEmailIsNull();

// 반대: IS NOT NULL
@Query("SELECT u FROM User u WHERE u.email IS NOT NULL")
List<User> findByEmailIsNotNull();
  • ⚙️ SQL 변환: SELEC * ROM user WHERE email IS NULL
  • 결과: 이메일이 없는 User 리스트
  • 🚀 언제 사용: 미완성 프로필 대상으로 알림을 보낼 때

📌 예제 17: 복합 조건 (AND, OR)

상황: 나이가 25세 이상이고 서울에 사는 사용자 찾기

🔍 JPQL: SELECT u FROM User u WHERE u.age >= :age AND u.city = :city

@Query("SELECT u FROM User u WHERE u.age >= :age AND u.city = :city")
List<User> findByAgeAndCity(@Param("age") int age, @Param("city") String city);
  • ⚙️ SQL 변환: SELEC * ROM user WHERE age >= 25 AND city = '서울'
  • 결과: 두 조건을 동시에 만족하는 User 리스트
  • 🚀 언제 사용: 다중 필터 검색 기능 구현 시

📌 예제 18: NOT 조건

상황: 탈퇴하지 않은 활성 사용자만 조회하기

🔍 JPQL: SELECT u FROM User u WHERE u.status != :status

@Query("SELECT u FROM User u WHERE u.status != :status")
List<User> findByStatusNot(@Param("status") String status);

// 사용: findByStatusNot("DELETED")
  • ⚙️ SQL 변환: SELEC * ROM user WHERE status != 'DELETED'
  • 결과: 해당 상태가 아닌 User 리스트
  • 🚀 언제 사용: 소프트 딜리트 구현에서 삭제되지 않은 데이터만 조회할 때

📌 예제 19: 날짜 조건

상황: 특정 날짜 이후에 가입한 사용자 찾기

🔍 JPQL: SELECT u FROM User u WHERE u.createdDate > :date

@Query("SELECT u FROM User u WHERE u.createdDate > :date")
List<User> findByCreatedDateAfter(@Param("date") LocalDateTime date);
  • ⚙️ SQL 변환: SELEC * ROM user WHERE created_date > ?
  • 결과: 해당 날짜 이후 가입한 User 리스트
  • 🚀 언제 사용: 신규 회원 대상 이벤트 대상자를 뽑을 때

📌 예제 20: LOWER/UPPER 함수 (대소문자 무시 검색)

상황: 이메일을 대소문자 구분 없이 검색하고 싶을 때

🔍 JPQL: SELECT u FROM User u WHERE LOWER(u.email) = LOWER(:email)

@Query("SELECT u FROM User u WHERE LOWER(u.email) = LOWER(:email)")
Optional<User> findByEmailIgnoreCase(@Param("email") String email);
  • ⚙️ SQL 변환: SELEC * ROM user WHERE LOWER(email) = LOWER(?)
  • 결과: 대소문자에 관계없이 일치하는 User
  • 🚀 언제 사용: 이메일 로그인, 아이디 중복 체크 시 대소문자를 무시할 때

4. JOIN 및 관계 쿼리 (예제 21~25)

📌 예제 21: INNER JOIN

상황: 부서가 있는 사용자만 조회하기 (부서 정보도 함께)

🔍 JPQL: SELECT u FROM User u JOIN u.department d WHERE d.name = :deptName

@Query("SELECT u FROM User u JOIN u.department d WHERE d.name = :deptName")
List<User> findByDepartmentName(@Param("deptName") String deptName);
  • ⚙️ SQL 변환: SELECT u.* FROM user u INNER JOIN department d ON u.dept_id = d.id WHERE d.name = ?
  • 결과: 해당 부서에 속한 User 리스트
  • 🚀 언제 사용: 특정 팀/부서에 속한 직원을 조회할 때

📌 예제 22: LEFT JOIN

상황: 부서가 없는 사용자도 포함하여 조회하기

🔍 JPQL: SELECT u FROM User u LEFT JOIN u.department d

@Query("SELECT u FROM User u LEFT JOIN u.department d")
List<User> findAllWithDepartment();
  • ⚙️ SQL 변환: SELECT u.* FROM user u LEFT JOIN department d ON u.dept_id = d.id
  • 결과: 부서 유무에 관계없이 모든 User 리스트
  • 🚀 언제 사용: 부서 미배정 직원까지 포함한 전체 명단을 뽑을 때

📌 예제 23: FETCH JOIN (N+1 문제 해결)

상황: 사용자와 부서 정보를 한 번의 쿼리로 함께 로딩하기

🔍 JPQL: SELECT u FROM User u JOIN FETCH u.department

@Query("SELECT u FROM User u JOIN FETCH u.department")
List<User> findAllWithDepartmentFetch();
  • ⚙️ SQL 변환: SELECT u.*, d.* FROM user u INNER JOIN department d ON u.dept_id = d.id
  • 결과: 부서 정보가 즉시 로딩된 User 리스트 (LazyLoading 방지)
  • 🚀 언제 사용: 연관 엔티티를 반드시 함께 사용할 때, N+1 쿼리 문제를 해결할 때

📌 예제 24: 컬렉션 JOIN (일대다)

상황: 특정 주문을 한 사용자 조회 (User - Orders 일대다 관계)

🔍 JPQL: SELECT DISTINCT u FROM User u JOIN u.orders o WHERE o.status = :status

@Query("SELECT DISTINCT u FROM User u JOIN u.orders o WHERE o.status = :status")
List<User> findByOrderStatus(@Param("status") String status);
  • ⚙️ SQL 변환: SELECT DISTINCT u.* FROM user u INNER JOIN orders o ON o.user_id = u.id WHERE o.status = ?
  • 결과: 해당 상태의 주문이 있는 User 리스트 (DISTINCT로 중복 제거)
  • 🚀 언제 사용: 특정 행동(주문, 리뷰 등)을 한 사용자를 뽑을 때

📌 예제 25: 서브쿼리 (EXISTS)

상황: 주문 이력이 있는 사용자만 조회하기

🔍 JPQL: SELECT u FROM User u WHERE EXISTS (SELECT o FROM Order o WHERE o.user = u)

@Query("SELECT u FROM User u WHERE EXISTS (SELECT o FROM Order o WHERE o.user = u)")
List<User> findUsersWithOrders();
  • ⚙️ SQL 변환: SELEC * ROM user u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)
  • 결과: 주문 이력이 있는 User 리스트
  • 🚀 언제 사용: 특정 활동 이력이 있는 사용자만 필터링할 때

5. 정렬 및 그룹 쿼리 (예제 26~30)

📌 예제 26: ORDER BY 다중 정렬

상황: 도시별로 정렬하고, 같은 도시 안에서 나이 순으로 정렬하기

🔍 JPQL: SELECT u FROM User u ORDER BY u.city ASC, u.age DESC

@Query("SELECT u FROM User u ORDER BY u.city ASC, u.age DESC")
List<User> findAllOrderByCityAndAge();
  • ⚙️ SQL 변환: SELEC * ROM user ORDER BY city ASC, age DESC
  • 결과: 도시 오름차순, 나이 내림차순으로 정렬된 User 리스트
  • 🚀 언제 사용: 복합 기준으로 리스트를 정렬하여 보여줄 때

📌 예제 27: GROUP BY와 HAVING

상황: 도시별 사용자 수를 구하고, 10명 이상인 도시만 보기

🔍 JPQL: SELECT u.city, COUNT(u) FROM User u GROUP BY u.city HAVING COUNT(u) >= :minCount

@Query("SELECT u.city, COUNT(u) FROM User u GROUP BY u.city HAVING COUNT(u) >= :minCount")
List<Object[]> findCityWithMinUsers(@Param("minCount") long minCount);
  • ⚙️ SQL 변환: SELECT city, COUNT(*) FROM user GROUP BY city HAVING COUNT(*) >= 10
  • 결과: [도시명, 사용자 수] 쌍의 Object[] 리스트
  • 🚀 언제 사용: 지역별 통계 리포트, 그룹별 집계 결과를 보여줄 때

📌 예제 28: CASE WHEN (조건부 값 반환)

상황: 나이에 따라 "청년", "중년", "장년" 라벨을 붙이고 싶을 때

🔍 JPQL: SELECT u.name, CASE WHEN u.age < 30 THEN '청년' WHEN u.age < 50 THEN '중년' ELSE '장년' END FROM User u

@Query("SELECT u.name, CASE WHEN u.age < 30 THEN '청년' WHEN u.age < 50 THEN '중년' ELSE '장년' END FROM User u")
List<Object[]> findUsersWithAgeGroup();
  • ⚙️ SQL 변환: SELECT name, CASE WHEN age < 30 THEN '청년' WHEN age < 50 THEN '중년' ELSE '장년' END FROM user
  • 결과: [이름, 나이 그룹] 쌍의 Object[] 리스트
  • 🚀 언제 사용: 분류 라벨링이나 조건부 표시 값이 필요할 때

📌 예제 29: 서브쿼리 (IN 절 활용)

상황: VIP 등급인 사용자들의 최근 주문만 조회하기

🔍 JPQL: SELECT o FROM Order o WHERE o.user IN (SELECT u FROM User u WHERE u.grade = :grade)

@Query("SELECT o FROM Order o WHERE o.user IN (SELECT u FROM User u WHERE u.grade = :grade)")
List<Order> findOrdersByUserGrade(@Param("grade") String grade);
  • ⚙️ SQL 변환: SELEC * ROM orders WHERE user_id IN (SELECT id FROM user WHERE grade = 'VIP')
  • 결과: 특정 등급 사용자들의 Order 리스트
  • 🚀 언제 사용: 특정 조건의 사용자와 연관된 데이터를 한 번에 조회할 때

📌 예제 30: 네이티브 쿼리 혼용 (nativeQuery)

상황: JPQL로 표현하기 어려운 DB 전용 함수나 힌트를 써야 할 때

🔍 SQL (Native): SELEC * ROM user WHERE MATCH(name, email) AGAINST (?)

@Query(value = "SELEC * ROM user WHERE MATCH(name, email) AGAINST (:keyword)", nativeQuery = true)
List<User> findByFullText(@Param("keyword") String keyword);
  • ⚙️ SQL 변환: 그대로 실행 (DB 종속적)
  • 결과: 전문 검색(Full-Text Search)에 매칭된 User 리스트
  • 🚀 언제 사용: MySQL FULLTEXT 인덱스, Oracle 힌트 등 DB 전용 기능이 필요할 때

JPQL vs Spring Data JPA 메서드 비교

JPQL과 Spring Data JPA의 메서드 쿼리는 함께 사용됩니다. 간단한 조건은 메서드 이름으로, 복잡한 조건은 JPQL @Query로 작성하는 것이 실무 표준입니다.

상황 추천 방법 예시
단순 필드 조회 메서드 이름 findByName(String name)
복합 조건 조회 @Query JPQL @Query("SELECT u FROM User u WHERE ...")
DTO 변환 조회 @Query JPQL + NEW SELECT NEW dto(...) FROM ...
DB 전용 함수 nativeQuery=true @Query(value="...", nativeQuery=true)
N+1 문제 해결 FETCH JOIN JOIN FETCH u.department

마치며: JPQL 핵심 정리

JPQL 30가지 예제를 통해 기본 SELECT부터 서브쿼리, FETCH JOIN까지 실무에서 자주 쓰이는 패턴을 모두 살펴봤습니다. 중요한 포인트를 다시 정리하면 아래와 같습니다.

  • 엔티티 중심: 테이블이 아닌 엔티티 클래스명과 필드명을 사용합니다.
  • N+1 해결: 연관 엔티티가 필요하면 반드시 JOIN FETCH를 사용하세요.
  • DTO 조회: SELECT NEW 구문으로 불필요한 데이터 로딩을 줄이세요.
  • 페이징: LIMIT 대신 Pageable을 파라미터로 넘겨 처리하세요.
  • 네이티브 쿼리: DB 전용 기능이 필요할 때만 nativeQuery = true를 사용하세요.

이 글이 도움이 되셨다면 즐겨찾기 해두고, Spring JPA를 사용할 때 레퍼런스로 활용해 보세요!

반응형