📘 JPQL 실전 예제 30선: 문법 완전 정복 가이드
JPQL(Java Persistence Query Language)은 JPA에서 데이터를 조회하기 위한 객체지향 쿼리 언어입니다. SQL과 유사하지만 테이블이 아닌 엔티티(Entity)와 속성(Field)을 대상으로 동작하는 것이 핵심 차이점입니다. 이 글에서는 실무에서 바로 사용할 수 있는 30가지 실전 예제를 통해 JPQL 문법을 완전히 정복해 봅니다.
📑 목차
- JPQL 기본 문법 (SQL과 비교)
- 기본 SELECT 쿼리 (예제 1~10)
- WHERE 조건 쿼리 (예제 11~20)
- JOIN 및 관계 쿼리 (예제 21~25)
- 정렬 및 그룹 쿼리 (예제 26~30)
- JPQL vs Spring Data JPA 메서드 비교
- 마치며: 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를 사용할 때 레퍼런스로 활용해 보세요!