Hibernate 쿼리 완벽 가이드 - HQL과 Criteria API
Hibernate를 쓰면 SQL을 직접 박지 않아도 여러 가지 방식으로 데이터를 조회할 수 있다. 그중 손이 가장 많이 가는 게 HQL과 Criteria API다. 어떤 상황에서 어느 쪽을 쓰는 게 편한지, 문법은 어떻게 되는지 한 번에 정리해 둔다.
HQL (Hibernate Query Language)
HQL은 SQL과 유사하지만, 테이블이 아닌 Entity 객체를 대상으로 쿼리합니다. 데이터베이스에 독립적이며 객체 지향적인 쿼리 작성이 가능합니다.
기본 조회 (Select)
// 기본 조회
Query query = session.createQuery("from Emp");
List<Emp> list = query.list();
// 페이징 처리
Query query = session.createQuery("from Emp");
query.setFirstResult(5); // 시작 위치 (0부터 시작)
query.setMaxResults(10); // 조회할 개수
List<Emp> list = query.list(); // 6번째부터 10개 조회
HQL에서는 테이블명이 아닌 클래스명을 사용합니다.
SELECT * FROM emp대신from Emp로 작성합니다.
조건 조회 (Where)
// 파라미터 바인딩 (Named Parameter)
Query query = session.createQuery("from Emp where salary > :minSalary");
query.setParameter("minSalary", 50000);
List<Emp> results = query.list();
// 여러 조건
Query query = session.createQuery(
"from Emp e where e.department = :dept and e.salary > :salary"
);
query.setParameter("dept", "Engineering");
query.setParameter("salary", 40000);
수정 (Update)
HQL의 UPDATE는 벌크 연산으로, 영속성 컨텍스트를 거치지 않고 직접 데이터베이스를 수정합니다.
Transaction tx = session.beginTransaction();
Query query = session.createQuery("update User set name = :name where id = :id");
query.setParameter("name", "김철수");
query.setParameter("id", 111);
int affectedRows = query.executeUpdate();
System.out.println("수정된 행 수: " + affectedRows);
tx.commit();
벌크 연산 후에는 영속성 컨텍스트를 초기화(
session.clear())하는 것이 좋습니다. 그렇지 않으면 캐시된 데이터와 실제 데이터베이스 값이 불일치할 수 있습니다.
삭제 (Delete)
Transaction tx = session.beginTransaction();
Query query = session.createQuery("delete from Emp where id = :empId");
query.setParameter("empId", 100);
query.executeUpdate();
tx.commit();
HQL에서는 테이블명이 아닌 클래스명(Emp)을 사용합니다.
집계 함수 (Aggregate Functions)
// SUM
Query query = session.createQuery("select sum(salary) from Emp");
List<Long> list = query.list();
Long totalSalary = list.get(0);
// COUNT
Query query = session.createQuery("select count(*) from Emp where department = :dept");
query.setParameter("dept", "Engineering");
Long count = (Long) query.uniqueResult();
// 여러 집계 함수
Query query = session.createQuery(
"select min(salary), max(salary), avg(salary) from Emp"
);
Object[] result = (Object[]) query.uniqueResult();
그룹화와 정렬
// GROUP BY
Query query = session.createQuery(
"select department, avg(salary) from Emp group by department"
);
List<Object[]> results = query.list();
// ORDER BY
Query query = session.createQuery(
"from Emp order by salary desc, name asc"
);
// GROUP BY + HAVING
Query query = session.createQuery(
"select department, count(*) from Emp " +
"group by department having count(*) > 5"
);
조인 (Join)
// Inner Join
Query query = session.createQuery(
"select e.name, d.name from Emp e join e.department d"
);
// Left Join
Query query = session.createQuery(
"from Emp e left join fetch e.address"
);
// Fetch Join - 연관 엔티티를 함께 조회 (N+1 문제 해결)
Query query = session.createQuery(
"from Emp e join fetch e.orders"
);
Criteria API (HCQL)
Criteria API는 프로그래밍 방식으로 쿼리를 작성합니다. 동적 쿼리에 특히 유용합니다.
기본 사용법
// 전체 조회
Criteria criteria = session.createCriteria(Emp.class);
List<Emp> employees = criteria.list();
조건 추가 (Restrictions)
Criteria criteria = session.createCriteria(Emp.class);
// 단일 조건
criteria.add(Restrictions.eq("name", "홍길동"));
// 여러 조건 (AND)
criteria.add(Restrictions.gt("salary", 40000));
criteria.add(Restrictions.eq("department", "Engineering"));
// OR 조건
criteria.add(Restrictions.or(
Restrictions.eq("department", "Engineering"),
Restrictions.eq("department", "Sales")
));
List<Emp> results = criteria.list();
주요 Restriction 메서드:
| 메서드 | 설명 | SQL 동등 |
|---|---|---|
eq(property, value) |
같음 | = |
ne(property, value) |
같지 않음 | <> |
gt(property, value) |
초과 | > |
ge(property, value) |
이상 | >= |
lt(property, value) |
미만 | < |
le(property, value) |
이하 | <= |
like(property, value) |
패턴 매칭 | LIKE |
between(property, lo, hi) |
범위 | BETWEEN |
isNull(property) |
NULL 체크 | IS NULL |
isNotNull(property) |
NOT NULL 체크 | IS NOT NULL |
in(property, collection) |
IN 조건 | IN |
정렬 (Order)
Criteria criteria = session.createCriteria(Emp.class);
criteria.addOrder(Order.desc("salary"));
criteria.addOrder(Order.asc("name"));
List<Emp> results = criteria.list();
페이징 (Pagination)
Criteria criteria = session.createCriteria(Emp.class);
criteria.setFirstResult(0); // 시작 위치
criteria.setMaxResults(10); // 조회 개수
List<Emp> results = criteria.list();
프로젝션 (Projection)
특정 필드만 조회할 때 사용합니다.
Criteria criteria = session.createCriteria(Emp.class);
criteria.setProjection(Projections.projectionList()
.add(Projections.property("name"))
.add(Projections.property("salary"))
);
List<Object[]> results = criteria.list();
집계 함수
Criteria criteria = session.createCriteria(Emp.class);
// 단일 집계
criteria.setProjection(Projections.rowCount());
Long count = (Long) criteria.uniqueResult();
// 그룹별 집계
criteria.setProjection(Projections.projectionList()
.add(Projections.groupProperty("department"))
.add(Projections.avg("salary"))
);
List<Object[]> results = criteria.list();
JPA 2.0 Criteria API
JPA 2.0부터는 표준 Criteria API가 도입되었습니다. Hibernate Criteria보다 타입 안전합니다.
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Emp> query = cb.createQuery(Emp.class);
Root<Emp> emp = query.from(Emp.class);
// 조건 추가
query.select(emp)
.where(
cb.and(
cb.equal(emp.get("department"), "Engineering"),
cb.gt(emp.get("salary"), 40000)
)
)
.orderBy(cb.desc(emp.get("salary")));
List<Emp> results = entityManager.createQuery(query).getResultList();
HQL vs Criteria API 비교
| 특징 | HQL | Criteria API |
|---|---|---|
| 문법 | SQL과 유사한 문자열 | 프로그래밍 방식 |
| 동적 쿼리 | 문자열 조합 필요 | 메서드 체이닝으로 편리 |
| 타입 안전성 | 런타임 오류 | 컴파일 타임 검증 가능 |
| 가독성 | 익숙한 SQL 스타일 | 복잡한 쿼리는 코드가 길어짐 |
| 사용 시점 | 정적 쿼리, 간단한 쿼리 | 동적 쿼리, 조건이 많은 쿼리 |
선택 가이드
- HQL 추천: 쿼리가 정적이고 변경이 적은 경우, SQL에 익숙한 팀
- Criteria API 추천: 검색 조건이 동적으로 변하는 경우, 타입 안전성이 중요한 경우
Spring Data JPA에서의 활용
Spring Data JPA에서는 메서드 이름으로 쿼리를 생성하거나 @Query 어노테이션을 사용합니다.
public interface EmpRepository extends JpaRepository<Emp, Long> {
// 메서드 이름으로 쿼리 생성
List<Emp> findByDepartmentAndSalaryGreaterThan(String department, int salary);
// JPQL 직접 작성
@Query("select e from Emp e where e.department = :dept order by e.salary desc")
List<Emp> findByDepartmentOrderBySalary(@Param("dept") String department);
// Native Query
@Query(value = "SELECT * FROM emp WHERE salary > ?1", nativeQuery = true)
List<Emp> findHighSalaryEmployees(int minSalary);
}
동적 쿼리가 필요한 경우 Specification을 활용합니다:
public interface EmpRepository extends JpaRepository<Emp, Long>,
JpaSpecificationExecutor<Emp> {
}
// Specification 정의
public class EmpSpecifications {
public static Specification<Emp> hasDepartment(String department) {
return (root, query, cb) -> cb.equal(root.get("department"), department);
}
public static Specification<Emp> salaryGreaterThan(int salary) {
return (root, query, cb) -> cb.gt(root.get("salary"), salary);
}
}
// 사용
List<Emp> results = empRepository.findAll(
Specification.where(hasDepartment("Engineering"))
.and(salaryGreaterThan(40000))
);
정리하며
정적인 쿼리는 HQL이 짧고 읽기 좋고, 조건이 런타임에 결정되는 쿼리는 Criteria API가 손에 잘 붙는다. Spring Data JPA를 쓴다면 메서드 이름과 @Query로 대부분의 단순 케이스는 해결되고, 그 이상의 동적 쿼리는 Specification이나 QueryDSL로 빠지는 흐름이 자연스럽다.
Comments