💡 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에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
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 사용에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
- 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();
💡 쿼리 결과 조회 방식 - 쿼리를 수행한 후 결과를 조회하는 방식에는 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;