10. 객체지향 쿼리 언어 JPQL (1)

2025. 6. 23. 17:41Spring/JPA

1. 객체지향 쿼리 언어 소개
 1) JPA에서 지원하는 다양한 쿼리 방법
 2) JPQL
 3) JPQL의 특징

 

2. JPQL 사용 흐름 

 1) Query 객체 생성
 2) 파라미터 바인딩 
 3) 쿼리 실행 

 

3. JPQL 기본 문법과 기능
 1) 문법 구조
 2) 집합과 정렬 함수
 3) 결과 조회 API
 4) 파라미터 바인딩
 5) 프로젝션
 6) 페이징 API

 

4. 조인 및 서브 쿼리
 1) 조인 종류
 2) ON 절
 3) 서브 쿼리

 

5. 타입 표현과 조건식
 1) 타입
 2) 조건식
 3) 기본 함수


1. 객체지향 쿼리 언어 소개

1) JPA에서 지원하는 다양한 쿼리 방법

쿼리 방법 설명
JPQL JPA에서 제공하는 객체지향 쿼리 언어. 가장 널리 사용됨.
Criteria API 자바 코드 기반으로 쿼리 생성. 정적 타입 보장. 하지만 코드 복잡도가 높아 잘 쓰이지 않음.
QueryDSL 자바 코드 기반 + 정적 타입 보장 + 문법 간결. 실무에서 가장 선호되는 방식.
네이티브 SQL SQL을 그대로 작성. 복잡한 DB 종속 기능(예: 오라클 CONNECT BY 등)에 사용.
JDBC 직접 사용 /
MyBatis / SpringJdbcTemplate
필요 시 병행 사용 가능. 단, JPA와의 영속성 컨텍스트 동기화에 주의 필요.
  • 실무 활용시 CRUD 중심 → JPQL 또는 QueryDSL 위주로 처리 
  • 복잡한 리포트성 쿼리 → 네이티브 SQL 또는 MyBatis로 처리하는 구조가 일반적

2) JPQL (Java Persistence Query Language)

  • SQL을 추상화한 객체 지향 쿼리 언어
  • JPQL 쿼리의 대상은 테이블이 아니라 엔티티 객체
  • SQL 문법과 유사하며 SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 등 지원

3) JPQL의 특징

  • 쿼리 대상의 차이
    • SQL: 테이블/컬럼 기준
      SELECT * FROM member WHERE age > 18 → member 테이블 대상
    • JPQL: 엔티티/필드 기준
      SELECT m FROM Member m WHERE m.age > 18 → Member 엔티티 대상
  • JPQL 쿼리는 결국 SQL로 변환된다. 
    • 개발자가 SQL을 직접 작성하는 대신 JPQL을 사용해서 추상화된 쿼리를 작성하면 
    • JPA 구현체가 실행 단계에서 적절한 SQL로 변환하여 DB에 전달한다. 
  • DB 벤더에 종속되지 않음
    • DBMS마다 SQL 문법이 조금씩 다르지만,
      JPA는 방언(Dialect)을 통해 JPQL을 각 DBMS에 맞는 SQL로 자동 변환한다. 
    • persistence.xml 또는 application.yml에 DB 방언을 설정해줘야함
      spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect

2. JPQL 사용 흐름

// 1단계: JPQL 쿼리 작성
String jpql = "SELECT m FROM Member m WHERE m.age > :age";

// 2단계: 쿼리 객체 생성 (em이 JPQL을 이해하고 준비함)
TypedQuery<Member> query = em.createQuery(jpql, Member.class);

// 3단계: 파라미터 바인딩
query.setParameter("age", 20);

// 4단계: 쿼리 실행
List<Member> resultList = query.getResultList();

// 결과 사용
for (Member member : resultList) {
    System.out.println("회원 이름: " + member.getUsername() + ", 나이: " + member.getAge());
}

 

1) Query 객체 생성: em.createQuery()

  • EntityManager가 JPQL 문장을 분석해서 실행 준비를 함
  • 내부적으로 쿼리 객체(org.hibernate.query.QueryImpl)가 생성됨
  • 아직 SQL은 만들어지지 않음 
  • 반환 타입(Member.class) 에 따라 내부적으로 조회 대상 엔티티와 매핑 방식도 준비함

2) 파라미터 바인딩: query.setParameter()

  • JPQL 쿼리 안에 있는 :age라는 변수 자리에 실제 값(20)이 저장됨.
  • 쿼리 객체 내부에 “바인딩된 값 맵”이 설정됨. (예: "age" -> 20 형태로 저장돼 있음)

3) 쿼리 실행: query.getResult..()

  • JPQL → SQL 변환 시작
    • Member라는 엔티티에서 → member 테이블로, m.age에서 → age 컬럼으로 변환 
    • 조건문도 전부 SQL 문법에 맞게 변경
    • 바인딩된 파라미터도 SQL문에 반영됨
  • JPA는 내부적으로 JDBC를 통해 DB 커넥션을 열고 SQL을 날린다. 
  • DB가 실행 결과(ResultSet)를 반환하면 Member 엔티티 객체들로 매핑 
    • JPA가 컬럼 → 필드로 하나씩 값을 복사함
    • 영속성 컨텍스트에 관리되는 상태로 등록됨 (1차 캐시도 여기에 연관됨)
    • 최종적으로 List<Member>가 리턴됨

 

3. JPQL 기본 문법과 기능 

1) 문법 구조

SELECT ...
FROM ...
WHERE ...
GROUP BY ...
HAVING ...
ORDER BY ...
  • 기본 구조는 SQL과 매우 유사
  • 데이터 조작문 update, delete도 가능
  • 주의 사항
    • JPQL 키워드: 대소문자 구분 X (SELECT, select 동일)
    • 엔티티 이름과 속성명: 대소문자 구분 O (Member, member, m.age 등은 정확히 일치해야 함)
    • 테이블명 사용 X, 반드시 엔티티명 사용O

2) 집합과 정렬 함수

  • 집계 함수: COUNT, SUM, AVG, MAX, MIN
  • 정렬 및 그룹화 함수: GROUP BY, HAVING, ORDER BY 지원
  • GROUP BY 없이 집계 함수만 단독 사용하는 것도 가능

3) 결과 조회 API (메서드)

//1. 결과가 여러개일 때
List<Member> result = em.createQuery("SELECT m FROM Member m", Member.class)
                        .getResultList();
//2. 결과가 딱 하나일 때
Member member = em.createQuery("SELECT m FROM Member m WHERE m.id = :id", Member.class)
                  .setParameter("id", 1L)
                  .getSingleResult();
  • getResultList() :결과가 여러 개인 경우에 사용 
    • 결과 있으면 List<T>로 반환
    • 결과 없으면 빈 리스트 반환 (null 아님)
  • getSingleResult(): 결과가 정확히 1개일 때 사용
    • 반환 타입: 단일 객체 
    • 결과 없으면: NoResultException
    • 2개 이상이면: NonUniqueResultException
    • 실무에서는 getSingleResult()는 자칫 잘못하면 예외터지므로 try-catch로 감싸는 경우가 많다.
  • 메서드들을 API라고 표현하는 이유는
    이러한 메서드들이 개발자가 사용할 수 있도록 JPA가 준비하여 제공하는 기능 집합이기 때문이다. 

4) 파라미터 바인딩

//1. 이름 기준 바인딩, 많이 사용되는 방법
//:username은 이 자리가 나중에 "username"이라는 키 이름으로 바인딩될 자리라고 알려주는 신호
TypedQuery<Member> query = 
	em.createQuery("SELECT m FROM Member m WHERE m.username = :username", Member.class);
query.setParameter("username", "Lee");
//"username"은 setParameter()에서 사용할 키 이름, "Lee"는 실제 바인딩될 값

//2. 위치 기준 바인딩
//?1, ?2는 순서를 의미함 (1부터 시작)
SELECT m FROM Member m WHERE m.username = ?1
query.setParameter(1, "Lee"); //setParameter(1, ...)로 바인딩할 값을 설정함
//단점: 쿼리에 파라미터가 많아지면 순서 헷갈림
  • 파라미터 바인딩: 쿼리 안에 있는 변수에 실제 값을 묶어서 채워 넣는 것
    • JPQL 쿼리 안에 변수 자리를 :이름 또는 ?숫자 로 직접 표시
    • 그걸 setParameter()로 묶어서 연결함  
  • 바인딩이 중요한 이유
    • 동적 데이터 사용 가능: 유저 이름, 나이, 검색 키워드 등 실행 시점에 정해지는 값을 넣을 수 있음
    • SQL 인젝션 방지: 문자열을 직접 쿼리에 박아넣는 방식은 보안 위험이 있음 (예: ' OR 1=1 -- 같은 해킹 공격)
    • setParameter()는 내부적으로 PreparedStatement를 써서 안전하게 처리함

5) 프로젝션

  • 쿼리의 SELECT 절에 무엇을 조회할지 지정하는 것
    → “엔티티 전체를 조회할지”, “필드만 조회할지”, “DTO로 변환할지” 결정하는 것을 말한다. 
  • 엔티티, 임베디드 타입, 스칼라 타입 모두 가능
종류 예시 설명
엔티티 프로젝션 SELECT m FROM Member m Member 전체 엔티티 객체 반환
임베디드 타입 SELECT m.address FROM Member m 내장 타입(예: 주소 객체) 조회
스칼라 타입 SELECT m.username, m.age FROM Member m 필드(문자열, 숫자 등)만 조회
  • 여러 값 조회 시 Object[], DTO, Query 사용 
//① Object[]로 받기 (가장 간단하지만 비추) 
//→ 타입 안전 X, 가독성 떨어짐
List<Object[]> results = em.createQuery("SELECT m.username, m.age FROM Member m")
                           .getResultList();
for (Object[] row : results) {
    String username = (String) row[0];
    int age = (Integer) row[1];
}

//② Query 타입으로 받기 (반환 타입 불명확할 때)
//→ 거의 Object[]와 비슷한데, 반환 타입 명시가 없다는 차이 
Query query = em.createQuery("SELECT m.username, m.age FROM Member m");
List<Object[]> result = query.getResultList();

//③ DTO로 바로 매핑 (실무에서 가장 선호)
//→ 타입 안전, 명확한 반환 구조, 유지보수 좋음
//단, DTO 클래스에 필드 순서와 타입이 정확히 일치하는 생성자가 있어야 함
String jpql = "SELECT new com.example.MemberDTO(m.username, m.age) FROM Member m";
List<MemberDTO> dtos = em.createQuery(jpql, MemberDTO.class).getResultList();

6) 페이징 API

  • JPA는 DB 종류 상관없이 페이징 기능을 추상화하여 제공한다.
  • 두 메서드를 사용해서 구현
    • setFirstResult(int): 시작 위치
    • setMaxResults(int): 조회할 개수
.setFirstResult(int startPosition)  // 0부터 시작 (OFFSET)
.setMaxResults(int maxResult)       // LIMIT 개수

List<Member> result = em.createQuery("SELECT m FROM Member m ORDER BY m.username", Member.class)
                        .setFirstResult(10) //11부터 시작 
                        .setMaxResults(20)	//20개를 가져옴 
                        .getResultList();
  • 페치 조인과 페이징을 함께 사용하면
    • 단일 값 연관 (ManyToOne, OneToOne)은 OK
    • 컬렉션 (OneToMany, ManyToMany)은 페이징 X
      → 이럴 땐 별도 쿼리 분리해서 처리하거나, DTO로 페이징 전용 쿼리 따로 만들기

 

4. 조인 및 서브 쿼리

1) 조인 종류

  • JPA는 엔티티 간 연관관계를 기준으로 SQL처럼 조인할 수 있다.
  • 단, 엔티티에 @ManyToOne, @OneToMany 등이 매핑되어 있어야 한다. 
-- 1. 내부조인 (INNER JOIN)
-- 연관관계가 있는 엔티티를 양쪽 모두 존재하는 교집합 데이터만 조회
SELECT m FROM Member m JOIN m.team t
-- 생성 SQL문 
SELECT m.*
FROM member m
JOIN team t ON m.team_id = t.id

--2. 외부 조인 (LEFT OUTER JOIN)
-- 한쪽(Member)은 있고, 다른 쪽(Team)이 없어도 모두 포함해서 조회
SELECT m FROM Member m LEFT JOIN m.team t
-- 생성 SQL문 
SELECT m.*
FROM member m
LEFT OUTER JOIN team t ON m.team_id = t.id
--Team이 없는 경우에도 Member는 가져옴
--실무에서 데이터 유실 없이 다 가져오고 싶을 때 많이 씀

--3. 세타조인 (CROSS JOIN + WHERE 조건) 
-- 연관관계가 아예 없어도 그냥 모든 조합 만들어 놓고 조건으로 필터링
SELECT m FROM Member m, Team t WHERE m.username = t.name
-- 생성 SQL문 
SELECT m.*
FROM member m
CROSS JOIN team t
WHERE m.username = t.name
--Member와 Team은 아무 연관관계 없어도 조인 가능
--실무에서는 거의 비추천 → 조합 수가 많아지면 성능 안좋아짐

 

2) ON 절  (JPA 2.1+)

  • JPQL에서도 이제 SQL처럼 조인 조건을 ON 절에 명시할 수 있음
  • WHERE절에 필터를 걸면 조인 자체가 안 일어날 수 있어서, JOIN 기준 자체를 제한하려면 ON절 사용
  • 직접적인 연관관계가 없는 객체끼리도 ON 조건으로 조인 가능 (세타 조인 업그레이드 버전, 성능에 주의) 

3) 서브 쿼리

  • 쿼리 안에 쿼리를 넣는 방식.
  • JPQL에서는 WHERE, HAVING에서만 사용 가능 
  • SELECT는 Hibernate에서만 지원
  • FROM 절에서 서브쿼리가 필요한 경우 → Join + Group By 로 풀어내는 게 표준
  • 자주 쓰는 조건은 서브쿼리보다 Join으로 미리 가져와서 처리하는 편이 낫다
-- 평균 나이보다 많은 회원
SELECT m FROM Member m
WHERE m.age > (SELECT AVG(m2.age) FROM Member m2)

-- 주문한 적이 있는 회원만 조회
SELECT m FROM Member m
WHERE (SELECT COUNT(o) FROM Order o WHERE o.member = m) > 0

-- 특정 팀에 소속된 회원
SELECT m FROM Member m
WHERE EXISTS ( --결과 존재하면 TRUE
  SELECT t FROM m.team t WHERE t.name = 'A팀' 
)

 

5. 타입 표현과 조건식

1) 타입 표현 

  • JPQL은 타입 리터럴을 SQL과 비슷하게 표현하지만, Java 문법을 반영해서 약간 다르게 써야 한다. 
타입 예시 설명
문자 'Hello' 작은 따옴표 ' '로 감싸야 함
숫자 10L, 10D, 10F L: Long, D: Double, F: Float
Boolean TRUE, FALSE 대문자로
ENUM MemberType.ADMIN 패키지명 생략 가능, 하지만 상황에 따라 전체 이름 필요함
엔티티 타입 TYPE(e) = SomeSubType 상속 관계에서 자식 타입 조건 걸 때 사용
SELECT m FROM Member m WHERE m.username = 'Lee'
SELECT m FROM Member m WHERE m.age >= 10L
SELECT m FROM Member m WHERE m.isActive = TRUE
SELECT m FROM Member m WHERE m.memberType = jpabook.MemberType.ADMIN

2) 조건식

-- 기본 CASE 식
SELECT 
  CASE 
    WHEN m.age <= 10 THEN '학생요금'
    WHEN m.age >= 60 THEN '경로요금'
    ELSE '일반요금'
  END
FROM Member m

-- 단순 CASE 식
SELECT 
  CASE t.name
    WHEN '팀A' THEN '인센티브 110%'
    WHEN '팀B' THEN '인센티브 120%'
    ELSE '기본 인센티브'
  END
FROM Team t

-- COALESCE (널 대체)
SELECT COALESCE(m.username, '이름 없는 회원') FROM Member m
-- username이 null이면 '이름 없는 회원' 반환한다. 
-- SQL의 NVL, IFNULL 과 비슷

-- NULLIF (두 값이 같으면 null 반환)
SELECT NULLIF(m.username, '관리자') FROM Member m
-- username이 '관리자'이면 → null, 아니면 원래 값을 반환

3) 기본 함수

함수 설명 예시
CONCAT(a, b) 문자열 합치기 `'Hello'
SUBSTRING(s, start, length) 문자열 자르기 '블랙핑크' → '블랙'
TRIM(s) 양쪽 공백 제거 ' Hello ' → 'Hello'
LOWER(s) / UPPER(s) 소문자/대문자 변환 'hello' → 'HELLO'
LENGTH(s) 문자열 길이 'Lee' → 3
LOCATE(sub, str) 부분 문자열 위치 'e', 'hello' → 2
ABS(n), SQRT(n), MOD(a, b) 수학 함수 절대값, 제곱근, 나머지 등
SIZE(c) 컬렉션 크기 SIZE(m.orders)
INDEX(c) 컬렉션 인덱스 (JPA 전용) @OrderColumn 사용 시 가능
-- 실무에서는 문자열 조건 처리하거나, enum → 문자열 매핑하거나, DTO의 표시용 컬럼 처리할 때 자주 사용
SELECT CONCAT(m.username, '님') FROM Member m
SELECT LENGTH(m.username) FROM Member m
SELECT LOWER(m.username) FROM Member m
SELECT ABS(m.age), MOD(m.age, 2) FROM Member m