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 기능) 모두 의존
- 간단한 CRUD는 ItemRepository 기능 사용
동적 쿼리는 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의 변경 내역을 데이터베이스에 반영