💡 Spring Boot JPA(Java Persistence API) - 데이터베이스를 쉽게 다루기 위한 ‘데이터 액세스 기술’로 ORM(Object-Relational Mapping) 기법을 사용하여 자바 애플리케이션에서 사용하는 객체와 관계형 데이터베이스 사이의 매핑을 관리하는 ORM 기술에 대한 API 표준 명세서(인터페이스) 의미합니다.
- 이 API를 사용하여 개발자가 직접적인 SQL을 작성하지 않고도 데이터베이스에서 데이터를 저장, 업데이트, 삭제, 조회하는 등의 작업을 수행할 수 있게 해 줍니다. - JPA는 표준화된 API를 제공함으로써, 다양한 ORM 프레임워크(예: Hibernate, EclipseLink, OpenJPA 등)와의 호환성을 보장합니다. 이로 인해 개발자는 특정 ORM 프레임워크에 종속되지 않고 필요에 따라 다른 프레임워크로 쉽게 전환할 수 있습니다.
- SQL을 기반으로 한 객체 모델용 쿼리 언어입니다. SQL과 매우 유사한 형태지만 데이터베이스 ‘테이블과 컬럼’이 아닌 ‘자바 클래스와 변수(객체)’에 작업을 수행한다는 점에서 차이가 있습니다. - 그렇기에 데이터베이스 테이블을 대상으로 쿼리 하는 것이 아닌 엔티티(객체)를 대상으로 쿼리를 수행합니다.
💡 SQL 사용예시
- SQL문에서는 ‘테이블과 컬럼’을 대상으로 쿼리를 작성합니다.
💡 JPQL 사용예시
- JPQL에서는 SQL문과 형태가 비슷합니다. 단, ‘테이블과 컬럼’을 대상으로 하는 것이 아닌 ‘엔티티(클래스)와 변수’를 대상으로 쿼리를 작성합니다.
- 반환되는 ‘타입이 명확하지 않을 때’ 사용하는 클래스입니다. 다양한 타입의 결과로 반환받을 수 있지만 타입체크가 런타임 시점에 수행되어 안정성이 떨어질 수 있습니다.
💡 Query 사용예시
- Query 클래스를 이용하면 쿼리 수행 반환값의 타입을 지정하지 않았습니다.
@PersistenceContextprivate EntityManager em;
Queryquery= em.createQuery("select u from UserEntity u where u.id = :id");
// 단일한 결과를 반환 받습니다 : NoResultException, NonUniqueResultException 오류가 발생 할 수 있음.Objectresult= query.getSingleResult();
// 리스트 형태로 결과를 반환 받습니다.
List<UserEntity> resultList = query.getResultList();
- 쿼리 내에서 ‘:변수명’ 형태로 선언한 후에 setParameter 메서드를 사용하여 값을 설정하는 방식을 의미합니다.
@PersistenceContextprivate 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");
- 쿼리 내에 파라미터를 '?위치번호' 형태로 선언한 후 setParameter 메서드를 사용하여 값을 설정합니다. 위치번호는 1부터 시작합니다. - 해당 방식은 권장하지 않습니다. 이유는 복잡한 쿼리문의 같은 경우 위치값이 변할 수 있기에 직접적인 매핑이 되는 이름 기준 파라미터 바인딩을 권장하고 있습니다.
@PersistenceContextprivate 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");
- 쿼리 결과로 ‘특정 필드(컬럼)’만 선택하여 가져오는 것을 의미합니다. 프로젝션 대상은 엔티티 프로젝션, 임베디드 타입 프로젝션, 스칼라 타입 프로젝션으로 선택이 가능합니다. - 이를 통해 필요한 정보만 선별적으로 가져올 수 있어서 애플리케이션의 성능을 향상할 수 있으며, JOIN 연산을 사용하여 여러 테이블의 정보를 가져올 때 프로젝션을 활용하면 많은 도움이 됩니다.
프로젝션
유형
예시
엔티티 프로젝션
엔티티를 직접 선택하여 조회하는 방식. 엔티티에 있는 모든 필드가 조회됨
모든 엔티티의 정보를 조회하고자 할때 사용
임베디드 타입 프로젝션
엔티티의 특정 부분(임베디드 타입)을 직접 조회하는 방식. 여러 속성을 한번에 묶어서 조회할 수 있음
엔티티 내에 필드가 임베디드 타입인 경우(여러 속성을 묶는 경우)에 사용하고자 할때 사용
스칼라 타입 프로젝션
엔티티 내의 특정 필드들(개별 속성들)을 선택하여 조회하는 방식. 필요한 정보만 조회 가능
- 해당 예시에서는 UserEntity에 대해 Alias로 ‘u’를 지정하였습니다. 지정된 값을 SELECT문 내에서 Entity Alias인 ‘u’를 직접 지정하여 조회하고 있습니다. - 즉 u를 조회함에 따라 UserEntity에 있는 모든 필드값이 조회가 됩니다.
@PersistenceContextprivate 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");
💡 임베디드 타입 프로젝션(Embedded Type Projection) - JPQL의 조회 방식 중 ‘엔티티의 특정 부분(임베디드 타입)’을 직접 조회하는 방식을 의미합니다.
- 즉 여러 속성을 한 번에 묶어서 조회하는 것이 가능하므로 데이터의 관계성이나 구조를 보존하면서 필요한 부분만 조회할 수 있다는 장점이 있습니다
💡 임베디드 타입 프로젝션(Embedded Type Projection) 사용예시
- Order Entity에서 m.address라는 필드(컬럼)을 조회하고 있습니다. 해당 address의 경우는 @Embedded 어노테이션으로 지정된 필드입니다.
@PersistenceContextprivate EntityManager em;
List<Address> resultList = em
.createQuery("select m.address from Order m", Address.class)
.getResultList();
💡 Order Entity
- Order Entity 내에서 Address라는 객체로 필드를 구성하였습니다. 해당 필드는 @Embedded 어노테이션으로 선언된 임베디드 타입입니다.
@EntitypublicclassOrder {
@Id@GeneratedValueprivate Long id;
@Embeddedprivate Address address;
}
💡 Address 객체
- @Embeddable로 선언된 Address 클래스에는 street, city, state, zipCode 필드로 구성되어 있습니다.
- 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");
- 이 방식은 쿼리의 결과로 ‘단일 엔티티 객체’를 반환합니다. - 만약 쿼리 결과가 없거나 결과가 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);
- 이 방식은 쿼리의 결과를 ‘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();
- SQL과 같이 동적 쿼리를 사용하는 것이 어렵습니다. 동적 쿼리란 실행 시점에 SQL문이 결정되는 쿼리를 의미합니다. - 이러한 동적 쿼리는 보통 사용자의 입력값에 따라 SQL문이 변경되는 경우에 사용됩니다. 이런 경우에 JPQL보다는 Criteria API나 QueryDSL 같은 도구를 사용하는 것이 더 효과적입니다.
💡 동적 쿼리 사용의 어려움 예시
- UserEntity에서 사용자의 이름에 따른 조건절로 수행할 수도 있지만, 이메일에 따라 조건절을 동적으로 처리가 필요한 경우 createQuery() 쿼리 내에서 처리가 불가능하기에 비즈니스 로직에서 분기처리가 필요하기에 불편함이 있습니다.
/**
* 사용자를 조회할때, 이름으로 조회 할수도 있지만, 이메일을 통해 조회를 하는 경우 '동적쿼리'가 수행되어야 합니다.
* 이를 위해 비즈니스 로직의 분기 처리가 필요할 수 있기에 곤란합니다.
*/
List<UserEntity> resultList = em
.createQuery("select u from UserEntity u where u.name = :name")
.getResultList();
- JPA의 표준 스펙이기 때문에, 특정 데이터베이스에 특화된 SQL 문법을 사용할 수 없습니다. - 예를 들어, Oracle의 CONNECT BY나 SQL Server의 PIVOT 같은 문법은 JPQL에서 지원하지 않습니다. 이런 경우에는 native query를 사용해야 합니다.
- JPQL에서 복잡한 Join 조건이나 서브 쿼리를 사용하는 것이 어렵습니다. 이런 경우에도 SQL을 직접 사용하는 것이 더 효과적입니다.
💡 복잡한 Join 문제 사용예시
- JPQL에서 다음과 같은 서브 쿼리를 사용한 복잡한 Join 조건은 작성할 수 없습니다. - 이런 경우, native query를 사용하여 직접 SQL 쿼리를 작성해야 합니다.
SELECT * FROM Orders o
JOIN (
SELECT CustomerID, COUNT(OrderID) as OrderCount
FROM Orders
GROUPBY CustomerID
) oc ON o.CustomerID = oc.CustomerID
WHERE oc.OrderCount > 5;