Spring + JPA의 조합으로 개발이 이루어지던 도중 다른 여러 데이터베이스 기술들이 등장하게 된다.
그런데 데이터베이스 기술들이 아무리 나와도 결국 동작은 데이터를 저장하고, 조회하는 기능이 모두 공통적으로 있다. 그것이 데이터베이스의 목표이다.
그래서 이러한 것을 공통 기능들을 처리하는 것이 필요하다고 느껴져 만들어진게 Spring DATA이다.
여기서 각각의 데이터베이스 기술에 조금 더 특화된 기술이 나오게 되는데 그것이 Spring Data JPA, Spring Data Redis, Spring Data Mongo 등등 이다.
이 기술들이 기본적인 CRUD, 페이징 등등의 다양한 기능을 간단하게 처리할 수 있도록 제공해준다. 그렇다고 Spring Data만 알면 되느냐? 그렇지 않다.
Spring Data만 알고 어떻게 동작하도록 개발해볼 수는 있겠으나, 이는 자바를 모르고 스프링을 사용하는 것과 같다. Spring Data는 JPA나 Mongo같은 해당 기술을 아는 사람이 편하게 사용하려고 사용하는 것이다.
스프링 데이터 JPA
간단히 알아보자면 기본적으로 스프링 데이터 JPA를 사용하면 인터페이스를 만들어놓기만해도 알아서 CRUD 메서드들을 다 만들어주고, 필요한 추가 기능이 있으면 메서드를 선언만 해도 그 이름을 가지고 로직을 만들어준다.
장점
- 코딩량이 현저하게 줄어듦
- 도메인 클래스를 중요하게 다룸
- 비즈니스 로직 이해 쉬움
- 더 많은 테스트 케이스 작성 가능
주의
- JPA(하이버네이트) 이해 필요
- 데이터베이스 설계 이해
- 대부분의 문제는 JPA를 모르고 사용해서 발생한다.
Spring Data JPA는 JPA를 편하게 사용하는 기술이라는 것을 꼭 명심해야 한다. 어떤 오류가 발생했을 때 고치려면 JPA에 대해 잘 알아야 한다.
스프링 데이터 JPA 주요 기능
수 많은 편리한 기능을 제공하지만 대표적인 기능은 아래와 같다.
- 공통 인터페이스 기능
- 쿼리 메서드 기능
공통 인터페이스 기능
스프링 데이터 JPA를 이용하면 JPARepository 인터페이스를 통해 Spring Data로 기본적인 CRUD 기능을 제공해준다.
- JpaRepository 인터페이스를 통해 기본적인 CRUD 기능을 제공한다.
- 공통화 가능한 기능이 거의 모두 포함되어 있다.
- 기본적인 CRUD를 포함해, 페이징, 카운팅 등등이 제공된다.
- CrudRepository에서 fineOne()이 findById()로 변경되었다.
1
2
public interface ItemRepository extends JpaRepository<Member, Long> {
}
- JpaRepository 인터페이스를 만들어 인터페이스 상속(extends)받고, 제네릭에 관리할 엔티티, 엔티티 ID를 주면 된다.
- 그러면 JpaRepository가 제공하는 기본 CRUD 기능을 모두 사용할 수 있다.
- ItemRepository 안에 내용이 아무것도 없어도 동작하는 것이다. (extends)
- JpaRepository 인터페이스만 상속받으면 스프링 데이터 JPA가 프록시 기술을 사용해 구현 클래스를 만들어준다. 그리고 만든 구현 클래스의 인스턴스를 만들어 스프링 빈으로 등록한다.
- 따라서 개발자는 구현 클래스 없이 인터페이스만 만들면 기본 CRUD 기능을 사용할 수 있다.
쿼리 메서드 기능
스프링 데이터 JPA는 인터페이스에 메서드만 적어두면, 메서드 이름을 분석해 쿼리를 자동으로 만들고 실행해주는 기능을 제공한다.
우선 순수 JPA 리포지토리에서 작성하는 예시를 보자.
1
2
3
4
5
6
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를 사용한다면?
1
2
3
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
- findByUsername => username으로 찾겠다.
- UsernameAnd => And를 통해 조건을 더 붙일 수 있고,
- AndAgeGreaterThan => GreaterThan처럼 더 크다 같은 것도 넣을 수 있다.
- findHelloBy 처럼 …에 식별하기 위한 내용이 들어가기도 한다.
- COUNT
- count…By
- 반환타입: long
- EXISTS
- exists…By
- 반환타입: boolean
- 삭제
- delete…By, remove…By
- 반환타입: long
- DISTINCT
- findDistinct, findMemberDistinctBy
- LIMIT
- findFirst3, findFirst, findTop, findTop3
자세한 내용은 아래 공식문서를 보면 알 수 있다.
JPQL 직접 사용하기
JPQL을 직접 사용할 수도 있다.
1
2
3
4
5
6
7
8
public interface ItemRepository 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를 통해 직접 작성하여 사용할 수 있다.
- 이때는 메서드 이름으로 실행하는 규칙은 무시된다.
- 참고로 스프링 데이터 JPA는 JPQL뿐만 아니라 JPA의 네이티브 쿼리 기능도 지원하는데, JPQL 대신 SQL을 직접 작성할 수 있다.
스프링 데이터 JPA 적용
spring-boot-starter-data-jpa를 추가하면 스프링 데이터 JPA도 포함되기 때문에 build.gradle에 따로 추가하지 않아도 된다.
Repository
1
2
3
4
5
6
7
8
9
10
11
12
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 기능을 사용할 수 있다.
- findById() 등등..
- 그런데 이름으로 검색하거나, 가격으로 검색하는 기능은 공통으로 제공할 수 없다. 따라서 쿼리 메서드 기능을 사용하거나 @Query를 사용해 직접 쿼리를 실행하면 된다.
여기서는 데이터를 조건에 따라 4가지로 분류해 검색한다.
- 모든 데이터 조회
- findAll()
- 이름 조회
- findByItemNameLike()
- 가격 조회
- findByPriceLessThanEqual()
- 이름 + 가격 조회
- findItems(), findByItemNameLikeAndPriceLessThanEqual()
동적 쿼리를 사용하면 좋겠지만, 스프링 데이터 JPA는 동적쿼리에 약하기 때문에 직접 4가지 상황을 스프링 데이터 JPA로 구현해 본 것이다.
이러한 문제는 Querydsl을 사용하면 깔끔하게 해결된다.
- findItems()
- 메서드 이름으로 쿼리 실행하는 기능은 조건이 많으면 findByItemNameLikeAnd… 처럼 메서드 이름이 너무 길어진다. 그리고 조인 같은 복잡한 조건을 사용할 수 없다.
- 따라서 복잡해질 경우 직접 JPQL 쿼리를 작성하는 것이 좋다.
- @Query 애노테이션을 사용한다.
- 파라미터를 명시적으로 바인딩해주어야 한다.
- 파라미터 바인딩은 @Param() 애노테이션을 사용하고 애노테이션 값에 파라미터 이름을 주면 된다.
적용에서의 문제(새로운 적용)
Service 코드를 보면 ItemRepository를 주입받고 itemRepository를 사용해 save(), update() 등등의 코드를 작성했다.
위에서 만든 SpringDataJpaItemRepository를 직접 서비스 코드에 가져다 쓰게 되면 기존의 서비스 코드를 싹 다 고쳐야 한다.
이러한 문제를 해결하기 위해 중간 계층을 하나 둔다.
Repository
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@RequiredArgsConstructor
@Repository
@Transactional
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();
}
}
}
- ItemService는 ItemRepository에 의존하기 때문에 ItemService에서는 JpaRepository를 인터페이스 상속받는 SpringDataJpaItemRepository를 직접 사용할 수 없다.
- 코드를 고치면 작동하게 할 수는 있지만 ItemService의 코드 변경 없이 ItemRepository에 대한 의존을 유지하면서 DI를 통해 구현 기술의 변경이 목적이기 때문이다.
따라서 새로운 리포지토리를 만들어 이 문제를 해결한다.
JpaItemRepositoryV2가 ItemRepository와 SpringDataJpaItemRepository 사이를 맞추기 위한 어댑터 처럼 동작하는 것이다.
- JpaItemRepositoryV2는 ItemRepository를 구현한다. 그리고 JpaItemRepositoryV2가 SpringDataJpaItemRepository를 사용한다.
런타임 시 이러한 의존관계 동작을 가진다.
추가로 findAll()의 코드를 보면 동적 쿼리가 아닌 상황에 따라 각각 스프링 데이터 JPA의 메서드를 호출하는 것을 볼 수 있다.
이러한 구조는 상당히 비효율적이다. 이러한 부분은 반복해서 말했듯 Querydsl을 사용하여 해결할 수 있다.
정리
스프링 데이터 JPA의 대표 기능들을 알아보았다. 이 외에도 수 많은 기능들을 제공한다. 어렵게 사용하는 페이징을 위한 기능들도 제공한다.
스프링 데이터 JPA는 단순하게 편리함을 넘어 많은 개발자들이 똑같은 코드로 중복 개발하는 부분을 개선해준다.
하지만 이런 기술을 사용하기 위해 JPA를 자세히 알아야 한다. JPA와 스프링 데이터 JPA의 상세한 내용은 추후 포스트에서 기술한다.