티스토리 뷰
스프링 부트 쇼핑몰 프로젝트 with JPA 라는 책을 읽으며 정리한 내용입니다,
이미 스프링부트로는 개발을 많이 해보았으나 JPA를 공부하기 위해 이 책을 골랐습니다.
이전 포스트를 보려면 아래 링크를 클릭하세요.
[SpringBoot + JPA] tutorial: 스프링 부트 쇼핑몰 프로젝트 with JPA -1. 애플리케이션 생성 및 설정하기
[SpringBoot + JPA] tutorial: 스프링 부트 쇼핑몰 프로젝트 with JPA -2. Controller, DTO(lombok)
[SpringBoot + JPA] tutorial: 스프링 부트 쇼핑몰 프로젝트 with JPA -3. JPA
[SpringBoot + JPA] tutorial: 스프링 부트 쇼핑몰 프로젝트 with JPA -4. Entity 설계하기
[SpringBoot + JPA] tutorial: 스프링 부트 쇼핑몰 프로젝트 with JPA -5. Repository 설계하기
1. 쿼리 메소드
Repository 인터페이스에 간단한 네이밍을 이용하여 메소드를 작성하면 원하는 쿼리를 실행할 수 있습니다.
쿼리 메소드를 이용할 때 가장 많이 사용하는 문법으로 find를 사용합니다.
find + (Entity명) + By + 변수명
엔티티의 이름은 생략 가능하며, By 뒤에는 검색할 때 사용할 변수의 이름을 적어줍니다.
이전 포스팅에서 만들어놨던 ItemRepository에 쿼리메소드를 사용해봅시다.
public interface ItemRepository extends JpaRepository<Item, Long> {
// 상품명으로 데이터 조회
List<Item> findByItemNm(String itemNm);
}
ItemRepositoryTest 클래스에 테스트 코드를 추가합니다.
package com.shop.repository;
import com.shop.constant.ItemSellStatus;
import com.shop.entity.Item;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import java.time.LocalDateTime;
import java.util.List;
// 통합테스트를 위해 스프링부트에서 제공하는 어노테이션
@SpringBootTest
// 테스트 코드 실행시 application-test.properties에 더 높은 우선순위를 부여합니다.
@TestPropertySource(locations="classpath:application-test.properties")
class ItemRepositoryTest {
// ItemRepository를 사용하기 위해서 @Autowired 어노테이션을 사용하여 Bean을 주입합니다.
@Autowired
ItemRepository itemRepository;
// 테스트할 메소드 위에 선언하여 해당 메소드를 테스트 대상으로 지정합니다.
@Test
// Unit5에 추가된 어노테이션으로, 테스트코드 실행 시 테스트명이 노출됩니다.
@DisplayName("상품 저장 테스트")
public void createItemTest(){
Item item = new Item();
item.setItemNm("상품명");
item.setPrice(10000);
item.setItemDetail("상세 설명");
item.setItemSellStatus(ItemSellStatus.SELL);
item.setStockNumber(100);
item.setRegTime(LocalDateTime.now());
item.setUpdateTIme(LocalDateTime.now());
Item saveItem = itemRepository.save(item);
System.out.println(saveItem.toString());
}
// 상품명으로 조회 테스트
@Test
@DisplayName("상품명으로 조회 테스트")
public void findByItemNmTest(){
// 상품 저장
this.createItemTest();
// 상품 조회
List<Item> itemList = itemRepository.findByItemNm("상품명");
for (Item item : itemList){
System.out.println("찾은 상품 : " +item.toString());
}
}
}
findByItemNmTest 메소드를 실행해보면,
insert문과 select문이 있고, 찾은 상품도 확인할 수 있습니다.
더 많은 쿼리 메소드를 살펴봅시다.
Keyword | Sample | JPQL snippet ( JPQL 쿼리의 짧은 예시 ) |
And | findByLastnameAndFirstname | where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | where x.lastname = ?1 or x.firstname = ?2 |
Is, Equals | findByFirstname findByFirstnameIs findByFirstnameEquals |
where x.fistname = ?1 |
Between | findByStartDateBetween | where x.startDate between ?1 and ?2 |
LessThan | findByAgeLessThen | where x.age < ?1 |
LessThanEqual | findByAgeListThanEqual | where x.age <= ?1 |
GreaterThan | findByAgeGreaterThan | where x.age > ?1 |
GreaterThanEqual | findByAgeGreaterThanEqual | where x.age >= ?1 |
After | findByStartDateAfter | where x.startDate > ?1 |
Before | findByStartDateBefore | where x.startDate < ?1 |
IsNull, Null, isNotNull | findByAge(Is)Null | where x.age is null |
NotNull | findByAge(Is)NotNull | where x.age not null |
Like | findByFirstnameLike | where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | where x.firstname not like ?1 |
StartingWith | findByFirstnameStartWith | where x.firstname like ?1% |
EndingWith | findByFirstnameEndingWith | where x.firstname like ?%1 |
Containing | findByFirstnameContaining | where x.firstname like ?%1% |
OrderBy | findByAgeOrderByLastnameDesc | where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | where x.lastname <>?1 |
In | findByAgeIn(Collection<Age> ages) | where x.age in ?1 |
NotIn | findByAgeNotIn(Collection<Age> ages) | where x.age not in ?1 |
True | findByActiveTrue() | where x.active = true |
False | findByActiveFalse() | where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | where UPPER(x.firstname) = UPPER(?1) |
상품명 또는 상품 상세설명을 OR조건을 이용하여 조회해봅시다.
ItemRepository에 findByItemNmOrItemDetail 메소드를 만들어줍니다.
public interface ItemRepository extends JpaRepository<Item, Long> {
// 상품명으로 데이터 조회
List<Item> findByItemNm(String itemNm);
// 상품명 또는 상품 상세설명을 OR조건을 이용하여 조회
List<Item> findByItemNmOrItemDetail(String itemNm, String ItemDetail);
}
ItemRepositoryTest에 테스트메소드를 만들어 실행해봅시다.
// 통합테스트를 위해 스프링부트에서 제공하는 어노테이션
@SpringBootTest
// 테스트 코드 실행시 application-test.properties에 더 높은 우선순위를 부여합니다.
@TestPropertySource(locations="classpath:application-test.properties")
class ItemRepositoryTest {
... 생략 ...
// 상품명 또는 상품 상세설명을 OR조건을 이용하여 조회
@Test
@DisplayName("상품명 또는 상품 상세설명을 OR조건을 이용하여 조회 테스트")
public void findByItemNmOrItemDetailTest(){
// 상품 저장
this.createItemTest();
// 상품 조회
List<Item> itemList = itemRepository.findByItemNmOrItemDetail("상품명", "상세 설명");
for (Item item : itemList){
System.out.println("찾은 상품 : " +item.toString());
}
}
}
참고로 테스트 시 작성한 @DisplayName은 좌측 하단에 설정해둔 이름을 클릭하면
그 테스트의 결과를 로그로 확인할 수 있습니다.
2. @Query 어노테이션
쿼리 메소드를 사용하면서 예제에서는 한두 개 정도의 조건을 사용하여 데이터를 조회했습니다.
조건이 많아질 때 쿼리 메소드를 선언하면 이름이 정말 길어지기도 합니다.
그런 경우, 오히려 일므을 보고 해석하는 게 더 힘들 수 있으므로, 간단한 쿼리를 처리할 때는 유용하지만
복잡한 쿼리를 다루기에는 적합하지 않습니다.
이를 보완하기 위해 Spring Data JPA에서 제공하는 @Query 어노테이션을 이용하면 SQL과 유사한 JPQL(Java Persistence Query Language)이라는 객체지향 쿼리 언어를 통해 복잡한 쿼리도 처리가 가능합니다.
SQL의 경우 데이터베이스 테이블을 대상으로 쿼리를 수행하고,
JPQL은 엔티티 객체를 대상으로 쿼리를 수행합니다. 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리입니다.
JPQL은 SQL을 추상화하여 사용하기 때문에 특정 데이터베이스 SQL에 의존하지 않습니다.
즉, JPQL로 작성을 했다면 데이터베이스가 변경이 되어도 애플리케이션이 영향을 받지 않습니다.
@Query 어노테이션을 이용하여 상품 데이터를 조회하는 예제를 진행해보겠습니다.
상품 상세 설명을 파라미터로 받아 해당 내용을 상품 상세 설명에 포함하고 있는 데이터를 조회하며,
정렬 순서는 가격이 높은 순으로 조회합니다.
기존 ItemRepository.java 에 아래와 같이 @Query 어노테이션을 사용한 쿼리문을 생성합니다.
// 라이브러리 추가
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface ItemRepository extends JpaRepository<Item, Long> {
// 상품명으로 데이터 조회
List<Item> findByItemNm(String itemNm);
// 상품명 또는 상품 상세설명을 OR조건을 이용하여 조회
List<Item> findByItemNmOrItemDetail(String itemNm, String ItemDetail);
// @Query 어노테이션 사용하여 JPQL을 작성한 쿼리문을 넣어줍니다.
@Query("select i from Item i where i.itemDetail like %:itemDetail% order by i.price desc")
List<Item> findByItemDetail(@Param("itemDetail") String itemDetail);
}
테스트코드는 이전에 쿼리메소드를 활용한 조회테스트를 했을 때와 동일합니다.
(ItemRepositoryTest.java)
// @Query 어노테이션을 활용한 상품검색 테스트
@Test
@DisplayName("@Query 어노테이션을 활용한 상품검색 테스트")
public void findByItemDetailTest(){
// 상품 저장
this.createItemTest();
// 상품 조회
List<Item> itemList = itemRepository.findByItemDetail("상세 설명");
for (Item item : itemList){
System.out.println("찾은 상품 : " +item.toString());
}
}
@Query 어노테이션을 이용한 방법에도 단점이 있습니다.
JPQL 문법으로 문자열을 입력하기 때문에 잘못 입력하면 컴파일 시점에 에러를 발견할 수 없습니다.
이를 보완할 수 있는 방법으로 Querydsl을 알아보겠습니다.
3. Querydsl
Querydsl은 JPQL을 코드로 작성할 수 있도록 도와주는 빌더 API입니다.
소스코드로 SQL문을 문자열이 아닌 코드로 작성하기 때문에 컴파일러의 도움을 받을 수 있습니다.
또한 동적으로 쿼리를 생성해주는 게 진짜 큰 장점입니다. JPQL은 문자를 계속 더해야 하기 때문에 작성이 힘듭니다.
- Querydsl의 장점
1. 고정된 SQL문이 아닌 조건에 맞게 동적으로 쿼리를 생성합니다.
2. 비슷한 쿼리를 재사용할 수 있으며 제약 조건 조립 및 가독성을 향상시킬 수 있습니다.
3. 문자열이 아닌 자바 소스코드로 작성하기 때문에 컴파일 시점에 오류를 발견할 수 있습니다.
4. IDE의 도움을 받아 자동완성 기능을 이용할 수 있기 때문에 생산성을 향상시킬 수 있습니다.
Querydsl을 사용하기 위해 pom.xml 파일에 다음과 같이 Querydsl 의존성을 추가합니다.
<dependencies>
... 생략 ...
<!-- Querydsl -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>5.0.0</version>
</dependency>
</dependencies>
그리고, Qdomain이라는 자바코드를 생성하는 플러그인을 추가해줍니다.
엔티티를 기반으로 접두사(prefix)로'Q'가 붙는 클래스들을 자동으로 생성해주는 플러그인입니다.
예를 들어 Item 테이블의 경우 QItem.java 클래스가 자동으로 생성됩니다.
Querydsl을 통해서 쿼리를 생성할 때 Qdomain 객체를 사용합니다.
//// 책에는 아래와같이 plugin을 추가하라고 적혀있으나 에러가납니다.
스프링 3 버전, Java 17버전 이후로부터 javax가 jakarta로 넘어가면서 위와 같은 Plugin은 deprecated 되었다고 합니다.
<build>
<plugins>
...
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
스프링 3 버전, Java 17버전 이후부터는 의존성을 아래와 같이 변경하여 작성합니다.
<!-- Querydsl -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>5.0.0</version>
<classifier>jakarta</classifier>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>5.0.0</version>
<classifier>jakarta</classifier>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-core</artifactId>
<version>5.0.0</version>
</dependency>
저장 후에 컴파일하거나.
프로젝트명에서 마우스오른쪽 버튼 > Maven > Genate Sources and Update Folders 를 클릭하면
target/generated-sources 안에 QItem이 생깁니다.
QItem이 생성되지 않는 경우
FIle > Project Structure > Modules 메뉴에서 generated-sources를 클릭하고 Sources버튼을 클릭해 소스코드로 인식할 수 있게 처리해야 합니다.
셋팅이 완료되었다면 이제 JPQL에서 문자열로 작성하던 쿼리를 자바 소스를 이용해서 동적으로 생성해보겠습니다.
다음 예제는 JPAQueryFactory를 이용한 상품 조회 예제입니다.
ItemRepositoryTest.java에 라이브러리들을 임포트합니다.
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.querydsl.jpa.impl.JPAQuery;
import com.shop.entity.QItem;
// spring 3, java 17 이상이라면 jakarta를, 아니라면 javax.persistence... 를 임포트
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.EntityManager;
아래와 같이 테스트 코드를 입력해줍니다.
... 생략 ...
// JPAQueryFactory를 이용한 상품 조회 테스트
// 영속성 컨텍스트를 사용하기 위해 @PersistenceContext 어노테이션을 통해 EntityManager Bean을 주입합니다.
@PersistenceContext
EntityManager em;
@Test
@DisplayName("JPAQueryFactory를 이용한 상품 조회 테스트")
public void querydslTest(){
// 상품 저장
this.createItemTest();
// JPAQueryFactory를 이용하여 쿼리를 동적으로 생성합니다. 생성자의 파라미터로는 EntityManager 객체를 넣어줍니다.
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
// querydsl을 통해 쿼리를 생성하기 위해 플러그인을 통해 자동으로 생성된 QItem 객체를 이용합니다.
QItem qItem = QItem.item;
//자바 소스코드이지만 SQL문과 비슷하게 소스코드를 작성할 수 있습니다.
JPAQuery<Item> query = queryFactory.selectFrom(qItem)
.where(qItem.itemSellStatus.eq(ItemSellStatus.SELL))
.where(qItem.itemDetail.like("%상세%"))
.orderBy(qItem.price.desc());
// 상품 조회
List<Item> itemList = query.fetch();
for(Item item : itemList){
System.out.println("찾은 상품 : " +item.toString());
}
}
저는 jakarta 관련 라이브러리들은 정상적으로 import 되었으나, 아래 코드부분에서 에러가 났습니다.
java: cannot access javax.persistence.EntityManager
class file for javax.persistence.EntityManager not found
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
JPAQueryFactory가 Querydsl 라이브러리의 클래스인데,
이 클래스 파일을 열어보니 javax.persistence 패키지를 참조하고 있었습니다.
저의 경우 레파지토리 경로로 이동해보니 querydsl-jpa-5.0.0-jakarta.jar 파일과 querydsl-jpa-5.0.0.jar 파일이 있더라구요
querydsl-jpa-5.0.0.jar 파일을 삭제해주었고, maven-compiler-plugin도 설정해주었습니다.
아래와 같이 설정해주고 mvn clean install 을 통해 캐시 정리 및 프로젝트를 재빌드해보면,
오류나던 부분도 사라지고, JPAQueryFactory.class에도 jakarta.persistence.EntityManager가 정상적으로 임포트 되어있는 것을 확인할 수 있었습니다.
// pom.xml에 아래 내용 추가
<build>
<plugins>
... 생략 ...
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
... 생략 ...
이제 JPAQueryFactory를 이용한 상품 조회 테스트를 실행해보면 !
아주 잘 조회되는 것을 확인할 수 있습니다.
JPAQuery 데이터 반환 메소드
JPAQuery 데이터 반환 메소드는 다음과 같습니다.
메소드 | 기능 |
List<T> fetch() | 조회 결과 리스트 반환 |
T fetchOne | 조회 대상이 1건인 경우 제네릭으로 지정한 타입 변환 |
T fetchFirst() | 조회 대상 중 1건만 반환 |
Long fetchCount() | 조회 대상 개수 반환 |
QueryResult<T> fetchResults() | 조회한 리스트와 전체 개수를 포함한 QUeryResults 반환 |
테스트코드에 썼던 query.fetch() 처럼 다음과 같이 사용하면 됩니다.
// 상품 조회
List<Item> itemList = query.fetch();
query.fetchOne();
query.fetchFirst();
query.fetchCount();
query.fetchResults();
QuerydslPredicateExecutor
다음은 QuerydslPredicateExecutor를 이용한 상품 조회 예제입니다.
QuerydslPredicateExecutor는 Spring Data JPA에서 Querydsl을 사용하여 동적 쿼리를 생성하고 실행하기 위한 인터페이스입니다.
이 인터페이스를 사용하면 복잡한 조건이나 동적 쿼리를 쉽게 작성할 수 있으며,
특히 필터링, 검색, 조건부 조회와 같은 기능을 유연하게 구현할 수 있습니다.
QuerydslPredicateExecutor는 기본적으로 아래와 같은 기능을 제공합니다:
1. Predicate를 사용한 검색: Querydsl의 Predicate 인터페이스를 사용하여 동적 쿼리를 작성할 수 있습니다.
2. 기본적인 CRUD 연산을 지원: Spring Data JPA의 CrudRepository를 확장하여 기본적인 CRUD 연산을 제공합니다.
3. 다양한 검색 메서드 제공: 다양한 조건으로 검색할 수 있는 메서드를 제공합니다.
QueryDslPredicateExecutor 인터페이스는 다음 메소드들이 선언되어 있습니다.
Predicate란 '이 조건이 맞다'고 판단하는 근거를 함수로 제공하는 것입니다.
메소드 | 기능 |
long count(Predicate) | 조건에 맞는 데이터의 총 개수 반환 |
boolean exists(Predicate) | 조건에 맞는 데이터 존재 여부 반환 |
Iterable findAll(Predicate) | 조건에 맞는 모든 데이터 반환 |
Page<T> findAll(Predicate, Pageable) | 조건에 맞는 페이지 데이터 반환 |
Iterable findALl(Predicate, Sort) | 조건에 맞는 정렬된 데이터 반환 |
T findOne(Predicate) | 조건에 맞는 데이터 1개 반환 |
Repository에 Predicate를 파라미터로 전달하기 위해
ItemRepository에서 QueryDslPredicateExcutor 인터페이스를 상속받습니다.
public interface ItemRepository extends JpaRepository<Item, Long>
, QuerydslPredicateExecutor<Item> {
...
}
테스트코드에 아래와 같이 QueryDslPredicateExecutor를 이용한 상품 조회 테스트 코드를 작성하고
실행시켜봅시다.
// QueryDslPredicateExecutor를 이용한 상품 조회 테스트
@Test
@DisplayName("QueryDslPredicateExecutor를 이용한 상품 조회 테스트")
public void querydslTest2(){
// 상품 저장
this.createItemTest();
// 쿼리에 들어갈 조건을 만들어주는 빌더로, Predicate를 구현하고 있으며 메소드 체인형식으로 사용할 수 있습니다.
BooleanBuilder booleanBuilder = new BooleanBuilder();
QItem item = QItem.item;
String itemDetail = "상세 설명";
int price = 10003;
// 상품 조회 조건 추가
booleanBuilder.and(item.itemDetail.like("%" + itemDetail +"%"));
booleanBuilder.or(item.price.gt(price));
booleanBuilder.and(item.itemSellStatus.eq(ItemSellStatus.SELL));
// 데이터를 페이징해 조회하도록 PageReuqest.of() 메소드를 이용해 Pageable 객체를 생성합니다.
// 첫번째 인자: 조회할 페이지의 번호, 두번째 인자: 한 페이지당 조회할 데이터의 개수
Pageable pageable = PageRequest.of(0, 5);
// QueryDslPredicateExecutor 인터페이스에서 정의한 findALl() 메소드를 이용해 조건에 맞는 데이터를 Page 객체로 받아옵니다.
Page<Item> itemPagingResult = itemRepository.findAll(booleanBuilder, pageable);
System.out.println("total elements : " + itemPagingResult.getTotalElements());
List<Item> resultItemList = itemPagingResult.getContent();
for(Item resultItem : resultItemList){
System.out.println("찾은 상품 : " +resultItem.toString());
}
}
여기까지 JPA가 무엇이고 어떻게 동작하는지 간단한 예제들을 통해 알아보았습니다.
앞으로의 예제는 여러 테이블을 조인해서 데이터를 가지고 오는 방법도 함께 배워보겠습니다.