스프링 DB 2편 - 데이터 접근 활용 기술 (8) - 데이터 접근 기술 - 스프링 데이터 JPA
인프런 스프링 DB 2편 - 데이터 접근 활용 기술편을 학습하고 정리한 내용 입니다.
스프링 데이터 JPA 주요 기능
스프링 데이터 JPA는 JPA를 편리하게 사용할 수 있도록 도와주는 라이브러리이다. 수많은 편리한 기능을 제공하지만 가장 대표적인 기능은 다음과 같다.
- 공통 인터페이스 기능
- 쿼리 메서드 기능
공통 인터페이스 기능능

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

별의 별게 다있다.

JpaRepository인터페이스만 상속받으면 스프링 데이터 JPA가 프록시 기술을 사용해서 구현 클래스를 만들어준다. 그리고 만든 구현 클래스의 인스턴스를 만들어서 스프링 빈으로 등록한다.- 따라서 개발자는 구현 클래스 없이 인터페이스만 만들면 기본 CRUD 기능을 사용할 수 있다.
쿼리 메서드 기능
스프링 데이터 JPA는 인터페이스에 메서드만 적어두면, 메서드 이름을 분석해서 쿼리를 자동으로 만들고 실행해주는 기능을 제공한다.
순수 JPA 리포지토리
1
2
3
4
5
6
7
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- 예:)
findHelloBy처럼 …에 식별하기 위한 내용(설명)이 들어가도 된다.
- 예:)
- COUNT:
count…By반환타입 long - EXISTS:
exists…By반환타입 boolean - 삭제:
delete…By,remove…By반환타입 long - DISTINCT:
findDistinct,findMemberDistinctBy - LIMIT:
findFirst3,findFirst,findTop,findTop3
쿼리 메소드 필터 조건
스프링 데이터 JPA 공식 문서 참고
JPQL 직접 사용하기
1
2
3
4
5
6
7
8
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는 JPQL 뿐만 아니라 JPA의 네이티브 쿼리 기능도 지원하는데, JPQL 대신에 SQL을 직접 작성할 수 있다.
스프링 데이터 JPA는 JPA를 편리하게 사용하도록 도와주는 도구이다. 따라서 JPA 자체를 잘 이해하는 것이 가장 중요하다.
스프링 데이터 JPA 적용 1
설정
스프링 데이터 JPA는 spring-boot-starter-data-jpa라이브러리를 넣어주면 된다.
build.gradle 추가
1
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
그런데 이미 앞에서 JPA를 설정하면서 spring-boot-starter-data-jpa라이브러리를 넣어주었다.
스프링 데이터 JPA 적용
SpringDataJpaItemRepository
1
2
3
4
5
6
7
8
9
10
11
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는 동적 쿼리에 약하다. 이번에는 직접 4가지 상황을 스프링 데이터JPA로 구현해보자.
findItems()
메서드 이름으로 쿼리를 실행하는 기능은 다음과 같은 단점이 있다.
- 조건이 많으면 메서드 이름이 너무 길어진다.
- 조인 같은 복잡한 조건을 사용할 수 없다.
메서드 이름으로 쿼리를 실행하는 기능은 간단한 경우에는 매우 유용하지만, 복잡해지면 직접 JPQL 쿼리를 작성하는 것이 좋다.
- 쿼리를 직접 실행하려면
@Query애노테이션을 사용하면 된다. - 메서드 이름으로 쿼리를 실행할 때는 파라미터를 순서대로 입력하면 되지만, 쿼리를 직접 실행할 때는 파라미터를 명시적으로 바인딩 해야 한다.
- 파라미터 바인딩은
@Param("itemName")애노테이션을 사용하고, 애노테이션의 값에 파라미터 이름을 주면 된다.
스프링 데이터 JPA 적용 2
SpringDataJpaItemRepository를 ItemService에 의존시키고 싶지만, 그럴려면 service 코드를 다바꿔야 한다.
하나 더 둬서 service에 맞게 작동시켜보자.
JpaItemRepositoryV2
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
41
42
43
44
45
46
47
@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);
}
if (StringUtils.hasText(itemName)) {
return repository.findByItemNameLike("%"+itemName+"%");
}
if (maxPrice != null) {
return repository.findByPriceLessThanEqual(maxPrice);
}
return repository.findAll();
}
}
의존관계와 구조
ItemService는ItemRepository에 의존하기 때문에ItemService에서SpringDataJpaItemRepository를 그대로 사용할 수 없다.- 물론
ItemService가SpringDataJpaItemRepository를 직접 사용하도록 코드를 고치면 되겠지만,ItemService코드의 변경 없이ItemService가ItemRepository에 대한 의존을 유지하면서 DI를 통해 구현 기술을 변경하고 싶다.
조금 복잡하지만, 새로운 리포지토리를 만들어서 이 문제를 해결했다.
여기서는 JpaItemRepositoryV2가 ItemRepository와 SpringDataJpaItemRepository사이를 맞추기 위한 어댑터 처럼 사용된다.
\
JpaItemRepositoryV2는ItemRepository를 구현한다. 그리고SpringDataJpaItemRepository를 사용한다.

- 런타임의 객체 의존관계는 다음과 같이 동작한다.
itemService→jpaItemRepositoryV2→springDataJpaItemRepository(프록시 객체)
기능에서 save(), update(), findById(), findAll()을 구현했는데
따로 볼 건 findAll()을 한번 보자.
findAll()
데이터를 조건에 따라 4가지로 분류해서 검색한다.
- 모든 데이터 조회
- 이름 조회
- 가격 조회
- 이름 + 가격 조회
모든 조건에 부합할 때는 findByItemNameLikeAndPriceLessThanEqual()를 사용해도 되고, repository.findItems()를 사용해도 된다. 그런데 보는 것 처럼 조건이 2개만 되어도 이름이 너무 길어지는 단점이 있다. 따라서 스프링 데이터 JPA가 제공하는 메서드 이름으로 쿼리를 자동으로 만들어주는 기능과 @Query로 직접 쿼리를 작성하는 기능 중에 적절한 선택이 필요하다.
추가로 코드를 잘 보면 동적 쿼리가 아니라 상황에 따라 각각 스프링 데이터 JPA의 메서드를 호출해서 상당히 비효율적인 코드인 것을 알 수 있다.
스프링 데이터 JPA는 동적 쿼리 기능에 대한 지원이 매우 약하다. 이부분은 이후에 Querydsl을 사용해서 개선해보자.
설정하고 실행해보자.
SpringDataJpaConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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);
}
}
SpringDataJpaItemRepository는 스프링 데이터 JPA가 프록시 기술로 만들어주고 스프링 빈으로도 등록해준다.
ItemServiceApplication - 변경
1
2
3
4
5
@Import(SpringDataJpaConfig.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {
..
}
SpringDataJpaConfig를 사용하도록 변경했다.

테스트가 잘 통과되었다.
댓글남기기