JPA에서 가장 중요한 부분은 객체와 테이블을 매핑하는 것이다. JPA가 제공하는 애노테이션을 사용해서 Item 객체와 테이블을 매핑해보자.
Item
package hello.itemservice.domain;
import lombok.Data;
import javax.persistence.*;
@Data
@Entity
public class Item {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "item_name", length = 10)
private String itemName;
private Integer price;
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
@Entity
: JPA가 사용하는 객체라는 뜻. 이 애노테이션이 있어야 JPA가 인식할 수 있다.
@Id
: 테이블의 PK와 해당 필드를 매핑한다.
@GeneratedValue(strategy = GenerationType.IDENTITY)
: PK 생성 값을 데이터베이스에서 생성하는 IDENTITY 방식을 사용한다.
@Column
: 객체의 필드를 테이블의 컬럼과 매핑한다. 객체는 itemName이지만 테이블의 컬럼은 item_name이기 때문에 "name"으로 매핑한다.
JPA는 public 또는 protected의 기본 생성자가 필수이니 꼭 기본 생성자를 넣어준다.
JpaItemRepositoryV1
package hello.itemservice.repository.jpa;
import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.List;
import java.util.Optional;
@Slf4j
@Repository
@Transactional
public class JpaItemRepositoryV1 implements ItemRepository {
private final EntityManager em;
public JpaItemRepositoryV1(EntityManager em) {
this.em = em;
}
@Override
public Item save(Item item) {
em.persist(item);
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
Item findItem = em.find(Item.class, itemId);
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);
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
String jpql = "select i from Item i";
Integer maxPrice = cond.getMaxPrice();
String itemName = cond.getItemName();
if (StringUtils.hasText(itemName) || maxPrice != null) {
jpql += " where";
}
boolean andFlag = false;
if (StringUtils.hasText(itemName)) {
jpql += " i.itemName like concat('%',:itemName,'%')";
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
jpql += " and";
}
jpql += " i.price <= :maxPrice";
}
log.info("jpql={}", jpql);
TypedQuery<Item> query = em.createQuery(jpql, Item.class);
if (StringUtils.hasText(itemName)) {
query.setParameter("itemName", itemName);
}
if (maxPrice != null) {
query.setParameter("maxPrice", maxPrice);
}
return query.getResultList();
}
}
EntityManager em
: 스프링을 통해 엔티티 매니저를 주입받는다. JPA의 모든 동작은 엔티티 매니저를 통해서 이루어진다. 엔티티 매니저는 내부에 데이터소스를 가지고 있고, 데이터베이스에 접근할 수 있다.
@Transactional
: JPA의 모든 데이터 변경은 트랜잭션 안에서 이루어져야 한다.(조회는 트랜잭션 없어도 가능) 일반적으로는 비즈니스 로직을 시작하는 서비스 계층에 트랜잭션을 걸어주는 것이 맞다.
update()
: JPA는 트랜잭션이 커밋되는 시점에 변경된 엔티티 객체가 있는지 확인한다. 변경된 경우 UPDATE SQL을 실행한다.
findAll()
: JPQL이라는 객체지향 쿼리 언어를 제공한다. 주로 여러 데이터를 복잡한 조건으로 조회할 때 사용한다.
SQL이 테이블을 대상으로 한다면, JPQL은 엔티티 객체를 대상으로 SQL을 실행한다.
엔티티 객체를 대상으로 하기 때문에 from 다음에 Item 엔티티 객체 이름이 들어간다. 엔티티 객체와 속성의 대소문자는 구분해야 한다.
JpaConfig
package hello.itemservice.config;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.jpa.JpaItemRepositoryV1;
import hello.itemservice.service.ItemService;
import hello.itemservice.service.ItemServiceV1;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
@Configuration
public class JpaConfig {
private final EntityManager em;
public JpaConfig(EntityManager em) {
this.em = em;
}
@Bean
public ItemService itemService() {
return new ItemServiceV1(itemRepository());
}
@Bean
public ItemRepository itemRepository() {
return new JpaItemRepositoryV1(em);
}
}
ItemServiceApplication까지 변경하고 실행해보면 정상 작동한다.
스프링 데이터 JPA
스프링 데이터 JPA는 JPA를 편리하게 사용할 수 있도록 도와주는 라이브러리이다.
* 공통 인터페이스 기능
* 쿼리 메서드 기능
공통 인터페이스 기능
public interface ItemRepository extends JpaRepository<Item, Long> {
}
JpaRepository 인터페이스를 상속 받고, 제네릭에 관리할 <엔티티, 엔티티ID> 를 주면 된다.
JpaRepository 인터페이스만 상속받으면 스프링 데이터 JPA가 프록시 기술을 사용해서 구현 클래스를 만들어준다. 그리고 만든 구현 클래스의 인스턴스를 만들어서 스프링 빈으로 등록한다. 따라서 개발자는 구현 클래스 없이 인터페이스만 만들면 기본 CRUD 기능을 사용할 수 있다.
쿼리 메서드 기능
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
스프링 데이터 JPA는 메서드 이름을 분석해서 필요한 JPQL을 만들고 실행해준다. JPQL은 JPA가 SQL로 번역해서 실행한다.
스프링 데이터 JPA 적용
SpringDataJpaItemRepository
package hello.itemservice.repository.jpa;
import hello.itemservice.domain.Item;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface SpringDataJpaItemRepository extends JpaRepository<Item, Long> {
List<Item> findByItemNameLike(String itemName);
List<Item> findByPriceLessThanEqual(Integer price);
//쿼리 메서드
List<Item> findByItemNameLikeAndPriceLessThanEqual(String itemName, Integer price);
//쿼리 직접 실행
@Query("select i from Item i where i.itemName like :itemName and i.price <= :price")
List<Item> findItems(@Param("itemName") String itemName, @Param("price") Integer price);
}
이름으로 검색하거나, 가격으로 검색하는 기능은 공통으로 제공할 수 있는 기능이 아니다. 따라서 쿼리 메서드 기능을 사용하거나 @Query 를 사용해서 직접 쿼리를 실행한다.
메서드 이름으로 쿼리를 실행하는 기능은 다음과 같은 단점이 있다.
1. 조건이 많으면 메서드 이름이 너무 길어진다.
2. 조인같은 복잡한 조건을 사용할 수 없다.
메서드 이름으로 쿼리를 실행하는 기능은 간단한 경우에는 매우 유용하지만, 복잡해지면 직접 @Query 애노테이션을 이용해 JPQL 쿼리를 작성하는 것이 좋다.
JpaItemRepositoryV2
package hello.itemservice.repository.jpa;
import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Optional;
@Repository
@Transactional
@RequiredArgsConstructor
public class JpaItemRepositoryV2 implements ItemRepository {
private final SpringDataJpaItemRepository repository;
@Override
public Item save(Item item) {
return repository.save(item);
}
//...
}
ItemService 는 ItemRepository 에 의존하기 때문에 ItemService 에서 SpringDataJpaItemRepository 를 그대로 사용할 수 없다.
물론 ItemService 가 SpringDataJpaItemRepository 를 직접 사용하도록 코드를 고치면 되겠지만, ItemService 코드의 변경없이 ItemService 가 ItemRepository 에 대한 의존을 유지하면서 DI를 통해 구현 기술을 변경하고 싶다.
이때 새로운 리포지토리를 만들어서 이 문제를 해결할 수 있다. JpaItemRepositoryV2 가 ItemRepository와 SpringDataJpaItemRepository 사이를 맞추기 위한 어댑터처럼 사용된다.

SpringDataJpaConfig
package hello.itemservice.config;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.jpa.JpaItemRepositoryV2;
import hello.itemservice.repository.jpa.SpringDataJpaItemRepository;
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;
@Configuration
@RequiredArgsConstructor
public class SpringDataJpaConfig {
private final SpringDataJpaItemRepository springDataJpaItemRepository;
@Bean
public ItemService itemService() {
return new ItemServiceV1(itemRepository());
}
@Bean
public ItemRepository itemRepository() {
return new JpaItemRepositoryV2(springDataJpaItemRepository);
}
}
ItemServiceApplication도 변경하고 정상 작동을 확인한다.
(정상 작동)
정리
JpaRepository<엔티티, 엔티티ID>를 구현해 스프링 데이터 JPA를 사용하면 쿼리 사용을 최소화해 개발할 수 있다.
Querydsl 적용
JpaItemRepositoryV3
package hello.itemservice.repository.jpa;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import hello.itemservice.domain.Item;
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 cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
BooleanBuilder builder = new BooleanBuilder();
if (StringUtils.hasText(itemName)) {
builder.and(item.itemName.like("%" + itemName + "%"));
}
if (maxPrice != null) {
builder.and(item.price.loe(maxPrice));
}
return query
.select(item)
.from(item)
.where(builder)
.fetch();
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
return query
.select(item)
.from(item)
.where(likeItemName(itemName), maxPrice(maxPrice))
.fetch();
}
private BooleanExpression maxPrice(Integer maxPrice) {
if (maxPrice != null) {
return item.price.loe(maxPrice);
}
return null;
}
private BooleanExpression likeItemName(String itemName) {
if (StringUtils.hasText(itemName)) {
return item.itemName.like("%" + itemName + "%");
}
return null;
}
}
JPAQueryFactory
: Querydsl 사용을 위해 JPAQueryFactory가 필요하다. JPAQueryFactory는 JPA 쿼리인 JPQL을 만들기 때문에 EntityManager가 필요하다.
findAll()
앞서 findAllOld 에서 작성한 코드를 깔끔하게 리팩토링했다.
Querydsl에서 where(A,B) 에 다양한 조건들을 직접 넣을 수 있는데, 이렇게 넣으면 AND 조건으로 처리된다. 참고로 where() 에 null을 입력하면 해당 조건은 무시한다.
이 코드의 또 다른 장점은 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을 변경 후 정상작동을 확인해본다.
(정상 작동)
JPA, 스프링 데이터 JPA, Querydsl은 추후 더 깊게 학습해보겠다.
현재 프로젝트는 중간에서 JpaItemRepositoryV2 가 어댑터 역할을 해준 덕분에 ItemService가 사용하는 ItemRepository 인터페이스를 그대로 유지할 수 있고 클라이언트인 ItemService의 코드를 변경하지 않아도 되는 장점이 있다.
하지만, 구조를 맞추기 위해서 중간에 어댑터가 들어가면서 전체 구조가 너무 복잡해지고 사용하는 클래스도 많아지는 단점이 생겼다.
대안으로, ItemService 코드를 일부 고쳐서 직접 스프링 데이터 JPA를 사용할 수 있다. 그러면 DI, OCP 원칙을 포기하는 대신에, 복잡한 어댑터를 제거하고, 구조를 단순하게 가져갈 수 있는 장점이 있다.

결국 여기서 발생하는 트레이드 오프는 구조의 안정성 vs 단순한 구조와 개발의 편리성 사이의 선택이다.
정답은 없다고 한다. 어떤 상황에서는 구조의 안정성이 매우 중요하고, 어떤 상황에서는 단순한 것이 더 나은 선택일 수 있다.
프로젝트의 현재 상황에 맞는 더 적절한 선택을 해야 한다. 추상화 비용을 넘어설 만큼 효과가 있을 때 추상화를 도입하는 것이 실용적이다.
실용적인 구조
마지막에 Querydsl을 사용한 리포지토리는 스프링 데이터 JPA를 사용하지 않는 아쉬움이 있었다. 물론 Querydsl을 사용하는 리포지토리가 스프링 데이터 JPA 리포지토리를 사용하도록 해도 된다.
스프링 데이터 JPA의 기능은 최대한 살리면서, Querydsl도 편리하게 사용할 수 있는 구조로 만들어보겠다.
ItemRepositoryV2 는 스프링 데이터 JPA의 기능을 사용해 기본 CRUD와 단순 조회를 담당하게 한다.
ItemQueryRepositoryV2는 Querydsl을 사용해 복잡한 쿼리 기능을 담당한다.
물론 ItemService는 기존 ItemRepository를 사용할 수 없기 때문에 코드를 변경해야한다.
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> {
}
기본 CRUD를 담당한다.
ItemQueryRepositoryV2
package hello.itemservice.repository.v2;
import com.querydsl.core.types.Predicate;
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(
maxPrice(cond.getMaxPrice()),
likeItemName(cond.getItemName())
)
.fetch();
}
private BooleanExpression maxPrice(Integer maxPrice) {
if (maxPrice != null) {
return item.price.loe(maxPrice);
}
return null;
}
private BooleanExpression likeItemName(String itemName) {
if (StringUtils.hasText(itemName)) {
return item.itemName.like("%" + itemName + "%");
}
return null;
}
}
복잡한 쿼리를 담당한다.
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 = 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);
}
}
ItemServiceV2는 ItemRepositoryV2와 ItemQueryRepositoryV2를 의존한다.
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;
@Bean
public ItemService itemService() {
return new ItemServiceV2(itemRepositoryV2, itemQueryRepositoryV2());
}
@Bean
public ItemQueryRepositoryV2 itemQueryRepositoryV2() {
return new ItemQueryRepositoryV2(em);
}
@Bean
public ItemRepository itemRepository() {
return new JpaItemRepositoryV3(em);
}
}
ItemServiceApplication을 변경해주고 정상 작동 여부를 확인해본다.
(정상 작동)
다양한 데이터 접근 기술 조합
JPA, 스프링 데이터 JPA, Querydsl을 기본으로 사용하고, 만약 복잡한 쿼리를 써야 하는데 해결이 잘 안되면 해당 부분에는 JdbcTemplate이나 MyBatis를 함께 사용한다.
JPA 기술을 사용하는 데이터 접근 기술들은 트랜잭션 매니저로 JpaTransactionManager을 사용한다. 하지만 내부에서 JDBC를 직접 사용하는 기술들은 DataSourceTransactionManager를 사용한다.
다행히도 JpaTransactionManager는 DataSourceTransactionManager의 기능도 대부분 제공하기 때문에, JpaTransactionManager만 스프링 빈으로 등록해도 다른 기술을 모두 하나의 트랜잭션으로 묶어서 사용할 수 있다.
정리
ItemServiceV2는 스프링 데이터 JPA를 제공하는 ItemRepositoryV2도 참조하고, Querydsl과 관련된 ItemQueryRepositoryV2도 직접 참조한다. 덕분에 ItemRepositoryV2를 통해서 스프링 데이터 JPA 기능을 적절히 활용할 수 있고, ItemQueryRepositoryV2 를 통해서 복잡한 쿼리를 Querydsl로 해결할 수 있다. 이렇게 하면서 구조의 복잡함 업싱 단순하게 개발하게 되었다.
단순한 CRUD는 스프링 데이터 JPA / 복잡한 쿼리는 Querydsl을 사용하자.
'공부 > DB' 카테고리의 다른 글
| [Spring DB](8) 스프링 트랜잭션 전파 (1) | 2024.02.29 |
|---|---|
| [Spring DB](7) 스프링 트랜잭션 (2) | 2024.02.29 |
| [Spring DB](5) 데이터 접근 기술 - MyBatis (0) | 2024.02.27 |
| [Spring DB](4) 데이터 접근 기술 - JDBC (0) | 2024.02.27 |
| [Spring DB](3) 자바 예외 (1) | 2024.02.26 |