스프링 DB : 스프링 데이터 JPA 관련

    Spring Data와 Spring Data JPA

    예전에는 RDBMS에 모든 데이터를 저장한 후에 처리하는 형식이었다. 그렇지만 최근에는 MongDB, Redis 등 다양한 형태의 DB가 등장했다. 이 모든 DB들은 궁극적으로는 소위 말하는 데이터의 CRUD를 하는데, 이 말은 비슷한 형태로 동작한다는 것을 의미한다. 스프링 진영에서는 다양한 DB에 공통적인 인터페이스를 제공하고자 Spring Data 인터페이스를 제공한다.

    스프링 데이터 JPA는 인터페이스의 구현체 역할을 한다. 그렇지만 스프링 데이터 JPA는 단순한 통합은 아니다. 아래 같은 기능을 제공해준다. 

    • CRUD + 쿼리
    • 동일한 인터페이스
    • 페이징 처리
    • 메서드 이름으로 쿼리 생성
    • 스프링 MVC에서 ID 값만 넘겨도 도메인 클래스로 바인딩 됨. 

     


    Spring Data JPA 주요 기능 

    Spring Data Common 기술이 있는데, 이곳에 공통적인 인터페이스가 있다. Spring Data Jpa는 Spring Data Common 인터페이스에서 JPA와 관련되어 좀 더 쓸만한 기술이 추가된 것으로 이해할 수 있다. 이 말은 JPA를 반드시 알아야 한다는 소리다. 

    스프링 데이터 JPA는 JPA를 편리하게 사용할 수 있도록 도와주는 라이브러리다. 수많은 편리한 기능을 제공하지만, 주요 기능은 아래 두 가지다.

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

     

    공통 인터페이스 기능

    • Spring Data JPA는 JpaRepository 인터페이스를 통해서 기본적인 CRUD 기능을 제공한다.
    • 공통화 가능한 기능이 거의 모두 포함되어 있다. findAll(), save() 등등.

    • JpaRepository 코드를 열어보면 아래 코드들이 있는 것이 제공되는 것을 알 수 있다. 
    • 또한 PagingAndSortingRepository를 인터페이스 상속 받은 것을 볼 수 있는데,  PagingAndSortingRepository에서 사용하는 쿼리까지 제공해준다. (타고타고 올라가면 CrudRepository까지 나옴) 
    public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
    
       @Override
       List<T> findAll();
    
       @Override
       List<T> findAll(Sort sort);
       ...
       
    }

     

     

    쿼리 메서드 기능

    • 스프링 데이터 JPA는 인터페이스에 메서드만 적어두면, 메서드 이름을 분석해서 쿼리를 자동으로 만들고 실행해주는 기능을 제공한다. 
    • 순수 JPA를 사용한다면 간단한 반복적인 쿼리를 직접 작성해야한다. 이 부분을 Spring Data JPA가 해결해준다.

     아래 코드에서 예시를 살펴볼 수 있다. 순수 JPA를 사용한다면 쿼리를 직접 작성하고 파라메터 바인딩을 직접해야한다. 이런 반복 코드를 수없이 작성해야하는데 순수 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();
    }
    
    // Spring Data JPA
    public interface MemberRepository extends JpaRepository<Member, Long> {
     List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
    }

    그렇지만 쿼리 메서드에는 한계점이 있다. 파라메터가 많고 복잡한 경우, 메서드 선언 부분이 과도하게 복잡해 질 수 있다는 점이다. 이 때문에 스프링 데이터 JPA는 JPQL과 NativeQuery를 직접 작성할 수 있는 기능도 제공한다. 문제가 되는 코드는 아래와 같다. 

    List<Item> findByItemNameLikeAndPriceLessThanEqual(@Param("itemName") String itemName, @Param("price") Integer price);

     

    쿼리 메서드의 규칙

    쿼리 메서드를 작성할 때는 반드시 특정 규칙으로 작성을 해야한다. 그렇지 않다면 메서드 명에서 쿼리를 뽑아낼 수 없다.

    • 조회 : find...By, read...By, query...By, get...By
    • Count : count...By 반환타입 long
    • Exists : exists...By 반환타입 boolean
    • 삭제 : delete...By, remove...By 반환타입 long
    • Distinct : findDistinct, findMemberDistinctBy 
    • Limit : findFirst3, findFirst, findTop, findTop3

    기본적으로 위와 같은 형태로 파싱된다. 알아두면 좋을 부분은 By 다음에 조건이 와야하고 and, or, graterThan 같은 조건들이 올 수 있다는 것이다. 쿼리 메서드 필터 조건은 아래 공식 문서에서 확인할 수 있다. 

     

    JPQL, Native Query 직접 사용하기

    쿼리 메서드는 간단한 반복성 쿼리를 작성하는데는 사용할만하다. 그렇지만 복잡하거나 파라미터가 많은 경우에는 쿼리 메서드를 적용하기 부적절할 때가 있다. Spring Data JPA는 이 때를 위해서 JPQL, Native Query를 직접 Spring Data JPA에서 사용할 수 있도록 기능을 제공해준다. 

    @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);

    다음은 Spring Data JPA에서 JPQL을 직접 작성한 예시이다.

    • @Query 어노테이션을 반드시 작성한다.
    • @Param 어노테이션을 반드시 파라메터마다 기입한다. 그래야 파라미터 바인딩이 정적으로 된다. 
      • 특히 두 개 이상의 파라메터가 있을 경우, 반드시 @Param으로 맵핑해줘야한다. 

     


    JPA Repository 사용법

    • Spring Data JPA가 제공하는 기능을 사용하고자 한다면, 개발자는 JpaRepository 인터페이스를 상속받은 인터페이스를 생성하기만 하면 된다. 
    • JpaRepository 인터페이스만 상속받으면 스프링 데이터 JPA가 프록시 기술을 사용해서 구현 클래스(SimpleJpaRepository)를 생성해준다. 그리고 생성된 구현체는 스프링 빈으로 등록한다. 
    • https://ojt90902.tistory.com/712

     

     

     


    스프링 데이터 JPA 적용1 (SpringDataJpaItemRepository)

    스프링 데이터 JPA를 적용해 볼 수 있다.

    이전에 사용하던 ItemRepository 인터페이스는 상속 받지 않고 생성했다. 따라서 ItemService에서 DI를 할 때, 코드의 수정이 전반적으로 필요할 수 있다. 만약 ItemRepository 인터페이스 구현체가 스프링 데이터 JPA 인터페이스를 상속받은 인터페이스를 포함 관계로 가진다면 ItemService는 코드의 수정이 필요하지 않다.

    • JpaRepository를 상속 받으면서 findItemById() 같은 메서드들은 일반적으로 지원한다. 
    • 이름을 검색하거나, 가격으로 검색하는 기능은 공통 기능이 아니다. 따라서 쿼리 메서드를 이용해서 생성한다. 
    • 이름과 가격을 동시에 검색할 때 쿼리 메서드를 이용하면 필요 이상으로 메서드의 이름이 길어진다. 이 때, @Query를 이용해서 직접 쿼리를 작성할 수 있다. 

    순수 JPA, Spring Data JPA는 동적 쿼리에는 취약한 면모를 보인다. 동적 쿼리 작성은 QueryDSL을 사용하는 것이 좋으며 이 곳에서는 그냥 넘어간다. 

    public interface SpringDataJpaItemRepository extends JpaRepository<Item, Long> {
    
    
        List<Item> findByItemNameLike(String itemName);
        List<Item> findByPriceLessThanEqual(Integer price);
    
    
        // 쿼리 메서드 --> 순서대로 파라메터가 들어가야 함. 아래 메서드가 동일한 기능을 수행하지만, 파라메터가 많아질수록.. 빡세다.
        List<Item> findByItemNameLikeAndPriceLessThanEqual(@Param("itemName") String itemName, @Param("price") Integer price);
    
        // JPQL을 직접 작성. 반드시 Param 넣어야 함.
        @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  적용2

    앞서 생성한 SpringDataJpaItemRepository는 JpaRepository를 상속받았다. 따라서 ItemRepository 인터페이스와 관련이 없기 때문에 DI를 이용해서 클래스를 바꿔치기 할 수 없다. 이 때는 ItemRepository 인터페이스 구현체가 JpaRepository를 포함하는 관계를 가지면 해결 가능해진다. 

    • ItemService는 ItemRepository에 의존하기 때문에 ItemService에서 SpringDataJpaItemRepository를 그대로 사용할 수 없다. 
    • ItemService가 SpringDataJpaItemRepository를 직접 사용하도록 코드를 고치면 되겠지만, ItemService 코드의 변경 없이 ItemService가 ItemRepository에 대한 의존을 유지하면서 DI를 통해 구현 기술을 변경한다.
    • JpaItemRepositoryV2 (ItemRepository 인터페이스의 구현체)가 MemberRepository와 SpringDataJpaItemRepository 사이를 맞추기 위한 Adapter처럼 사용된다.

    코드는 아래와 같이 작성할 수 있다. 

    @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 item = repository.findById(itemId).orElseThrow();
    
            item.setItemName(updateParam.getItemName());
            item.setQuantity(updateParam.getQuantity());
            item.setPrice(updateParam.getPrice());
        }
    
        @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.findItems("%" + itemName + "%", maxPrice);
            } else if (StringUtils.hasText(itemName)) {
                // 하이버네이트 5.6.7 버전이 있을 때 에러가 발생함.
                // build.gradle에서 하이버네이트 버전 설정할 수 있음.
                return repository.findByItemNameLike("%" + itemName + "%");
            } else if (maxPrice != null) {
                return repository.findByPriceLessThanEqual(maxPrice);
            } else{
                return repository.findAll();
            }
        }
    }

     

    스프링 데이터 JPA  적용2 - 객체 간의 의존 관계

    위의 구조에서 클래스의 의존 관계는 다음과 같다

    • ItemService는 ItemRepository 인터페이스에 의존.
    • JpaItemRepositoryV2는 ItemRepository 인터페이스의 구현체임.
    • JpaItemRepositoryV2는 SpringDataJpaItemRepository 인터페이스를 포함함. JpaItemRepositoryV2에는 SpringDataJpaItemRepository 인터페이스의 구현체(프록시 객체)가 주입됨. 

    객체가 주입된(혹은 조립된) 런타임 시점을 바라본다면 ItemService → jpaItemRepositoryV2 → <<Proxy>> 스프링 데이터 JPA에 의존하는 형식이 된다. 


    스프링 데이터 JPA의 예외 변환 

    JPA는 EntityManager로 인터페이스한다. 스프링 데이터 JPA도 Wrapped 되었지만, EntityManager를 이용해서 인터페이스한다. EntityManager는 동작 중 에러가 발생하면 PersistenceException 등을 던진다. 이 때 @Repository 어노테이션이 등록되어있다면 해당 클래스는 예외변환 AOP를 통해 PersistenceExceptionTranslationInterceptor가 생성된다. PersistenceException..Interceptor는 Repository 객체에서 생성된 예외를 Catch 해서 스프링의 예외로 바꿔주는 역할을 한다.

    길게 이야기 했는데 "스프링 데이터 JPA는 JPA와 동일하게 @Repository 클래스를 이용해서 JPA의 예외를 스프링에서 사용하는 예외로 바꿀 수 있다"로 이해하면 된다. 

    'Spring > Spring DB' 카테고리의 다른 글

    Spring DB : 스프링 트랜잭션의 이해  (0) 2023.01.29
    Spring DB : JPA  (0) 2023.01.23
    Spring DB : MyBatis  (0) 2022.07.08
    Spring DB : DB 테스트  (0) 2022.07.04
    Spring DB : JdbcTemplate  (0) 2022.06.11

    댓글

    Designed by JB FACTORY