4 분 소요


주요 기능

스프링 데이터 JPA는 JPA를 편리하게 사용할 수 있도록 도와주는 라이브러리이다.

대표적인 기능

  • 공통 인터페이스 기능
  • 쿼리 메서드 기능

공통 인터페이스 기능

5

  • JpaRepository 인터페이스를 통해서 기본적인 CRUD 기능을 제공한다.
  • 공통화 가능한 기능이 거의 포함되어 있다.
  • CrudRepository에서 findOne() -> findById()로 변경되었다.
public interface ItemRepository extends JpaRepository<Member, Long> {
}
  • JpaRepository 인터페이스를 상속 받고, 제네릭에 관리할 <엔티티, 엔티티ID>를 주면 된다.
  • 그러면 JpaRepository가 제공하는 기본 CRUD 기능을 모두 사용할 수 있다.

스프링 데이터 JPA가 구현 클래스를 대신 생성
6

  • JpaRepository인터페이스만 상속받으면 스프링 데이터 JPA가 프록시 기술을 사용해서 구현 클래스를 만들어준다. 그리고 만든 구현 클래스의 인스턴스를 만들어서 스프링 빈으로 등록한다.
  • 따라서, 개발자는 구현 클래스 없이 인터페이스만 만들면 기본 CRUD 기능을 사용할 수 있다.

쿼리 메서드 기능

스프링 데이터 JPA는 인터페이스에 메서드만 적어두면, 메서드 이름을 분석해서 쿼리를 자동으로 만들고 실행해주는 기능을 제공한다.

순수 JPA

public List<Member> findByUsernameAndAgeGreaterThan(String username, int age) {
    return em.createQuery("select m from Member m where m.username = :username and m.age > :age")
            .setParameter("username", username)
            .setParameter("age", age)
            .getResultList();
}

순수 JPA를 사용하면 직접 JPQL을 작성하고, 파라미터도 직접 바인딩 해야 한다.

스프링 데이터 JPA

public interface MemberRepository extends JpaRepository<Member, Long> {
    List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
  • 스프링 데이터 JPA는 메서드 이름을 분석해서 필요한 JPQL을 만들고 실행해준다.
    • JPQL은 JPA가 SQL로 번역해서 실행한다.

스프링 데이터 JPA가 제공하는 쿼리 메소드 기능

  • 조회: find…By, read…By, query…By, get…By
  • 예:) findHelloBy처럼 …에 식별하기 위한 내용(설명)이 들어가도 된다.
  • COUNT: count…By 반환타입 long
  • EXISTS: exists…By 반환타입 boolean
  • 삭제: delete…By, remove…By 반환타입 long
  • DISTINCT: findDistinct, findMemberDistinctBy
  • LIMIT: findFirst3, findFirst, findTop, findTop3

쿼리 메소드 필터 조건
스프링 데이터 JPA 공식 문서 참고1
스프링 데이터 JPA 공식 문서 참고2

JPQL 직접 사용

public interface SpringDataJpaItemRepository extends JpaRepository<Item, Long> {

    // 쿼리 메서드 기능
    List<Item> findByItemNameLike(String itemName);

    // 쿼리 직접 실행
    @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);
}
  • 쿼리 메서드 기능 대신에 직접 JPQL을 사용하고 싶으면 @Query와 함께 JPQL을 작성하면 된다.
    • 이때는 메서드 이름으로 실행하는 규칙은 무시된다.


스프링 데이터 JPA 적용1

SpringDataJpaItemRepository

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);
}
  • 스프링 데이터 JPA가 제공하는 JpaRepository 인터페이스를 상속받으면 기본적인 CRUD 기능을 사용할 수 있다.
  • 하지만, 이름으로 검색하거나, 가격으로 검색하는 기능은 공통으로 제공할 수 있는 기능이 아니기 때문에 쿼리 메서드 기능을 사용하거나 @Query를 사용해서 직접 쿼리를 실행하면 된다.
  • 여기서는 데이터를 조건에 따라 4가지로 분류해서 검색한다.
    • 모든 데이터 조회
    • 이름 조회
    • 가격 조회
    • 이름 + 가격 조회

[참고]
스프링 데이터 JPA도 Example이라는 기능으로 약간의 동적 쿼리를 지원하지만, 실무에서 사용하기에는 기능이 빈약하다. 실무에서 JPQL 동적 쿼리는 Querydsl을 사용하는 것이 좋다.

findAll()

코드에는 보이지 않지만 JpaRepository 공통 인터페이스가 제공하는 기능이다.
select i from Item i

findByItemNameLike()

이름 조건만 검색했을 때 사용하는 쿼리.
select i from Item i where i.name like ?

findByPriceLessThanEqual()

가격 조건만 검색했을 때 사용하는 쿼리 메서드
select i from Item i where i.price <= ?

findByItemNameLikeAndPriceLessThanEqual()

가격 조건과 이름 조건을 둘 다 검색했을 때 사용하는 쿼리 메서드
select i from Item i where i.itemName like ? and i.price <= ?

findItems()

메서드 이름으로 쿼리를 실행하는 기능은 다음과 같은 단점이 있다.

  1. 조건이 많으면 메서드 이름이 너무 길어진다.
  2. 조인같은 복잡한 조건을 사용할 수 없다.
    메서드 이름으로 쿼리를 실행하는 기능은 간단한 경우에는 매우 유용하지만, 복잡해지면 직접 JPQL 쿼리를 작성하는 것이 좋다.
  • 쿼리를 직접 실행하려면 @Query 애노테이션을 사용하면 된다.
  • 메서드 이름으로 쿼리를 실행할 때는 파라미터를 순서대로 입력하면 되지만, 쿼리를 직접 실행할 때는 파리미터를 명시적으로 바인딩 해야 한다.
  • 파라미터 바인딩은 @Param("itemName") 애노테이션을 사용하고, 애노테이션의 값에 파라미터 이름을 주면 된다.


스프링 데이터 JPA 적용2

JpaItemRepositoryV2

@Repository
@Transactional
@RequiredArgsConstructor
public class JpaItemRepositoryV2 implements ItemRepository {

    private final SpringDataJpaItemRepository repository;

    @Override
    public Item save(Item item) {
        return repository.save(item);
    }

    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        Item findItem = repository.findById(itemId).orElseThrow();
        findItem.setItemName(updateParam.getItemName());
        findItem.setPrice(updateParam.getPrice());
        findItem.setQuantity(updateParam.getQuantity());
    }

    @Override
    public Optional<Item> findById(Long id) {
        return repository.findById(id);
    }

    @Override
    public List<Item> findAll(ItemSearchCond cond) {

        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        if (StringUtils.hasText(itemName) && maxPrice != null) {
            //return repository.findByItemNameLikeAndPriceLessThanEqual("%" + itemName + "%", maxPrice);
            return repository.findItems("%" + itemName + "%", maxPrice);
        } else if (StringUtils.hasText(itemName)) {
            return repository.findByItemNameLike("%" + itemName + "%");
        } else if (maxPrice != null) {
            return repository.findByPriceLessThanEqual(maxPrice);
        } else {
            return repository.findAll();
        }
    }
}

의존관계와 구조

  • ItemServiceItemRepository에 의존하기 때문에 ItemService에서 SpringDataJpaItemRepository를 그대로 사용할 수 없다.
  • 물론 ItemServiceSpringDataJpaRepository를 직접 사용하도록 코드를 고치면 되겠지만, ItemService 코드의 변경없이 ItemRepository에 대한 의존을 유지하면서 DI를 통해 구현 기술을 변경하고 싶다.

여기서는 JpaItemRepositoryV2ItemRepositorySpringDataJpaRepository 사이를 맞추기 위한 어댑터 처럼 사용된다.

클래스 의존 관계
7

  • JpaItemRepositoryV2ItemRepository를 구현하고, SpringDataJpaItemRepository를 사용한다.

런타임 객체 의존 관계
8

  • 런타임의 객체 의존관계는 다음과 같이 동작한다.
  • itemService -> jpaItemRepositoryV2 -> springDataJpaItemRepository(프록시 객체)

save()

repository.save(item)
스프링 데이터 JPA가 제공하는 save()를 호출한다.

update()

스프링 데이터 JPA가 제공하는 findById()메서드를 사용해서 엔티티를 찾고, 데이터를 수정한다.
이후 트랜잭션이 커밋될 때 변경 내용이 데이터베이스에 반영된다.(JPA가 제공하는 기능)

findById()

repository.findById(itemId)
스프링 데이터 JPA가 제공하는 findById() 메서드를 사용해서 엔티티를 찾는다.

findAll()

데이터를 조건에 따라 4가지로 분류해서 검색한다.

  • 모든 데이터 조회
  • 이름 조회
  • 가격 조회
  • 이름 + 가격 조회

추가로, 코드를 보면 동적 쿼리가 아니라 상황에 따라 각각 스프링 데이터 JPA의 메서드를 호출해서 상당히 비효율적인 코드인 것을 알 수 있다. 스프링 데이터 JPA는 동적 쿼리 기능에 대한 지원이 매우 약하기 때문에 Querydsl을 사용해서 개선하는 것이 좋다.


<출처 : 인프런 - 스프링 DB 2편 : 데이터 접근 활용 기술(김영한)>

댓글남기기