반응형
해당 글에서는 Spring Boot JPA 기반의 JPQL(Java Persistence Query Language)을 활용하는 방법에 대해 이해를 돕기 위해 작성한 글입니다.
💡 [참고] JPA 관련해서 구성 내용에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
분류 | 링크 |
Spring Boot Data JPA -1: ORM, JPA, Hibernate, QueryDSL 이론 | https://adjh54.tistory.com/421 |
Spring Boot Data JPA -2: 초기 환경 구성 + JpaRepository 활용 방법 | https://adjh54.tistory.com/422 |
Spring Boot Data JPA -3: 상세 JpaRepository 활용 방법 | https://adjh54.tistory.com/481 |
Spring Boot Data JPA 엔티티 어노테이션 -1 : 테이블 컬럼 단위 | https://adjh54.tistory.com/466 |
Spring Boot Data JPA 엔티티 어노테이션 -2 : 엔티티(테이블)간의 관계 | https://adjh54.tistory.com/477 |
Spring Boot Data JPA FetchType 이해하기 : 즉시/지연로딩 | https://adjh54.tistory.com/476 |
Spring Boot Data JPA + JPQL 활용 방법 | https://adjh54.tistory.com/479 |
Spring Boot Data JPA + Criteria API 활용 방법 | https://adjh54.tistory.com/483 |
Spring Boot Data JPA : JpaRepository, JPQL, Criteria API 소스코드 Github | https://github.com/adjh54ir/blog-codes/tree/main/spring-boot-jpa |
Spring Boot Data JPA + QueryDSL 활용 방법-1 : 정의 및 구성요소 | https://adjh54.tistory.com/484 |
Spring Boot Data JPA + QueryDSL 활용 방법-2 : 초기 환경설정 및 활용예시 | https://adjh54.tistory.com/485 |
1) Spring Boot JPA(Java Persistence API)
💡 Spring Boot JPA(Java Persistence API)
- 데이터베이스를 쉽게 다루기 위한 ‘데이터 액세스 기술’로 ORM(Object-Relational Mapping) 기법을 사용하여 자바 애플리케이션에서 사용하는 객체와 관계형 데이터베이스 사이의 매핑을 관리하는 ORM 기술에 대한 API 표준 명세서(인터페이스) 의미합니다.
- 이 API를 사용하여 개발자가 직접적인 SQL을 작성하지 않고도 데이터베이스에서 데이터를 저장, 업데이트, 삭제, 조회하는 등의 작업을 수행할 수 있게 해 줍니다.
- JPA는 표준화된 API를 제공함으로써, 다양한 ORM 프레임워크(예: Hibernate, EclipseLink, OpenJPA 등)와의 호환성을 보장합니다. 이로 인해 개발자는 특정 ORM 프레임워크에 종속되지 않고 필요에 따라 다른 프레임워크로 쉽게 전환할 수 있습니다.
2) JPQL(Java Persistence Query Language)
💡 JPQL(Java Persistence Query Language)
- SQL을 기반으로 한 객체 모델용 쿼리 언어입니다. SQL과 매우 유사한 형태지만 데이터베이스 ‘테이블과 컬럼’이 아닌 ‘자바 클래스와 변수(객체)’에 작업을 수행한다는 점에서 차이가 있습니다.
- 그렇기에 데이터베이스 테이블을 대상으로 쿼리 하는 것이 아닌 엔티티(객체)를 대상으로 쿼리를 수행합니다.
💡 SQL 사용예시
- SQL문에서는 ‘테이블과 컬럼’을 대상으로 쿼리를 작성합니다.
💡 JPQL 사용예시
- JPQL에서는 SQL문과 형태가 비슷합니다. 단, ‘테이블과 컬럼’을 대상으로 하는 것이 아닌 ‘엔티티(클래스)와 변수’를 대상으로 쿼리를 작성합니다.
1. JPQL 특징
특징 | 설명 |
기본적인 연산 지원 | SELECT, UPDATE, DELETE, INSERT(Embeddable 클래스에 한정) 등의 기본적인 연산을 지원하며, 함수, 연산자, 키워드 등 다양한 기능을 제공합니다. |
객체지향 쿼리 언어 | 자바의 특성을 최대한 활용할 수 있으며, 쿼리 결과를 객체 또는 객체의 컬렉션으로 직접 반환받을 수 있습니다. |
타입 안정성 제공 | 컴파일 시점에 쿼리의 문법 오류를 검사할 수 있습니다. |
2. JpaRepository와 비교
분류 | JpaRepository | JPQL |
정의 | Spring Data JPA에서 제공하는 인터페이스로 개발자가 JPA를 더 쉽고 편하게 사용할 수 있게 도와줍니다. | Java Persistence Query Language의 약자로, SQL을 기반으로 한 객체 모델용 쿼리 언어입니다. |
사용 용이성 | JpaRepository를 상속받은 Repository 인터페이스를 생성함으로써 간단하게 CRUD 기능을 사용할 수 있습니다. | SQL과 비슷한 문법을 가지며, 객체 지향적인 쿼리를 작성할 수 있습니다. |
유연성 | Spring Data JPA에 의해 구현되므로, 개발자는 쿼리를 직접 작성하지 않아도 됩니다. 하지만 이는 복잡한 쿼리를 작성하는데 한계가 있을 수 있습니다. | 개발자가 직접 쿼리를 작성하므로 상황에 따른 복잡한 쿼리 작성이 가능합니다. |
호환성 | JpaRepository는 JPA를 기반으로 하므로 JPA를 지원하는 모든 데이터베이스 시스템과 호환됩니다. | JPQL도 JPA를 기반으로 하므로, JPA를 지원하는 모든 데이터베이스 시스템과 호환됩니다. |
💡 [참고] JpaRepository에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
3. JPQL 처리방식
💡 JPQL 처리방식
1. 리턴 타입 ‘쿼리 타입’을 지정합니다 : TypedQuery, Query
2. 쿼리를 구성합니다 : createQuery
3. 쿼리 프로젝션을 설정합니다. : 엔티티, 임베디드 타입, 스칼라 타입 프로젝션
4. 쿼리 파라미터를 지정합니다 : 이름 기준, 위치 기준
5. 쿼리 결과를 조회 방식을 선택합니다.: getSingleResult(), getResultList()
3) JPQL 쿼리 방식
💡 JPQL 쿼리 방식
- JPQL에서 쿼리를 만드는 방식에는 TypedQuery 클래스를 이용하는 방식과 Query 클래스를 이용하는 방식 두 가지가 있습니다.
1. TypedQuery
💡 TypedQuery
- 반환되는 ‘타입이 명확할 때’ 사용하는 클래스입니다. 반환 타입을 미리 지정하기 때문에 컴파일 시점에 오류를 잡을 수 있어서 안정적입니다.
💡 TypedQuery 사용예시
- TypedQuery의 쿼리 수행 반환값을 제너릭으로 UserEntity 객체로 지정하였습니다.
- 즉, 쿼리를 수행한 결과의 반환 값을 UserEntity로 반환받습니다.
@PersistenceContext
private EntityManager em;
// 쿼리를 수행합니다.
TypedQuery<UserEntity> typedQuery = em.createQuery("select u from UserEntity u where u.id = :id", UserEntity.class);
// 리스트 형태로 결과값을 반환 받습니다.
List<UserEntity> resultList = typedQuery.getResultList();
2. Query
💡 Query
- 반환되는 ‘타입이 명확하지 않을 때’ 사용하는 클래스입니다. 다양한 타입의 결과로 반환받을 수 있지만 타입체크가 런타임 시점에 수행되어 안정성이 떨어질 수 있습니다.
💡 Query 사용예시
- Query 클래스를 이용하면 쿼리 수행 반환값의 타입을 지정하지 않았습니다.
@PersistenceContext
private EntityManager em;
Query query = em.createQuery("select u from UserEntity u where u.id = :id");
// 단일한 결과를 반환 받습니다 : NoResultException, NonUniqueResultException 오류가 발생 할 수 있음.
Object result = query.getSingleResult();
// 리스트 형태로 결과를 반환 받습니다.
List<UserEntity> resultList = query.getResultList();
4) JPQL 파라미터 방식
💡 JPQL 파라미터 방식
- JPQL 쿼리를 처리할 때 파라미터에 따라 동적으로 처리하는 방식에 대해 알아봅니다.
1. 이름 기준 파라미터 바인딩
💡 이름 기준 파라미터 바인딩
- 쿼리 내에서 ‘:변수명’ 형태로 선언한 후에 setParameter 메서드를 사용하여 값을 설정하는 방식을 의미합니다.
@PersistenceContext
private EntityManager em;
TypedQuery<UserEntity> typedQuery = em
.createQuery("select u from UserEntity u where u.id = :id and u.userNm = :userNm", UserEntity.class)
.setParameter("id", 1234)
.setParameter("userNm", "admin");
2. 위치 기준 파라미터 바인딩
💡 위치 기준 파라미터 바인딩
- 쿼리 내에 파라미터를 '?위치번호' 형태로 선언한 후 setParameter 메서드를 사용하여 값을 설정합니다. 위치번호는 1부터 시작합니다.
- 해당 방식은 권장하지 않습니다. 이유는 복잡한 쿼리문의 같은 경우 위치값이 변할 수 있기에 직접적인 매핑이 되는 이름 기준 파라미터 바인딩을 권장하고 있습니다.
@PersistenceContext
private EntityManager em;
TypedQuery<UserEntity> typedQuery2 = em
.createQuery("select u from UserEntity u where u.id = ?1 and u.userNm = ?2", UserEntity.class)
.setParameter(1, 1234)
.setParameter(2, "admin");
5) JPQL 프로젝션(JPQL Projection)
💡 JPQL 프로젝션
- 쿼리 결과로 ‘특정 필드(컬럼)’만 선택하여 가져오는 것을 의미합니다. 프로젝션 대상은 엔티티 프로젝션, 임베디드 타입 프로젝션, 스칼라 타입 프로젝션으로 선택이 가능합니다.
- 이를 통해 필요한 정보만 선별적으로 가져올 수 있어서 애플리케이션의 성능을 향상할 수 있으며, JOIN 연산을 사용하여 여러 테이블의 정보를 가져올 때 프로젝션을 활용하면 많은 도움이 됩니다.
프로젝션 | 유형 | 예시 |
엔티티 프로젝션 | 엔티티를 직접 선택하여 조회하는 방식. 엔티티에 있는 모든 필드가 조회됨 | 모든 엔티티의 정보를 조회하고자 할때 사용 |
임베디드 타입 프로젝션 | 엔티티의 특정 부분(임베디드 타입)을 직접 조회하는 방식. 여러 속성을 한번에 묶어서 조회할 수 있음 | 엔티티 내에 필드가 임베디드 타입인 경우(여러 속성을 묶는 경우)에 사용하고자 할때 사용 |
스칼라 타입 프로젝션 | 엔티티 내의 특정 필드들(개별 속성들)을 선택하여 조회하는 방식. 필요한 정보만 조회 가능 | 엔티티 내의 특정 필드만 조회하고자 하는 경우에 사용 |
1. 엔티티 프로젝션(Entity Projection)
💡 엔티티 프로젝션(Entity Projection)
- JPQL의 조회 방식 중 ‘엔티티를 직접 선택’하여 조회하는 방식을 의미합니다.
💡 엔티티 프로젝션 사용예시
- 해당 예시에서는 UserEntity에 대해 Alias로 ‘u’를 지정하였습니다. 지정된 값을 SELECT문 내에서 Entity Alias인 ‘u’를 직접 지정하여 조회하고 있습니다.
- 즉 u를 조회함에 따라 UserEntity에 있는 모든 필드값이 조회가 됩니다.
@PersistenceContext
private EntityManager em;
// [CASE1] 엔티티(테이블)의 모든 필드(컬럼)을 조회합니다.
TypedQuery<UserEntity> typedQuery = em
.createQuery("select u from UserEntity u where u.id = :id and u.userNm = :userNm", UserEntity.class)
.setParameter("id", 1234)
.setParameter("userNm", "admin");
2. 임베디드 타입 프로젝션(Embedded Type Projection)
💡 임베디드 타입 프로젝션(Embedded Type Projection)
- JPQL의 조회 방식 중 ‘엔티티의 특정 부분(임베디드 타입)’을 직접 조회하는 방식을 의미합니다.
- 즉 여러 속성을 한 번에 묶어서 조회하는 것이 가능하므로 데이터의 관계성이나 구조를 보존하면서 필요한 부분만 조회할 수 있다는 장점이 있습니다
💡 임베디드 타입 프로젝션(Embedded Type Projection) 사용예시
- Order Entity에서 m.address라는 필드(컬럼)을 조회하고 있습니다. 해당 address의 경우는 @Embedded 어노테이션으로 지정된 필드입니다.
@PersistenceContext
private EntityManager em;
List<Address> resultList = em
.createQuery("select m.address from Order m", Address.class)
.getResultList();
💡 Order Entity
- Order Entity 내에서 Address라는 객체로 필드를 구성하였습니다. 해당 필드는 @Embedded 어노테이션으로 선언된 임베디드 타입입니다.
@Entity
public class Order {
@Id
@GeneratedValue
private Long id;
@Embedded
private Address address;
}
💡 Address 객체
- @Embeddable로 선언된 Address 클래스에는 street, city, state, zipCode 필드로 구성되어 있습니다.
@Embeddable
public class Address {
private String street;
private String city;
private String state;
private String zipCode;
}
[ 더 알아보기 ]
💡 @Embedded 어노테이션
- 엔티티 내에서 다른 객체를 임베디드 하는 데 사용하는 어노테이션입니다.
- 이를 통해 객체지향적인 설계를 유지하면서 데이터베이스 테이블을 객체에 매핑할 수 있습니다. 해당 어노테이션의 속성은 존재하지 않습니다.
💡 @Embeddable 어노테이션
- 클래스가 값 타입을 구현하고 있음을 지정하는 데 사용하는 어노테이션입니다. 이를 사용하면 여러 엔티티에서 재사용 가능한 공통 클래스를 정의할 수 있습니다.
💡 [참고] @Embedded과 @Embeddable 사용에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
3. 스칼라 타입 프로젝션(Scalar Type Projection)
💡 스칼라 타입 프로젝션(Scalar Type Projection)
- JPQL의 조회 방식 중 ‘엔티티 내의 특정 필드들(개별속성들)을 선택’하여 조회하는 방식을 의미합니다. 이를 통해 필요한 정보만 조회하기에 효율적인 조회가 가능합니다.
TypedQuery<UserEntity> typedQuery = em
// UserEntity 내에서 userId, userNm의 특정 엔티티 필드만 가져옵니다.
.createQuery("select u.userId, u.userNm from UserEntity u where u.id = :id and u.userNm = :userNm", UserEntity.class)
.setParameter("id", 1234)
.setParameter("userNm", "admin");
6) JPQL JOIN
💡 JPQL JOIN
- JPQL에서 JOIN은 SQL에서 사용하는 JOIN과 비슷하게 사용되지만, 객체지향 쿼리 언어이기 때문에 테이블 간의 관계가 아닌 객체 간의 관계에 기반을 두고 있습니다.
1. INNER JOIN
💡 INNER JOIN
- 두 테이블 간에 공통적으로 존재하는 데이터만을 반환합니다.
💡 INNER JOIN 사용예시
- UserEntity를 기준으로 PassportEntity 엔티티를 INNER JOIN 하고 있는 예시입니다.
- 해당 INNER JOIN의 경우 INNER를 생략하고 JOIN만 사용해도 동일하게 사용이 가능합니다.
List<UserEntity> resultList = em
.createQuery("SELECT t1, t2 FROM UserEntity t1 JOIN PassportEntity t2 ON t1.userId = t2.passportId")
.getResultList();
2. LEFT JOIN
💡 LEFT JOIN
- 왼쪽 테이블의 모든 데이터와 오른쪽 테이블에서 일치하는 데이터를 반환합니다.
- 오른쪽 테이블에서 일치하는 데이터가 없는 경우에는 NULL을 반환합니다.
💡 LEFT JOIN
- UserEntity를 기준으로 OrderEntity 엔티티와 LEFT JOIN을 하고 있는 예시입니다.
List<UserEntity> resultList2 = em
.createQuery("SELECT t1, t2 FROM UserEntity t1 LEFT JOIN OrderEntity t2 ON t1.userId = t2.userId")
.getResultList();
💡 데이터베이스의 조인에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
7) JPQL 쿼리 결과 조회 방식
💡 쿼리 결과 조회 방식
- 쿼리를 수행한 후 결과를 조회하는 방식에는 getSingleResult와 getResultList 두 가지 방식이 있습니다.
1. getSingleResult()
💡 getSingleResult()
- 이 방식은 쿼리의 결과로 ‘단일 엔티티 객체’를 반환합니다.
- 만약 쿼리 결과가 없거나 결과가 2개 이상인 경우 예외가 발생합니다.
💡 [참고] 쿼리 결과가 없거나 결과가 2개 이상인 경우 아래와 같은 예외가 발생합니다.
예외 | 설명 |
javax.persistence.NoResultException | 쿼리 결과가 없을 때 발생하는 예외 |
javax.persistence.NonUniqueResultException | 쿼리 결과가 1개 이상일 때 발생하는 예외 |
💡 getSingleResult() 사용예시
- getSingleResult()를 사용하면 쿼리의 결과를 단일 엔티티 객체로 반환받습니다.
UserEntity userEntity = em
.createQuery("select u from UserEntity u where u.id = :id and u.userNm = :userNm", UserEntity.class)
.setParameter("id", 1234)
.setParameter("userNm", "admin")
.getSingleResult();
💡 [참고] 단건 예외처리를 피하는 방법
- 단건 데이터가 들어온다는 가정하에 getResultList()를 통해서 데이터를 받습니다.
- 데이터가 존재하지 않은 경우 null을 반환하며 데이터가 존재하는 경우 첫 번째 객체를 반환합니다.
List<UserEntity> userList = em
.createQuery("select u from UserEntity u where u.id = :id and u.userNm = :userNm", UserEntity.class)
.setParameter("id", 1234)
.setParameter("userNm", "admin")
.getResultList();
UserEntity user = userList.isEmpty() ? null : userList.get(0);
2. getResultList()
💡 getResultList()
- 이 방식은 쿼리의 결과를 ‘List 형태의 객체’로 반환합니다.
- 쿼리 결과가 없는 경우 빈 리스트를 반환합니다.
💡 getResultList() 사용예시
- getResultList()를 사용하면 쿼리의 결과를 List 형태로 반환받습니다.
List<UserEntity> userEntityList = em
.createQuery("select u from UserEntity u where u.id = :id and u.userNm = :userNm", UserEntity.class)
.setParameter("id", 1234)
.setParameter("userNm", "admin")
.getResultList();
8) JPQL 한계
1. 동적 쿼리 사용의 어려움
💡 동적 쿼리 사용의 어려움
- SQL과 같이 동적 쿼리를 사용하는 것이 어렵습니다. 동적 쿼리란 실행 시점에 SQL문이 결정되는 쿼리를 의미합니다.
- 이러한 동적 쿼리는 보통 사용자의 입력값에 따라 SQL문이 변경되는 경우에 사용됩니다. 이런 경우에 JPQL보다는 Criteria API나 QueryDSL 같은 도구를 사용하는 것이 더 효과적입니다.
💡 동적 쿼리 사용의 어려움 예시
- UserEntity에서 사용자의 이름에 따른 조건절로 수행할 수도 있지만, 이메일에 따라 조건절을 동적으로 처리가 필요한 경우 createQuery() 쿼리 내에서 처리가 불가능하기에 비즈니스 로직에서 분기처리가 필요하기에 불편함이 있습니다.
/**
* 사용자를 조회할때, 이름으로 조회 할수도 있지만, 이메일을 통해 조회를 하는 경우 '동적쿼리'가 수행되어야 합니다.
* 이를 위해 비즈니스 로직의 분기 처리가 필요할 수 있기에 곤란합니다.
*/
List<UserEntity> resultList = em
.createQuery("select u from UserEntity u where u.name = :name")
.getResultList();
2. 문자열 구성 대한 오류 발생
💡 문자열 구성 대한 오류 발생
- 쿼리를 문자열로 작성하기 때문에, 쿼리 문자열 내부에 오타나 문법 오류가 있을 경우 이를 컴파일 시점에 체크할 수 없습니다. 이로 인해 실행 시점에 예상치 못한 오류가 발생할 수 있습니다.
💡 문자열 구성 대한 오류 발생 예시
- 생성한 쿼리 내에서 ‘u.namee = :name’ 부분에 ‘namee’라는 텍스트 입력 실수가 발생하였지만 컴파일 단계에서 체크할 수 없기에 해당 쿼리가 호출되기 전까지 확인이 불가능하기에 잠재적인 오류를 가지고 있습니다.
// 오타나 문법 오류가 포함된 JPQL 쿼리
List<UserEntity> resultList = em
.createQuery("select u from UserEntity u where u.namee = :name")
.getResultList();
3. SQL의 모든 기능을 사용할 수 없음
💡 SQL의 모든 기능을 사용할 수 없음
- JPA의 표준 스펙이기 때문에, 특정 데이터베이스에 특화된 SQL 문법을 사용할 수 없습니다.
- 예를 들어, Oracle의 CONNECT BY나 SQL Server의 PIVOT 같은 문법은 JPQL에서 지원하지 않습니다. 이런 경우에는 native query를 사용해야 합니다.
4. 복잡한 Join 문제
💡 복잡한 Join 문제
- JPQL에서 복잡한 Join 조건이나 서브 쿼리를 사용하는 것이 어렵습니다. 이런 경우에도 SQL을 직접 사용하는 것이 더 효과적입니다.
💡 복잡한 Join 문제 사용예시
- JPQL에서 다음과 같은 서브 쿼리를 사용한 복잡한 Join 조건은 작성할 수 없습니다.
- 이런 경우, native query를 사용하여 직접 SQL 쿼리를 작성해야 합니다.
SELECT * FROM Orders o
JOIN (
SELECT CustomerID, COUNT(OrderID) as OrderCount
FROM Orders
GROUP BY CustomerID
) oc ON o.CustomerID = oc.CustomerID
WHERE oc.OrderCount > 5;
💡 [참고] 해당 내용과 관련된 소스코드는 아래에서 확인이 가능합니다.
오늘도 감사합니다. 😀
반응형