목차
08 Spring Data JPA 활용
ㄴ8.1 프로젝트 설정
ㄴ8.2 JPQL
ㄴ8.3 쿼리 메서드 살펴보기
ㄴ8.3.1 쿼리 메서드의 생성
ㄴ8.3.2 쿼리 메서드의 주제 키워드
ㄴ8.3.3 쿼리 메서드의 조건자 키워드
ㄴ8.4 정렬과 페이징처리
ㄴ8.4.1 정렬 처리하기
ㄴ8.4.2 페이징 처리
ㄴ8.5 @Query 어노테이션 사용하기
ㄴ8.6 QueryDSL 적용하기
ㄴ8.6.1 QueryDSL이란?
ㄴ8.6.2 QueryDSL의 장점
ㄴ8.6.3 QueryDSL을 사용하기 위한 프로젝트 설정
ㄴ8.6.4 기본적인 QueryDSL 사용하기
ㄴ8.6.5 QuertdslPredicateExecutor, QuerydslRepositorySupport 활용
ㄴ8.7 [한걸음 더] JPA Auditing 적용
ㄴ8.7.1 JPA Auditing 기능 활성화
ㄴ8.7.2 BaseEntity 만들기
ㄴ8.8 정리
6장에서 Spring Data JPA의 기본 기능을 살펴보았고, 레포지토리를 활용해 데이터베이스에 접근하고 기본적인 CRUD 기능을 사용해봤다.
이번 장에서는 Spring Data JPA에서 제공하는 기능들을 더 알아보고 다양한 활용법을 살펴볼 것이며,
이 과정에서 레포지토리 예제를 작성하고 레포지토리의 활용법을 테스트 코드를 통해 살펴볼 것이다.
8.1 프로젝트 설정
Spring version 2.5.6
groupId : com.springboot
artifactId : advanced_jpa
name : advanced_jpa
Developer Tools : Lombok, Spring Configuration Processor
Web : Spring Web
SQL : Spring Data JPA, MariaDB Driver
지난 6장에서 jpa를 다룬 자바 파일을 가져와 기본적인 프로젝트를 생성하면 됩니다.
8.2 JPQL
JPQL(JPA Query Language) : JPA에서 사용할 수 있는 쿼리
JPQL의 문법은 SQL과 매우 비슷해 DB 쿼리에 익숙하다면 어렵지 않게 사용할 수 있다.
차이점
SQL에서는 테이블이나 칼럼의 이름을 사용하는 것과 달리
JPQL은 엔티티 객체를 대상으로 수행하는 쿼리이고, 매핑된 엔티티의 이름과 필드의 이름을 사용한다.
8.3 쿼리 메서드 살펴보기
레포지토리는 JpaRepository를 상속받는 것만으로도 다양한 CRUD 메서드를 제공함.
이러한 메서드는 기본 메서드이고, 기본 메서드들은 식별자 기반으로 생성되기 때문에,
결국 별도의 메서드를 정의해서 사용하는 경우가 많다.
이 때 간단한 쿼리문을 작성하기 위해 사용되는 것이 쿼리 메서드이다.
쿼리 메서드의 생성
쿼리 메서드는 크게 동작을 결정하는 주제(Subject)와 서술어(Predicate)로 구분한다.
'find ... By', ' exists ... By'와 같은 키워드로 쿼리의 주제를 정하며 By는 서술어의 시작을 나타내는 구분자 역할을 한다.
서술어 부분은 검색 및 정렬 조건을 지정하는 영역이다.
기본적으로 엔티티의 속성으로 정의할 수 있고, AND나 OR를 사용해 조건을 확장하는 것도 가능하다.
서술어에 들어가는 엔티티의 속성 식(Expression)은 위의 예시와 같이 엔티티에서 관리하고 있는 필드(DB의 컬럼)만 참조할 수 있다.
쿼리 메서드의 주제 키워드
쿼리 메서드의 주제 부분에 사용할 수 있는 주요 키워드는 다음과 같다.
** 조회하는 기능을 수행하는 키워드 **
- find...By
- read...By
- get...By
- query...By
- search...By
- stream...By
조회하는 기능을 수행하는 키워드이며, '...'으로 표시한 영역에는 도메인(엔티티)를 표현할 수 있다.
그러나 레포지토리에서 이미 도메인을 설정(JpaReposritory<도메인(엔티티), 도메인(엔티티)의 pk타입>)해놓기 때문에 중복으로 판단해 생략하기도 한다.
리턴 타입으로는 Collection이나 Stream에 속한 하위 타입을 설정할 수 있다.
** 특정 데이터가 존재하는지 확인하는 키워드 **
- exists...By
리턴 타입으로는 boolean 타입을 사용한다.
** 조회 쿼리를 수행한 후 쿼리 결과로 나온 레코드의 개수를 리턴 **
- count...By
** 삭제 쿼리를 수행하는 키워드 **
- delete...By
- remove...By
리턴 타입은 없거나, 삭제한 횟수를 리턴함
** 쿼리를 통해 조회된 결괏값의 개수를 제한하는 키워드 **
- ....First<number>....
- ....Top<number>....
두 키워드는 동일한 동작을 수행하며, 주제와 By 사이에 위치한다.
일반적으로 이 키워드는 한 번의 동작으로 여러 건을 조회할 때 사용되며, 단 건으로 조회하기 위해서는 <number>를 생락하면 된다.
쿼리 메서드의 조건자 키워드
JPQL의 서술어 부분에서 사용할 수 있는 몇가지 조건자 키워드들
- Is
-> 값의 일치를 조건으로 사용하는 조건자 키워드
-> 생략되는 경우가 많으며 Equals와 동일한 기능을 수행함.
- (Is)Not
-> 값의 불일치를 조건으로 사용하는 조건자 키워드
-> Is는 생략하고, Not 키워드만 사용할 수 있다.
- (Is)Null, (Is)NotNull
-> 값이null인지 검사하는 조건자 키워드
- (Is)True, (Is)False
-> boolean 타입으로 지정된 칼럼값을 확인하는 키워드
- And, Or
-> 여러 조건을 묶을 때 사용하는 키워드
- (Is)GreaterThan, (Is)LessThan, (Is)Between
-> 숫자나 DateTime 칼럼을 대상으로 한 비교 연산에 사용할 수 있는 조건자 키워드
-> GreaterThan, LessThan 키워드는 비교 대상에 대한 초과/미만의 개념으로 비교 연산을 수행하고, 경곗값을 포함하려면 Equal키워드를 추가하면된다.
- StartingWith(==StartWith), (Is)EndingWith(==EndWith), (Is)Containing
-> 컬럼값에서 일부 일치 여부를 확인하는 조건자 키워드
-> SQL 쿼리문에서 값의 일부를 포함하는 값을 추출할 때 사용하는 '%' 키워드와 동일한 역할을 하는 키워드
-> 자동으로 생성되는 SQL문을 보면 Containing 키워드는 문자열의 양 끝, StartingWith 키워드는 문자열의 앞,
EndingWith 키워드는 문자열의 끝에 '%'가 배치된다.
-> Like 키워드는 코드 수준에서 메서드를 호출하면서 전달하는 값에 %를 명시적으로 입력 해야 한다.
8.4 정렬과 페이징처리
애플리케이션에서 자주 사용되는 정렬과 페이징 처리는 앞서 소개한 쿼리 메서드를 작성하는 방법을 기반으로 수행할 수 있다.
물론 다른 방법도 존재하지만 이번 장에서는 기본적인 정렬과 페이징 처리 방법을 알아본다.
정렬 처리하기
- OrderBy 구분 사용 +Asc or Desc
2번 Line인 findByNumberAsc(String name) 메서드를 호출할 때 나오는 하이버네이트 로그는 다음과 같다.
아펀 쿼리 메서드들에서는 조건 구문에서 조건을 여러개 사용하기 위해 And나 Or 키워드를 사용했으나
정렬 구문은 해당 키워드 사용없이 우선순위를 차례대로 작성하면 된다.
그러나 정렬하는 기준이 다양하고 많으면 메서드의 이름이 가독성이 떨어진다.
이 점을 해결하기 위해 다음과 같이 매개변수를 활용해 정렬할 수 있다.
** 사용 예제 **
Sort 클래스는 내부 클래스로 정의돼 있는 Order 객체를 활용해 정렬 기준을 생성한다.
Order 객체에는 asc와 desc 메서드가 포함돼있어 이 메서드를 통해 오름차순/내림차순을 지정한다.
여러 정렬 기준을 사용할 경우에는 2번 Line처럼 콤마를 사용해 구분한다.
해당 매개변수를 사용함에도 매개변수가 길어지는 것이 싫으면 Sort 부분을 하나의 메서드로 분리해서 작성하는 방법도 존재한다.
페이징 처리
페이징 = 데이터베이스의 레코드를 개수로 나눠 페이지를 구분하느 것을 의미
예를 들어 25개의 레코드가 있다면 레코드를 7개씩 총 4개의 페이지로 구분하고 그중에서 특정 페이지를 가져오는 것이다.
흔히 볼 수 있는 웹 페이지에서 각 페이지를 구분해서 데이터를 제공할 때 그에 맞게 데이터를 요청하는 것이라고 생각하면된다.
JPA에서는 이 같은 페이징 처리를 위해 Page와 Pageable를 사용한다.
** 페이징 처리 예시 **
페이징 쿼리 메서드를 호출 할 때 리턴 타입으로 Page 객체를 받아야하기 때문에 Page<Product>로 타입을 선언해서 객체를 리턴 받는다.
그리고 Pageable 파라미터를 전달하기 위해 PageRequest 클래스를 사용했으며, PageRequest는 Pageable의 구현체이다.
일반적으로 PageRequest는 of 메서드를 통해 PageRequest 객체를 생성한다.
of 메서드는 매개변수에 따라 다양한 형태로 오버로딩 되어있는데 다음과 같은 매개변수 조합을 지원한다.
예제 8.19의 메서드가 호출될 때 하이버네이트에서 생성하는 쿼리는 다음과 같다.
쿼리 로그에서 select 쿼리에 limit 쿼리가 포함되어있는걸 확인 할 수 있다.
만약 페이지 번호를 0이 아닌 1 이상의 숫자로 설정하면 offset 키워드도 포함되어 레코드 목록을 구분해서 가져오게된다.
이렇게 리턴 받은 객체를 출력하면 다음과 같은 출력결과를 확인 할 수 있다.
페이지를 구성하는 세부적인 값을 보려면 반환값에 .getContent()를 사용하면된다.
getContent()는 배열 형태로 값을 출력한다.
8.5 @Query 어노테이션 사용하기
데이터베이스에서 값을 가져올 때는 앞 절에서 소개한 것처럼 메서드의 이름만으로 쿼리 메서드를 생성할 수도 있고,
이번 절에서 살펴볼 @Query 어노테이션을 사용해 직접 JPQL을 작성할 수도 있다.
JPQL을 사용하면 JPA 구현체에서 자동으로 쿼리 문장을 해석하고 실행하게 된다.
만약 데이터베이스를 다른 데이터베이스로 변경할 일이 없다면
직접 해당 데이터베이스에 특화된 SQL을 작성할 수 있으며,
주로 튜닝된 쿼리를 사용하고자 할 때 직접 SQL을 작성한다.
먼저 기본적인 JPQL을 사용해 상품정보를 조회하는 메서드를 레포지토리를 추가한다.
@Query 어노테이션을 사용해 JPQL 형식의 쿼리문을 작성한다.
조건문에서 사용한 '?1'는 파라미터를 전달받기 위한 인자에 해당된다.
1은 첫 번째 파라미터를 의미한다.
이와 같은 방식은 파라미터의 순서가 바뀌면 오류가 발생할 가능성이 있어 @Param 어노테이션을 사용하는 것이 좋다.
*** @Query 어노테이션 @Param 어노테이션을 사용한 메서드
-> 파라미터를 바인딩하는 방식으로 메서드를 구현하면 코드의 가독성이 높아지고, 유지보수가 수월해짐.
@Query를 사용하면 엔티티 타입이 아니라 원하는 칼럼의 값을 추출할 수 있다.
8.6 QueryDSL 적용하기
앞에는 @Query 어노테이션을 사용해 직접 JPQL의 쿼리를 작성하는 방법을 알아본 것이다.
메서드의 이름을 기반으로 생성하는 JPQL의 한계는 @Query 어노테이션으로 대부분 해소할 수 있지만,
직접 문자열을 입력하기 때문에 컴파일 시점에 에러를 잡지 못하고 런타임 에러가 발생할 수 있다.
쿼리 문자열이 잘못된 경우에는 애플리케이션이 실행된 후 로직이 실행되고 나서야 오류를 발견할 수 있다.
--> 개발 환경에서는 문제가 없는 것처럼 보이다가 실제 운영 환경에 애플리케이션을 배포하고난 뒤
오류가 발견되는 리스크가 유발됨
이런 문제를 해결하기 위해 사용하는 것이 QueryDSL이다.
QueryDSL은 문자열이 아니라 코드로 쿼리를 작성할 수 있도록 도와줌.
QueryDSL이란?
QueryDSL은 정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 지원하는 프레임워크이다.
문자열이나 XML 파일을 통해 쿼리를 작성하는 대신 QueryDSL이 제공하는 플루언트(Fluent) API를 활용해 쿼리를 생성할 수 있다.
QueryDSL의 장점
- IDE가 제공하는 코드 자동 완성 기능을 사용할 수 있다.
- 문법적으로 잘못된 쿼리를 허용하지 않는다. 따라서 정상적으로 활용된 QueryDSL은 문법 오류를 발생시키지 않는다.
- 고정된 SQL 쿼리를 작성하지 않기 때문에 동적으로 쿼리를 생성할 수 있다.
- 코드로 작성하므로 가독성 및 생산성이 향상된다.
- 도메인 타입 프로퍼티를 안전하게 참조할 수 있다.
QueryDSL을 사용하기 위한 프로젝트 설정
QueryDSL을 사용하기 위한 pom.xml파일에 의존성을 다음과 같이 추가한다.
plubgins 태그에 QueryDSL을 사용하기 위한 APT 플러그인 추가
(APT란? Annotation Processing Tool의 줄임말이고, 어노테이션으로 정의된 코드를 기반으로 새로운 코드를 생성하는 기능 + 클래스를 컴파일하는 기능 제공)
** gradle 참고 **
plugins {
id 'org.springframework.boot' version '2.5.4'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
id 'java'
}
group = 'com'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'com.querydsl:querydsl-jpa'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main.java.srcDir querydslDir
}
configurations {
querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
options.annotationProcessorPath = configurations.querydsl
}
기본적인 QueryDSL 사용하기
** JPAQuery를 활용한 QueryDSL 테스트 코드 **
@PersistenceContext
EntityManager entityManager;
@Test
void queryDslTest() {
JPAQuery<Product> query = new JPAQuery(entityManger);
QProduct qProduct = QProduct.product;
List<Product> productList = query
.from(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
for (Product product : productList) {
System.out.println("-----------------");
System.out.println();
System.out.println("Product Number : " + product.getNumber());
System.out.println("Product Name : " + product.getName());
System.out.println("Product Price : " + product.getPrice());
System.out.println("Product Stock : " + product.getStock());
System.out.println();
System.out.println("-----------------");
}
}
-> QueryDSL에 의해 생성된 Q도메인 클래스를 활용하는 코드.
-> 다만, Q도메인 클래스와 대응되는 테스트 클래스가 없으므로 엔티티 클래스에 대응되는 레포지토리의 테스트 클래스에 포함해도 무관
JPAQueryFactory 객체를 @Bean 객체로 등록해두면 앞에서 작성한 예제처럼 JPAQueryFactory를 초기화하지 않고,
스프링 컨테이너에서 가져다 쓸 수 있다.
- List<T> fetch() : 조회 결과를 리스트로 반환한다.
- T fetchOne : 단건의 조회 결과를 반환
- T fetchFrist() : 여러 건의 조회 결과 중 1건을 반환. (내부로직 > '.limit(1).fetchOne()'으로 구현되어 있음)
- Long fetchCount() : 조회 결과의 개수를 반환
- QueryResult<T> recthResults() : 조회 결과 리스트와 개수를 포함한 QueryResults를 반환
QuerydslPredicateExecutor, QuerydslRepositorySupport 활용
Spring Data JPA에서는 QueryDSL을 더욱 편하게 사용할 수 있게 QuerydslPredicateExecutor 인터페이스와 QuerydslRepositorySupport 클래스를 제공한다.
QuerydslPredicateExecutor 인터페이스
QuerydslPredicateExecutor 인터페이스는 JpaRepository와 함께 레포지토리에서 QueryDSL을 사용할 수 있게 인터페이스를 제공한다.
** QuerydslPredicateExecutor에서 제공하는 메서드 **
해당 메서드들에서 매개변수로 받는 Predicate타입은 표현식을 작성할 수 있게 QueryDSL에서 제공하는 인터페이스이다.
- 테스트 클래스 생성
- findOne(Predicate predicate) 메서드 사용법1
- findOne(Predicate predicate) 메서드 사용법2
-> 단점 : join, fetch기능을 사용할 수 없음
QuerydslRepositorySupport 추상 클래스 사용하기
해당 클래스 역시 QueryDSL 라이브러리를 사용하는데 유용한 기능을 제공함
가장 보편적으로 사용하는 방식 : CustomRepository 활용해 레포지토리를 구현하는 방식
- ProductRepositoryCustom 인터페이스
- ProductRepositoryCustomImpl 클래스
- 기존에 Product 엔티티 클래스와 매핑된 레포지토리 : ProductRepository
-> 레포지토리 이름이 같을 경우 @Repository("이름")으로 구분 가능하다.
-> 이렇게 JpaRepository에서 제공하는 메서드도 사용 하능하고 Custom한 메서드도 구현체를 통해 사용할 수 있다.
8.7 [한걸음 더] JPA Auditing 적용
JPA에서 'Audit'이란 '감시하다'라는 뜻으로, 각 데이터마다 '누가', '언제' 데이터를 생성했고, 변경했는지 감시한다는 의미로 사용된다.
앞에서 작성한 코드를 보면 알 수 있듯이 엔티티 클래스에는 공통적으로 들어가는 필드가 있다.
예를 들면, '생성 일자'와 '변경 일자' 같은 것이다.
대표적으로 많이 사용되는 필드는 다음과 같다.
- 생성 주체
- 생성 일자
- 변경 주체
- 변경 일자
이러한 필드들은 매번 엔티티를 생성하거나 변경할 때마다 값을 주입해야하는 번거로움이 있다.
이같은 번거로움을 해소하기 위해 Spring Data JPA에서는 이러한 값을 자동으로 넣어주는 기능을 제공한다.
더 진행하기에 앞서 이 기능은 꼭 추가해야하는 기능은 아니지만 9장부터는 이 기능이 적용된 엔티티를 사용할 예정이다.
JPA Auditing 기능 활성화
스프링 부트 애플리케이션에 Auditing 기능을 활성화하는 방법은 간단하다.
main()메서드가 있는 클래스에 @EnableJpaAuditing 어노테이션을 추가하면된다.
그러나 이 방법은 애플리케이션을 테스트하는 일부 상황에서는 오류가 발생할 수 있다.(ex. @WebMvcTest 어노테이션으로 테스트를 수행하는 코드 작성시 예외가 발생할 수 있음)
따라서 별도의 Configuration 클래스를 생성해서 애플리케이션 클래스의 기능을 분리해서 활성화하는 방법을 권장한다.
BaseEntity 만들기
코드의 중복을 없애기 위해서는 각 엔티티에 공통으로 들어가게 되는 컬럼(필드)을 하나의 클래스로 빼내는 작업을 수행해야한다.(Must는 아니지만 많이 사용되는 권장하는 기법)
생성 주체와 변경 주체는 활용할 일이 없기 때문에 제외하고 생성 일자와 변경 일자를 공통으로 들어가는 컬럼으로 두면 다음과 같다.
** 주요 어노테이션 간단 정리 **
- @MappedSuperclass : JPA의 엔티티 클래스가 상속받을 경우 자식 클래스에게 매핑정보를 전달
- @EntityListers : 엔티티를 데이터베이스에 적용하기 전후로 콜백을 요청할 수 있게하는 어노테이션
- AuditingEntityListener : 엔티티의 Auditing 정보를 주입하는 JPA 엔티티 리스너 클래스
- @CreateDate : 데이터 생성 날짜를 자동으로 주입하는 어노테이션
- @LastModifiedDate : 데이터 수정 날짜를 자동으로 주입하는 어노테이션
공통 칼럼으로 뺀 컬럼만 필요한 엔티티의 필드로 넣어두고 BaseEntity를 상속받으면 된다.
@ToString, @EqualsAndHashCode 어노테이션에 적용한 callsuper 속성은 부모 클래스의 필드를 포함하는 역할을 수행함.
이렇게 설정하면 기존에 테스트했던 것 처럼 매번 LocalDateTime.now() 메서드를 사용해 시간을 주입하지 않아도
자동으로 값이 생성되는 것을 볼 수 있다.
** 테스트 코드 **
Product 엔티티에 생성일자를 입력하지 않은 상태에서 데이터베이스에 저장했으나
출력 결과는 생성일자가 같이 출력되는 것을 볼 수 있다.
** Tip **
JPA Auditing 기능에는 @CreateBy, @ModifiedBy 어노테이션도 존재한다.
누가 엔티티를 생성하고 수정했는지 자동으로 값을 주입하는 기능이고
사용하기 위해서는 AuditorAware를 스프링 빈으로 등록할 필요가 있다.
8.8 정리
ORM의 개념
자바의 표준 ORM 기술 스펙 = JPA
대부분의 로직에서 데이터를 가공해서 데이터베이스에 저장하거나 값을 효율적으로 가져오는 부분이 중요하다.
'BookStudy > 스프링 부트 핵심 가이드' 카테고리의 다른 글
[스프링 부트 핵심 가이드] 10 유효성 검사와 예외 처리 (0) | 2023.10.01 |
---|---|
[스프링 부트 핵심 가이드] 09 연관관계 매핑 (0) | 2023.09.23 |
[스프링 부트 핵심가이드] 06. 데이터베이스 연동 (0) | 2023.09.08 |
[스프링 부트 핵심 가이드] 05. API를 작성하는 다양한 방법 (0) | 2023.08.31 |
[스프링 부트 핵심 가이드] 04. 스프링 부트 애플리케이션 개발하기 (0) | 2023.08.30 |