JPA에서 QUERY 방법은 크게 3가지
JPQL(HQL)
장점: SQL QUERY와 비슷해서 금방 익숙해짐
단점: type-safe 아님, 동적쿼리 생성이 어려움
Criteria API
장점: 동적쿼리 생성이 약간 쉬움
단점: type-safe가 아님, 복잡함, 어려움
MetaModel Criteria API(type-safe)
DSL?
QueryDSL?
Querydsl-JPA
작동 방식
장점
단점
기능
SpringDataJPA + Querydsl
build.gradle
//Querydsl 추가
implementation 'com.querydsl:querydsl-jpa'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
2가지 방법을 설정 할 수 있는데(gradle, IntelliJ IDEA) 저는 IntelliJIDEA로 설정해서 사용하였습니다.
Gradle IntelliJ 사용법
Gradle -> Tasks -> build -> clean
Gradle -> Tasks -> other -> compileJava
Q 타입 생성 확인
build -> generated -> sources -> annotationProcessor -> java/main
하위에 hello.itemservice.domain.QItem
이 생성되어야 한다.IntelliJ IDEA - Q타입 생성 확인 방법
Build -> Build Project
또는 Build -> Rebuild
src/main/generated
하위에 hello.itemservice.domain.QItem
이 생성되어 있어야 한다.
참고: Q타입은 컴파일 시점에 자동 생성되므로 버전관리(GIT)에 포함하지 않는 것이 좋다.
package hello.itemservice.repository.jpa;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import hello.itemservice.domain.Item;
import hello.itemservice.domain.QItem;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
import static hello.itemservice.domain.QItem.*;
@Repository
@Transactional
public class JpaItemRepositoryV3 implements ItemRepository {
private final EntityManager em;
private final JPAQueryFactory query;
public JpaItemRepositoryV3(EntityManager em) {
this.em = em;
this.query = new JPAQueryFactory(em);
}
@Override
public Item save(Item item) {
em.persist(item);
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
Item findItem = findById(itemId).orElseThrow();
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
@Override
public Optional<Item> findById(Long id) {
Item item = em.find(Item.class, id);
return Optional.ofNullable(item);
}
public List<Item> findAllOld(ItemSearchCond itemSearch) {
String itemName = itemSearch.getItemName();
Integer maxPrice = itemSearch.getMaxPrice();
QItem item = QItem.item;
BooleanBuilder builder = new BooleanBuilder();
if (StringUtils.hasText(itemName)) {
builder.and(item.itemName.like("%" + itemName + "%"));
}
if (maxPrice != null) {
builder.and(item.price.loe(maxPrice));
}
List<Item> result = query
.select(item)
.from(item)
.where(builder)
.fetch();
return result;
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
List<Item> result = query
.select(item)
.from(item)
.where(likeItemName(itemName), maxPrice(maxPrice))
.fetch();
return result;
}
private BooleanExpression likeItemName(String itemName) {
if (StringUtils.hasText(itemName)) {
return item.itemName.like("%" + itemName + "%");
}
return null;
}
private BooleanExpression maxPrice(Integer maxPrice) {
if (maxPrice != null) {
return item.price.loe(maxPrice);
}
return null;
}
}
JPAQueryFactory
가 필요하다. JPAQueryFactory
는 JPA 쿼리인 JPQL을 만들기 때문에 EntityManager
가 필요하다.findAllOld
BooleanBuilder
를 사용해서 원하는where
조건들을 넣어주면 된다.findAll
where(A,B)
에 다양한 조건들을 직접 넣을 수 있는데, 이렇게 넣으면 AND 조건으로 처리된다.likeItemName()
, maxPrice()
를 다른 쿼리를 작성할 때 재사용 할 수 있다는 점이다.QuerydslConfig
package hello.itemservice.config;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.jpa.JpaItemRepositoryV3;
import hello.itemservice.service.ItemService;
import hello.itemservice.service.ItemServiceV1;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
@Configuration
@RequiredArgsConstructor
public class QuerydslConfig {
private final EntityManager em;
@Bean
public ItemService itemService() {
return new ItemServiceV1(itemRepository());
}
@Bean
public ItemRepository itemRepository() {
return new JpaItemRepositoryV3(em);
}
}
ItemServiceApplication 적용 후 테스트
애플리케이션을 실행
현재 프로젝트 의존 관계에 대해서 살펴보자
클래스 의존 관계
런타임 객체 의존 관계
JpaItemRepositoryV2
가 어댑터 역할을 해준 덕분에 ItemService
가 사용하는ItemRepository
인터페이스를 그대로 유지할 수 있고 클라이언트인 ItemService
의 코드를 변경하지 않아도 되는 장점이 있다.다른 방식도 있다.
클래스 의존 관계
런타임 객체 의존 관계
트레이드 오프
ItemService
코드를 직접 변경한다.스프링 데이터 JPA의 기능은 최대한 살리면서, Querydsl도 편리하게 사용할 수 있는 구조를 만들어보겠습니다.
복잡한 쿼리 분리하기
ItemRepositoryV2
는 스프링 데이터 JPA의 기능을 제공하는 리포지토리이다.ItemQueryRepositoryV2
는 Querydsl을 사용해서 복잡한 쿼리 기능을 제공하는 리포지토리이다.ItemRepositoryV2
package hello.itemservice.repository.v2;
import hello.itemservice.domain.Item;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ItemRepositoryV2 extends JpaRepository<Item, Long> {
}
ItemRepositoryV2
는 JpaRepository
를 인터페이스 상속 받아서 스프링 데이터 JPA의 기능을 제공하는 리포지토리가 된다.ItemQueryRepositoryV2
package hello.itemservice.repository.v2;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemSearchCond;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import javax.persistence.EntityManager;
import java.util.List;
import static hello.itemservice.domain.QItem.item;
@Repository
public class ItemQueryRepositoryV2 {
private final JPAQueryFactory query;
public ItemQueryRepositoryV2(EntityManager em) {
this.query = new JPAQueryFactory(em);
}
public List<Item> findAll(ItemSearchCond cond) {
return query.select(item)
.from(item)
.where(
maxPrice(cond.getMaxPrice()),
likeItemName(cond.getItemName()))
.fetch();
}
private BooleanExpression likeItemName(String itemName) {
if (StringUtils.hasText(itemName)) {
return item.itemName.like("%" + itemName + "%");
}
return null;
}
private BooleanExpression maxPrice(Integer maxPrice) {
if (maxPrice != null) {
return item.price.loe(maxPrice);
}
return null;
}
}
ItemQueryRepositoryV2
는 Querydsl을 사용해서 복잡한 쿼리 문제를 해결한다.ItemServiceV2
package hello.itemservice.service;
import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import hello.itemservice.repository.v2.ItemQueryRepositoryV2;
import hello.itemservice.repository.v2.ItemRepositoryV2;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Transactional
public class ItemServiceV2 implements ItemService {
private final ItemRepositoryV2 itemRepositoryV2;
private final ItemQueryRepositoryV2 itemQueryRepositoryV2;
@Override
public Item save(Item item) {
return itemRepositoryV2.save(item);
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
Item findItem = findById(itemId).orElseThrow();
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
@Override
public Optional<Item> findById(Long id) {
return itemRepositoryV2.findById(id);
}
@Override
public List<Item> findItems(ItemSearchCond cond) {
return itemQueryRepositoryV2.findAll(cond);
}
}
V2Config
package hello.itemservice.config;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.jpa.JpaItemRepositoryV3;
import hello.itemservice.repository.v2.ItemQueryRepositoryV2;
import hello.itemservice.repository.v2.ItemRepositoryV2;
import hello.itemservice.service.ItemService;
import hello.itemservice.service.ItemServiceV2;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
@Configuration
@RequiredArgsConstructor
public class V2Config {
private final EntityManager em;
private final ItemRepositoryV2 itemRepositoryV2; //SpringDataJPA
@Bean
public ItemService itemService() {
return new ItemServiceV2(itemRepositoryV2, itemQueryRepository());
}
@Bean
public ItemQueryRepositoryV2 itemQueryRepository() {
return new ItemQueryRepositoryV2(em);
}
@Bean
public ItemRepository itemRepository() {
return new JpaItemRepositoryV3(em);
}
}
ItemServiceApplication 변경 후 테스트 실행
애플리케이션 실행 테스트
어떤 데이터 접근 기술을 선택하는 것이 좋을까?
--> 정답은 없다. 각각 다 장단점이 있다.
JdbcTemplate
이나 MyBatis
같은 기술들은 SQL을 직접 작성해야 하는 단점은 있지만 기술이 단순하기 때문에 SQL에 익숙한 개발자라면 금방 적응할 수 있다.트랜잭션 매니저 선택
JpaTransactionManager
를 선택하면 된다.JdbcTemplate
, MyBatis
와 같은 기술들은 내부에서 JDBC를 직접 사용하기 때문에 DataSourceTransactionManager
를 사용한다.JpaTransactionManager의 다양한 지원
JpaTransactionManager
는 놀랍게도 DataSourceTransactionManager
가 제공하는 기능도 대부분 제공한다. JPA라는 기술도 결국 내부에서는 DataSource
와 JDBC 커넥션을 사용하기 때문이다. 따라서 JdbcTemplate
, MyBatis
와 함께 사용할 수 있다.개발 시 트레이드 오프를 알고, 선택하고 적절한 선택을 할 수 있도록 고민하고 정하도록 하자.
참고
김영한: 스프링 DB 2편 - 데이터 접근 활용 기술(인프런)
Github - https://github.com/b2b2004/Spring_DB