Spring DB - Querydsl, 데이터 접근 기술 활용 방안

Kwon Yongho·2023년 5월 24일
0

Spring-DB

목록 보기
13/16
post-thumbnail

Querydsl, 데이터 접근 기술 활용 방안

  1. Querydsl 소개1 - 기존 방식의 문제점 및 해결
  2. Querydsl 설정
  3. Querydsl 적용
  4. 스프링 데이터 JPA 예제와 트레이드 오프
  5. 실용적인 구조
  6. 다양한 데이터 접근 기술 조합

1. Querydsl 소개1 - 기존 방식의 문제점 및 해결

JPA에서 QUERY 방법은 크게 3가지

  1. JPQL(HQL)
    장점: SQL QUERY와 비슷해서 금방 익숙해짐
    단점: type-safe 아님, 동적쿼리 생성이 어려움

  2. Criteria API
    장점: 동적쿼리 생성이 약간 쉬움
    단점: type-safe가 아님, 복잡함, 어려움

  3. MetaModel Criteria API(type-safe)

  • Criteria API + MetaModel
  • Criteria API와 거의 동일
  • type-safe
  • 복잡함

DSL?

  • Domain(도메인)
  • Specific(특화)
  • Language(언어)
  • 도메인 + 특화 + 언어
  • 특정한 도메인에 초점을 맞춘 제한적인 표현력을 가진 컴퓨터 프로그래밍 언어
  • 특징: 단순, 간결, 유창

QueryDSL?

  • JPA, MongoDB, SQL 같은 기술들을 위해 type-safe SQL을 만드는 프레임워크

Querydsl-JPA

  • Querydsl은 JPA 쿼리(JPQL)을 typesafe하게 작성하는데 많이 사용됨

작동 방식

장점

  • type-safe
  • 단순함
  • 쉬움

단점

  • Q코드 생성을 위한 APT를 설정해야함

기능

  • from
  • innerJoin, join, leftJoin, fullJoin, on
  • where(and, or, allOf, anyOf)
  • groupBy
  • having
  • orderBy (desc, asc)
  • limit, offset, restrict(limit + offset) Paging
  • list
  • iterate
  • count
  • fetch()
  • fetchOne()

SpringDataJPA + Querydsl

  • Spring Data 프로젝트의 약점은 조회
  • Querydsl로 복잡한 조회 기능 보완
    • 복잡한 쿼리
    • 동적 쿼리
  • 단순한 경우: SpringDataJPA
  • 복잡한 경우: Querydsl 직접 사용

3. 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)에 포함하지 않는 것이 좋다.

4. Querydsl 적용

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;
    }
}
  • Querydsl을 사용하려면 JPAQueryFactory가 필요하다. JPAQueryFactory는 JPA 쿼리인 JPQL을 만들기 때문에 EntityManager가 필요하다.
  • 설정 방식은 JdbcTemplate 을 설정하는 것과 유사!

findAllOld

  • BooleanBuilder를 사용해서 원하는where조건들을 넣어주면 된다.
  • 이 모든 것을 자바 코드로 작성하기 때문에 동적 쿼리를 매우 편리하게 작성할 수 있다.

findAll

  • Querydsl에서 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 적용 후 테스트

애플리케이션을 실행

5. 스프링 데이터 JPA 예제와 트레이드 오프

현재 프로젝트 의존 관계에 대해서 살펴보자

클래스 의존 관계

런타임 객체 의존 관계

  • JpaItemRepositoryV2가 어댑터 역할을 해준 덕분에 ItemService가 사용하는
    ItemRepository인터페이스를 그대로 유지할 수 있고 클라이언트인 ItemService의 코드를 변경하지 않아도 되는 장점이 있다.
  • 단점은 복잡하다.
  • 실제 이 코드를 구현해야하는 개발자 입장에서 보면 중간에 어댑터도 만들고, 실제 코드까지 만들어야 하는 불편함이 생긴다.

다른 방식도 있다.

  • ItemService 코드를 일부 고쳐서 직접 스프링 데이터 JPA를 사용하는 방법이다.

클래스 의존 관계

런타임 객체 의존 관계

트레이드 오프

  • 이것을 트레이드 오프라고 한다.
  • DI, OCP 원칙을 포기하는 대신에, 복잡한 어댑터를 제거하고, 구조를 단순하게 가져갈 수 있는 장점이 있다.
  • 어댑터를 제거하고 구조를 단순하게 가져가지만, DI, OCP를 포기하고, ItemService코드를 직접 변경한다.

6. 실용적인 구조

스프링 데이터 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> {
}
  • ItemRepositoryV2JpaRepository를 인터페이스 상속 받아서 스프링 데이터 JPA의 기능을 제공하는 리포지토리가 된다.
  • 기본 CRUD는 이 기능을 사용하면 된다.

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을 사용해서 복잡한 쿼리 문제를 해결한다.
  • 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 변경 후 테스트 실행

애플리케이션 실행 테스트

7. 다양한 데이터 접근 기술 조합

어떤 데이터 접근 기술을 선택하는 것이 좋을까?
--> 정답은 없다. 각각 다 장단점이 있다.

  • JdbcTemplate이나 MyBatis같은 기술들은 SQL을 직접 작성해야 하는 단점은 있지만 기술이 단순하기 때문에 SQL에 익숙한 개발자라면 금방 적응할 수 있다.
  • JPA, 스프링 데이터 JPA, Querydsl 같은 기술들은 개발 생산성을 혁신할 수 있지만, 학습 곡선이 높기 때문에, 이런 부분을 감안해야 한다. 그리고 매우 복잡한 통계 쿼리를 주로 작성하는 경우에는 잘 맞지 않는다.
  • JPA, 스프링 데이터 JPA, Querydsl을 기본으로 사용하고, 만약 복잡한 쿼리를 써야 하는데, 해결이 잘 안되면 해당 부분에는 JdbcTemplate이나 MyBatis를 함께 사용하는 것이다.

트랜잭션 매니저 선택

  • JPA, 스프링 데이터 JPA, Querydsl은 모두 JPA 기술을 사용하는 것이기 때문에 트랜잭션 매니저로 JpaTransactionManager를 선택하면 된다.
  • JdbcTemplate, MyBatis와 같은 기술들은 내부에서 JDBC를 직접 사용하기 때문에 DataSourceTransactionManager를 사용한다.

JpaTransactionManager의 다양한 지원

  • JpaTransactionManager는 놀랍게도 DataSourceTransactionManager가 제공하는 기능도 대부분 제공한다. JPA라는 기술도 결국 내부에서는 DataSource와 JDBC 커넥션을 사용하기 때문이다. 따라서 JdbcTemplate, MyBatis와 함께 사용할 수 있다.

개발 시 트레이드 오프를 알고, 선택하고 적절한 선택을 할 수 있도록 고민하고 정하도록 하자.

참고
김영한: 스프링 DB 2편 - 데이터 접근 활용 기술(인프런)
Github - https://github.com/b2b2004/Spring_DB

0개의 댓글