JPA와 Hibernate
JPA
JPA (Java Persistence API)는 자바 진영의 ORM (Object-Relational Mapping) 기술 표준으로 채택된 인터페이스 (Interface)의 모음이다.
여기서 ORM이란, 객체 지향 언어에서 의미하는 객체 (클래스)와 RDB의 테이블을 자동으로 매핑 (Mapping)하는 방법을 의미한다.
객체 지향 프로그래밍에서는 데이터를 객체 형태로 다루고, RDB는 데이터를 테이블 형태로 저장한다. 이 두 시스템 간에는 다음과 같은 불일치가 존재한다.
Object와 RDB 간의 불일치
- 상속: 객체 지향 언어에서는 상속을 통해 계층 구조를 만들 수 있지만, RDB에는 상속 개념이 없다.
- 연관 관계: 객체 간의 연관 관계 (1:1, 1:N 등)를 표현하는 방법이 데이터베이스의 외래 키와는 다르다.
- 식별자: 객체는 주로 참조를 통해 식별되지만, 데이터베이스는 주로 기본 키를 통해 식별된다.
- 데이터 타입: 객체와 데이터베이스의 데이터 타입이 다를 수 있다.
즉, 이러한 불일치성을 해결하는 역할이 바로 ORM이다.
JPA는 이러한 ORM을 구현하기 위한 표준 인터페이스를 제공하며, JPA를 구현한 대표적인 구현체는 다음과 같다.
JPA의 구현체
- EclipseLink
- Hibernate (가장 널리 사용되는 JPA 구현체로, 다양한 기능과 높은 호환성을 자랑한다.)
- DataNucleus
Hibernate
Hibernate (하이버네이트)는 자바의 ORM 프레임워크로서, JPA가 정의하는 인터페이스를 구현한 JPA 구현체 중 하나이다.
Hibernate는 객체와 관계형 데이터베이스 간의 매핑을 관리하며, 데이터베이스와의 상호작용을 간소화한다. 또한, Query 문을 콘솔창에서 확인하고 싶다면 다음과 같은 설정을 application.properties 파일에 추가하면 된다.
# 쿼리 로그 Show를 true로 설정
# 실행되는 SQL 쿼리를 콘솔에 출력한다.
spring.jpa.show-sql=true
# SQL문을 정렬하여 출력
# 즉, 출력되는 SQL 쿼리를 보기 쉽게 포맷팅한다.
spring.jpa.properties.hibernate.format_sql=true
# 바인딩되는 파라미터 값을 출력
logging.level.org.hibernate.type.descriptor.sql=trace
Spring Data JPA
JPA를 편리하게 사용할 수 있도록 지원하는 스프링 하위 프로젝트 중 하나이다. CRUD 처리에 필요한 인터페이스를 제공하고 Hibernate의 엔티티 매니저를 직접 다루지 않고도 Repository를 정의해 사용함으로써 스프링이 적합한 쿼리를 동적으로 생성하는 방식으로 DB를 조작한다.
Spring Data JPA의 주요 기능
이번 글에서 다루는 Spring Data JPA 기능은 다음과 같다.
- JPQL (Java Persistence Query Language)
- Query Method
- 정렬과 페이징 처리
- @Query annotation
- QueryDSL
JPQL (Java Persistence Query Language)
JPA에서 사용할 수 있는 쿼리를 의미한다. JPQL의 문법은 SQL 문법과 매우 비슷하여 데이터베이스 쿼리에 익숙한 분들이 어렵지 않게 사용할 수 있다. JPQL은 엔티티 객체를 대상으로 수행하는 쿼리이므로 매핑된 엔티티의 이름과 필드의 이름을 사용한다.
// JPQL Basic Example
SELECT P FROM PRODUCT P WHERE P.NUMBER = ?1;
// PRODUCT는 엔티티 타입, NUMBER는 엔티티 객체의 속성을 의미한다.
// 1은 첫 번째 파라미터를 의미한다 (여기서는 매개변수로 받은 number의 값이 들어간다).
Query Method
Repository에서 기본으로 제공되는 메서드 외 별도의 메서드를 정의해야 하는 경우, 쿼리문을 작성하기 위해 사용되는 것이 Query Method이다.
우리가 사용하는 Repository 인터페이스는 기본적으로 JpaRepository를 구현받아 다양한 CRUD 메서드를 제공한다. Query Method는 크게 동작을 결정하는 Subject (주제)와 Predicate (동작)으로 구분한다.
예를 들어, 'findBy...'와 'getBy...' 등이 Subject를 나타내며, 'By'는 Predicate의 시작을 나타내어 구분자의 역할을 한다.
// 리포지터리의 쿼리 메서드 생성 예
List<Person> findByLastNameAndEmail(String lastName, String Email);
// 메서드 명에 들어가 있는 By 이후의 LastName과 Email이 메서드의 매개변수로 들어가 있다.
서술어에 들어갈 엔티티의 속성 식은 엔티티에서 관리하는 필드만 참조할 수 있다.
정렬과 페이징 처리
Query Method를 통한 정렬 처리
Spring Data JPA에서는 Query Method를 통해 간단히 정렬 처리를 할 수 있다.
// 쿼리 메서드의 정렬 처리
// Asc : 오름차순, Desc : 내림차순
List<Product> findByNameOrderByNumberAsc(String name);
List<Product> findByNameOrderByNumberDesc(String name);
기본 쿼리 메서드 (findByName)를 작성한 후 OrderBy 키워드를 삽입하여 정렬하고자 하는 컬럼 (Number)과 오름차순/내림차순 (Asc 혹은 Desc)을 설정하면 정렬이 수행된다.
// Query Method를 해석하면 다음과 같다
List<Product> findByNameOrderByNumberAsc(String name);
// 상품 정보를 이름으로 검색한 후 상품 번호로 오름차순 정렬을 수행한다.
List<Product> findByNameOrderByNumberDesc(String name);
// 상품 정보를 이름으로 검색한 후 상품 번호의 내림차순 정렬을 수행한다.
여러 정렬 기준 사용
Query Method에서 여러 정렬 기준을 사용할 수도 있다.
// 여러 정렬 기준 사용 (And를 붙이지 않음)
List<Product> findByNameOrderByPriceAscStockDesc(String name);
List<Product> findByNameOrderByPrice(String name);
다음과 같이 정렬 키워드를 삽입해서 정렬을 수행하는 것도 가능하지만, 메서드의 이름이 길어질수록 가독성이 떨어지는 문제가 발생할 수 있다. 이를 보완하기 위해 Sort 객체를 매개변수로 주는 방법도 있다.
List<Product> findByName(String name, Sort sort);
// Service 단에서 호출하는 예시
productRepository.findByName("연필", Sort.by(Order.asc("price")));
productRepository.findByName("볼펜", Sort.by(Order.asc("price"), Order.desc("stock")));
Sort 클래스는 내부 클래스로 정의된 Order 객체를 활용해 정렬 기준을 세우고, Order 객체 내 asc() 메서드와 desc() 메서드를 활용하여 오름차순과 내림차순을 지정한다. 여러 정렬 기준을 사용할 경우에는 , (콤마)를 이용해 구분한다.
정렬 기준이 길어져 가독성이 떨어질 때는 호출하는 부분에서 하나의 메서드로 분리하여 Query Method를 호출하는 코드를 작성하는 것도 좋은 방법이다.
class ProductService {
...
productRepository.findByName("연필", getSort());
...
private Sort getSort() { // 필요한 쿼리 메서드를 하나의 메서드로 분리하여 코드를 재활용
return Sort.by(
Order.asc("price"),
Order.desc("stock")
);
}
}
페이징 처리
페이징 (Paging)이란 데이터베이스의 레코드를 개수로 나눠 페이지를 구분하는 것을 의미한다. 이를 통해 많은 양의 데이터를 효율적으로 처리하고, 사용자에게 필요한 부분만 보여줄 수 있다.
JPA에서는 Page와 Pageable 인터페이스를 사용해 페이징을 구현한다.
// 페이징 처리를 위한 쿼리 메서드
Page<Product> findByName(String name, Pageable pageable);
- Page: 페이징된 결과를 포함하는 객체로, 조회된 데이터 리스트와 함께 페이지 정보 (총 페이지 수, 현재 페이지 번호, 총 레코드 수 등)를 제공한다.
- Pageable: 페이징 정보를 담고 있는 객체로, 페이지 번호와 페이지 크기 등을 설정할 수 있다.
// Service 단에서 호출하는 예시
Page<Product> productPage = productRepository.findByName("연필", PageRequest.of(0, 2));
for (Product product : productPage.getContent()) {
System.out.println(product);
}
// 출력결과 예시
// Product{id=1, name='연필', price=100.0, stock=50}
// Product{id=2, name='연필', price=120.0, stock=30}
// Hibernate에서 생성되는 SQL
select product0_.id as id1_0_, product0_.name as name2_0_, product0_.price as price3_0_, product0_.stock as stock4_0_ from product product0_ where product0_.name=? limit ?
select count(product0_.id) as col_0_0_ from product product0_ where product0_.name=?
PageRequest는 Pageable의 구현체이다. 다시 말해 Pageable 객체는 PageRequest를 사용해 생성한다. limit 절은 결과로 반환될 행(row)의 수를 제한하는 데 사용된다.
Page 객체에서 제공하는 getContent() 메서드를 사용해 엔티티의 리스트를 가져올 수 있다.
Pageable of() 메서드의 종류
| 메서드 | 매개변수 설명 | 비고 |
|---|---|---|
| of(int page, int size) | 페이지 번호(0부터 시작), 페이지당 데이터 갯수 | 데이터 정렬X |
| of(int page, int size, Sort) | 페이지 번호(0부터 시작), 페이지당 데이터 갯수, 정렬 | sort에 의해 정렬 |
| of(int page, int size, Direction, String ... properties) | 페이지 번호(0부터 시작), 페이지당 데이터 갯수, 정렬 방향, 속성(컬럼) | Sort.by(direction, properties)에 의해 정렬 |
@Query annotation
@Query 어노테이션은 JPQL 또는 네이티브 SQL을 직접 작성하여 튜닝된 쿼리를 사용할 때 사용하는 어노테이션이다.
기본적으로 Spring Data JPA는 메서드 이름을 기반으로 JPQL을 자동으로 생성한다. 하지만 개발자가 직접 작성한 JPQL 또는 네이티브 SQL을 사용하고 싶다면 @Query 어노테이션을 사용할 수 있다.
public interface ProductRepository extends JpaRepository<Product, Long> {
// @Query 어노테이션을 사용한 사용자 정의 JPQL 쿼리
@Query("SELECT p FROM Product p WHERE p.name = :name")
List<Product> findByNameCustom(@Param("name") String name);
// @Query 어노테이션을 사용한 네이티브 SQL 쿼리
@Query(value = "SELECT * FROM product WHERE name = :name", nativeQuery = true)
List<Product> findByNameNative(@Param("name") String name);
}
native SQL VS JPQL
| native SQL | JPQL |
|---|---|
| 대상 | 데이터베이스 테이블 |
| 데이터베이스 독립성 | 데이터베이스 종속적 |
| 구문 | SQL 구문 직접 사용 |
| 유연성 및 최적화 | 데이터베이스의 고유 기능 사용 |
JPQL은 FROM 절 뒤에 엔티티 타입을 지정하고 별칭을 설정한다. WHERE 절을 통해 SQL과 마찬가지로 조건을 지정하는데 ?1, ?2와 같이 순번을 이용해 인자를 받아올 수도 있다.
// 순번을 이용해 인자를 받아오는 예시
@Query("SELECT p FROM Product p WHERE p.name = ?1")
List<Product> findByName(String name);
하지만 파라미터의 순서가 바뀔 수 있기 때문에 @Param 어노테이션을 사용해 파라미터를 직접 바인딩하는 방식으로 메서드를 구현하면 오류 발생 확률을 줄이고 유지보수를 수월하게 할 수 있다.
마지막으로, @Query 어노테이션은 엔티티 타입이 아니라 원하는 컬럼의 값만 추출할 수도 있고 이때의 리턴 타입은 List<Object[]> 형태로 지정할 수 있다.
QueryDSL
QueryDSL은 타입 안전을 보장하면서 동적 쿼리를 생성할 수 있도록 도와주는 프레임워크이다. JPQL과 Criteria API의 단점을 보완하고자 만들어졌다.
QueryDSL의 장점
- 타입 안전성: QueryDSL은 컴파일 시점에 오류를 검출할 수 있으므로 타입 안전성을 보장한다.
- 동적 쿼리 작성 용이: 동적 쿼리를 작성할 때 코드 가독성이 좋고 유지보수가 용이하다.
- 간결한 코드: QueryDSL을 사용하면 Criteria API보다 훨씬 간결하게 쿼리를 작성할 수 있다.
사용 예시
QueryDSL을 사용하기 위해서는 JPAQueryFactory 객체를 생성하여 쿼리를 작성하면 된다. 엔티티 Q타입을 사용해 SQL과 유사한 방식으로 쿼리를 작성할 수 있다.
import com.querydsl.jpa.impl.JPAQueryFactory;
import static com.example.entity.QProduct.product;
@Service
public class ProductService {
@PersistenceContext
private EntityManager entityManager;
public List<Product> getProductsByPriceGreaterThan(int price) {
JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
return queryFactory.selectFrom(product)
.where(product.price.gt(price))
.fetch();
}
}
- Q타입: QueryDSL은 각 엔티티마다 Q타입 클래스를 생성하며, 이 클래스를 이용해 쿼리를 작성한다.
- JPAQueryFactory: QueryDSL 쿼리를 생성하기 위한 객체이다.
동적 쿼리 작성
QueryDSL은 조건을 추가할 때 BooleanBuilder를 사용해 동적 쿼리를 쉽게 작성할 수 있다.
public List<Product> searchProducts(String name, Integer minPrice, Integer maxPrice) {
JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
BooleanBuilder builder = new BooleanBuilder();
if (name != null) {
builder.and(product.name.contains(name));
}
if (minPrice != null) {
builder.and(product.price.goe(minPrice));
}
if (maxPrice != null) {
builder.and(product.price.loe(maxPrice));
}
return queryFactory.selectFrom(product)
.where(builder)
.fetch();
}
위와 같이 BooleanBuilder를 이용해 조건을 유동적으로 추가함으로써 동적 쿼리를 간편하게 작성할 수 있다.
'🌱 Spring > Spring Data JPA & QueryDSL' 카테고리의 다른 글
| [Spring] QueryDSL (1) | 2024.10.16 |
|---|
