Spring

[인프런/스프링 DB 2편] 4. 데이터 접근 기술 (6) SpringData JPA + Query DSL

주니어주니 2023. 7. 18. 12:18

 

 

 

1. 트레이드 오프 

 

* 스프링 데이터 JPA를 사용할 때의 두가지 방법

 

1) 구조의 안정성 (DI, OCP를 지킴 -> 어댑터 도입)

 

 

* Service

Repository 인터페이스 구현체의 메소드 호출

 

* Repository 인터페이스 구현체

SpringDataJpa 의 메소드 호출

 

  • Service -> Repository 어댑터를 거쳐서 -> 스프링 데이터 JPA 의 메소드 사용
  • 장점 : Service에서 의존하는 Repository 인터페이스를 유지, Service의 코드도 유지하면서 Repository의 구현체를 변경할 수 있음 (DI, OCP원칙)
  • 단점 : 중간 어댑터 작성, 실제 코드까지 함께 유지보수해야 함

 

 

2) 구조의 단순성 + 개발의 편리성 (service에서 직접 호출)

 

  • Service에서 스프링 데이터 JPA의 메소드를 바로 호출
  • Service의 코드를 변경(DI, OCP 원칙 포기)해야 하지만, 구조가 오히려 단순해짐

 

 

 

2. 실용적인 구조 

 

단순한 구조 + 스프링 데이터 JPA 활용 + 동적쿼리에서 Querydsl 활용

 

 

1) ItemRepositoryV2 - 스프링 데이터 JPA 기능 제공 (단순 CRUD)

package hello.itemservice.repository.v2;

import hello.itemservice.domain.Item;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ItemRepositoryV2 extends JpaRepository<Item, Long> {
    // 기본적인 CRUD 제공
}

 

 

2) ItemQueryRepositoryV2 - Querydsl 기능 제공 (동적 쿼리)

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.domain.QItem;
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.*;

@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(LikeItemName(cond.getItemName()), maxPrice(cond.getMaxPrice()))
                .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;
    }
}

 

 

3) ItemServiceV2 

  • ItemRepositoryV2 (스프링 데이터 JPA 기능), ItemQueryRepositoryV2 (Querydsl 기능) 모두 의존
  • 간단한 CRUDItemRepository 기능 사용
    동적 쿼리ItemQueryRepository 기능 사용

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 = itemRepositoryV2.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);
    }
}

 

 

4) V2Config

package hello.itemservice.config;

import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.jpa.JpaItemRepositoryV2;
import hello.itemservice.repository.jpa.JpaItemRepositoryV3;
import hello.itemservice.repository.jpa.SpringDataJpaItemRepository;
import hello.itemservice.repository.v2.ItemQueryRepositoryV2;
import hello.itemservice.repository.v2.ItemRepositoryV2;
import hello.itemservice.service.ItemService;
import hello.itemservice.service.ItemServiceV1;
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);
    }
}
  • itemServiceV2, itemQueryRepositoryV2스프링 빈으로 등록
    (SpringDataJPA를 상속받은 ItemRepositoryV2는 스프링이 알아서 빈으로 등록해줌)

 

 

5) Application 

@Slf4j
@Import(V2Config.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication { }

 

 

 

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

 

📌 추천 활용 방안

기본 : JPA, 스프링 데이터 JPA, Querydsl
복잡한 쿼리 부분 : JdbcTemplate, MyBatis 함께 사용

 

 

1) 주의점

 

(1) 트랜잭션 매니저

 

* 트랜잭션 매니저가 다름

 

JPA 기술 -> JpaTransactionManager 

JDBC 기술 -> DataSourceTransactionManager 

 

 

-> JpaTransactionManager 만 스프링 빈에 등록하면 모두 하나의 트랜잭션으로 묶어서 사용할 수 있음 !

(DataSourceTransactionManager 기능을 포함하고 있음) 

 

 

 

(2) JPA의 플러시 타이밍 

 

* JPA는 데이터 변경 사항을 즉시 반영 X, 커밋되는 시점에 반영 

-> JPA에서 변경한 후, Jdbc를 호출하는 경우 Jdbc는 JPA가 변경한 데이터를 읽지 못함

 

-> JPA 호출이 끝난 시점에 JPA가 제공하는 플러시 기능 사용

-> JPA의 변경 내역을 데이터베이스에 반영