22.11.14
JPA 적용1 - 개발
JPA에서 가장 중요한 부분은 객체와 테이블을 매핑하는 것이다.
JPA가 제공하는 애노테이션을 사용해서 Item 객체와 테이블을 매핑해보자.
Item 클래스 (도메인)
@Entity
@Setter @Getter
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가 인식할 수 있다. 이렇게 @Entity 가 붙은 객체를 JPA에서는 엔티티라 한다.
- @Id : 테이블의 PK와 해당 필드를 매핑한다.
- @GeneratedValue(strategy = GenerationType.IDENTITY) : PK 생성 값을 데이터베이스에서 생성하는 IDENTITY 방식을 사용한다. 예) MySQL auto increment
- @Column : 객체의 필드를 테이블의 컬럼과 매핑한다.
- name = "item_name" : 객체는 itemName 이지만 테이블의 컬럼은 item_name 이므로 이렇게 매핑했다.
- length = 10 : JPA의 매핑 정보로 DDL( create table )도 생성할 수 있는데, 그때 컬럼의 길이 값으로 활용된다. (varchar 10)
- @Column 을 생략할 경우 필드의 이름을 테이블 컬럼 이름으로 사용한다. 지금처럼 스프링 부트와 통합해서 사용하면 필드 이름을 테이블 컬럼 명으로 변경할 때 객체 필드의 카멜 케이스를 테이블 컬럼의 언더스코어로 자동으로 변환해준다. itemName->item_name , 따라서 위 예제의 @Column(name = "item_name") 를 생략해도 된다.
JPA는 public 또는 protected 의 기본 생성자가 필수이다. 기본 생성자를 꼭 넣어주자.
public Item() {}
JpaItemRepositoryV1 클래스
@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();
}
}
- private final EntityManager em : 생성자를 보면 스프링을 통해 엔티티 매니저( EntityManager ) 라는 것을 주입받은 것을 확인할 수 있다. JPA의 모든 동작은 엔티티 매니저를 통해서 이루어진다. 엔티티 매니저는 내부에 데이터소스를 가지고 있고, 데이터베이스에 접근할 수 있다.
- @Transactional : JPA의 모든 데이터 변경(등록, 수정, 삭제)은 트랜잭션 안에서 이루어져야 한다. 조회는 트랜잭션이 없어도 가능하다. 변경의 경우 일반적으로 서비스 계층에서 트랜잭션을 시작하기 때문에 문제가 없다. 하지만 이번 예제에서는 복잡한 비즈니스 로직이 없어서 서비스 계층에서 트랜잭션을 걸지 않았다. JPA에서는 데이터 변경시 트랜잭션이 필수다. 따라서 리포지토리에 트랜잭션을 걸어주었다. 일반적으로는 비즈니스 로직을 시작하는 서비스 계층에 트랜잭션을 걸어주는 것이 맞다.
참고: JPA를 설정하려면 EntityManagerFactory , JPA 트랜잭션 매니저( JpaTransactionManager ), 데이터소스 등등 다양한 설정을 해야 한다. 스프링 부트는 이 과정을 모두 자동화 해준다. main() 메서드 부터 시작해서 JPA를 처음부터 어떻게 설정하는지는 JPA 기본편을 참고하자. 그리고 스프링 부트의 자동 설정은 JpaBaseConfiguration 를 참고하자.
이전에는 스프링 부트가 DataSourceTransactionManager, JdbcTransactionManager 를 스프링 빈으로 등록해서 사용했다면, JPA 라이브러리를 의존하게 되면 JpaTransactionManager 를 스프링 빈으로 등록하는 등 최적화 해준다.
JpaConfig 설정 파일
@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 - 변경
@Import(JpaConfig.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {}
JPA 적용2 - 리포지토리 분석
JpaItemRepositoryV1 코드 분석
save() - 저장
public Item save(Item item) {
em.persist(item);
return item;
}
em.persist(item) : JPA에서 객체를 테이블에 저장할 때는 엔티티 매니저가 제공하는 persist() 메서드를 사용하면 된다.
JPA가 만들어서 실행한 SQL
insert into item (id, item_name, price, quantity) values (null, ?, ?, ?)
또는
insert into item (id, item_name, price, quantity) values (default, ?, ?, ?)
또는
insert into item (item_name, price, quantity) values (?, ?, ?)
JPA가 만들어서 실행한 SQL을 보면 id 에 값이 빠져있는 것을 확인할 수 있다. PK 키 생성 전략을 IDENTITY 로 사용했기 때문에 JPA가 이런 쿼리를 만들어서 실행한 것이다.
물론 쿼리 실행 이후에 Item 객체의 id 필드에 데이터베이스가 생성한 PK값이 들어가게 된다. (JPA가 INSERT SQL 실행 이후에 생성된 ID 결과를 받아서 넣어준다)
update() - 수정
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());
}
JPA가 만들어서 실행한 SQL
update item set item_name=?, price=?, quantity=? where id=?
- em.update() 같은 메서드를 전혀 호출하지 않았다. 그런데 어떻게 UPDATE SQL이 실행되는 것일까?
- JPA는 트랜잭션이 커밋되는 시점에, 변경된 엔티티 객체가 있는지 확인한다. (영속성 컨텍스트의 스냅샷과 비교) 특정 엔티티 객체가 변경된 경우에만 UPDATE SQL을 실행한다.
- 테스트의 경우 마지막에 트랜잭션이 롤백되기 때문에 JPA는 UPDATE SQL을 실행하지 않는다. 테스트에서 UPDATE SQL을 확인하려면 @Commit 을 붙이면 확인할 수 있다. ( 또는 em.flush() )
JPA가 어떻게 변경된 엔티티 객체를 찾는지 명확하게 이해하려면 영속성 컨텍스트라는 JPA 내부 원리를 이해해야 한다.
findById() - 단건 조회
public Optional<Item> findById(Long id) {
Item item = em.find(Item.class, id);
return Optional.ofNullable(item);
}
JPA에서 엔티티 객체를 PK를 기준으로 조회할 때는 find() 를 사용하고 조회 타입과, PK 값을 주면 된다. 그러면 JPA가 다음과 같은 조회 SQL을 만들어서 실행하고, 결과를 객체로 바로 변환해준다.
JPA가 만들어서 실행한 SQL
select
item0_.id as id1_0_0_,
item0_.item_name as item_nam2_0_0_,
item0_.price as price3_0_0_,
item0_.quantity as quantity4_0_0_
from item item0_
where item0_.id=?
JPA(하이버네이트)가 만들어서 실행한 SQL은 별칭이 조금 복잡하다.
조인이 발생하거나 복잡한 조건에서도 문제 없도록 기계적으로 만들다 보니 이런 결과가 나온 듯 하다.
JPA에서 단순히 PK를 기준으로 조회하는 것이 아닌, ( find( id) )
여러 데이터를 복잡한 조건으로 데이터를 조회하려면 어떻게 하면 될까? ( createQuery( jpql ) )
findAll - 목록 조회
public List<Item> findAll(ItemSearchCond cond) {
String jpql = "select i from Item i";
//동적 쿼리 생략
TypedQuery<Item> query = em.createQuery(jpql, Item.class);
return query.getResultList();
}
* JPQL *
JPA는 JPQL(Java Persistence Query Language)이라는 객체지향 쿼리 언어를 제공한다.
주로 여러 데이터를 복잡한 조건으로 조회할 때 사용한다.
SQL이 테이블을 대상으로 한다면, JPQL은 엔티티 객체를 대상으로 SQL을 실행한다.
엔티티 객체를 대상으로 하기 때문에 from 다음에 Item 엔티티 객체 이름이 들어간다.
엔티티 객체와 속성의 대소문자는 구분해야 한다.
JPQL은 SQL과 문법이 거의 비슷하기 때문에 개발자들이 쉽게 적응할 수 있다.
결과적으로 JPQL을 실행하면 그 안에 포함된 엔티티 객체의 매핑 정보를 활용해서 SQL을 만들게 된다.
실행된 JPQL
select i from Item i
where i.itemName like concat('%',:itemName,'%')
and i.price <= :maxPrice
JPQL을 통해 실행된 SQL
select
item0_.id as id1_0_,
item0_.item_name as item_nam2_0_,
item0_.price as price3_0_,
item0_.quantity as quantity4_0_
from item item0_
where (item0_.item_name like ('%'||?||'%'))
and item0_.price<=?
JPQL 파라미터
where price <= :maxPrice
파라미터 바인딩: query.setParameter("maxPrice", maxPrice)
동적 쿼리 문제
JPA는 거의 SQL을 사용하기 때문에 동적 쿼리 문제가 남아있다.
동적 쿼리는 뒤에서 설명하는 Querydsl이라는 기술을 활용하면 매우 깔끔하게 사용할 수 있다.
실무에서는 동적 쿼리 문제 때문에, JPA 사용할 때 Querydsl도 함께 선택하게 된다.
JPA 적용3 - 예외 변환
EntityManager 는 순수한 JPA 기술이고, 스프링과는 관계가 없다. 따라서 엔티티 매니저는 예외가 발생하면 JPA 관련 예외를 발생시킨다.
JPA는 PersistenceException 과 그 하위 예외를 발생시킨다. 추가로 JPA는 IllegalStateException , IllegalArgumentException 을 발생시킬 수 있다.
그렇다면 JPA 예외를 스프링 예외 추상화( DataAccessException )로 어떻게 변환할 수 있을까?
비밀은 바로 @Repository 에 있다.
예외 변환 전

@Repository의 기능
- @Repository 가 붙은 클래스는 컴포넌트 스캔의 대상이 된다.
- @Repository 가 붙은 클래스는 스프링 예외 변환 AOP의 적용 대상이 된다.
스프링과 JPA를 함께 사용하는 경우 스프링은 JPA 예외 변환기 ( PersistenceExceptionTranslator )를 등록한다.
예외 변환 AOP 프록시는 JPA 관련 예외가 발생하면 JPA 예외 변환기를 통해 발생한 예외를 스프링 데이터 접근 예외로 변환한다.
예외 변환 후

- 엔티티 매니저에서 발생한 PersistException (런타임 예외) 이 스프링이 생성한 예외 변환 AOP 프록시까지 던져지고 여기서 JPA 예외를 스프링 예외로 변환해준다.
- 즉, 서비스 계층에서 의존 관계 주입받은 @Repository ItemRepository 는 예외 변환 AOP 프록시이다. ( 현재 테스트 코드에서는 @Transactional 애노테이션이 있기 때문에 트랜잭션 AOP 프록시도 껴있다 )
- 결과적으로 리포지토리에 @Repository 애노테이션만 있으면 스프링이 예외 변환을 처리하는 AOP를 만들어준다.
참고: 복잡한 과정을 거쳐서 실제 예외를 변환하는데, 실제 JPA 예외를 변환하는 코드는 EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible() 이다.
6. 데이터 접근 기술 - 스프링 데이터 JPA
스프링 데이터 JPA 소개1 - 등장 이유
자바 ORM 표준 인터페이스: JPA (Java Persistence API)
JPA 구현체: 하이버네이트, EclipseLink, OpenJPA 등
스프링 데이터
다양한 RDB에 대한 CRUD 를 추상화한 인터페이스 제공 (어떤 저장소를 쓰던 동일한 방식으로 사용할 수 있음)
페이징 처리, 메서드 이름으로 쿼리 생성, 스프링 MVC 에서 id 값만 넘겨도 도메인 클래스로 바인딩 등 편의 기능 제공
스프링 데이터 JPA
스프링 데이터 Common 이라는 공통 인터페이스 + JPA 관련 기능
스프링 데이터 JPA 소개2 - 기능
스프링 JdbcTemplate
public Member findOne(Long id) {
String sql = "select MEMBER_ID as id, USERNAME, PHONE_NUMBER from MEMBER where id = ?";
Member member = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<Member>(), id);
return member;
}
스프링 JPA
@Repository
public class MemberRepository {
@PersistenceContext
EntityManager em; //컨테이너 주입
public Long save(Member member) {
em.persist(member);
return member.getId();
}
Spring Data JPA
public interface MemberRepository extends JpaRepository<Member, Long> {
//실제 아무것도 없음
}
JpaRepository 인터페이스
- <S extends T> S save(S entity)
- void delete(ID id)
- Optional<T> findById(ID id)
- Iterable<T> findAll()
- long count() 등
구현 클래스는 동적 프록시 기술로 자동으로 생성해준다.
Spring Data JPA 기능
- 메서드 이름으로 쿼리 생성
- @Query: 인터페이스에 쿼리 작성 가능, JPA 네이티브 쿼리 지원
- @Modifying: 수정 쿼리도 직접 정의 가능

장점
- 코딩양
- 도메인 클래스를 중요하게 다룸
- 비지니스 로직 이해 쉬움
- 더 많은 테스트 케이스 작성 가능
- 너무 복잡한 쿼리는 SQL 직접 사용하면 됨
JPA, 하이버네이트, JDBC, DB 에 대한 이해가 선행되어야 한다.
JPA 도입 전,
직접 작성한 JPQL이 어떤 SQL 쿼리로 생성될지 예상할 수 있어야 한다.
즉시, 지연 로딩 전략에 대한 이해가 있어야 한다.
데이터 JPA 주요 기능
스프링 데이터 JPA는 JPA를 편리하게 사용할 수 있도록 도와주는 라이브러리이다.
수많은 편리한 기능을 제공하지만 가장 대표적인 기능은 다음과 같다.
- 공통 인터페이스 기능
- 쿼리 메서드 기능
공통 인터페이스 기능

- JpaRepository 인터페이스를 통해서 기본적인 CRUD 기능 제공한다.
- 공통화 가능한 기능이 거의 모두 포함되어 있다.
- CrudRepository 에서 fineOne() -> findById() 로 변경되었다.
JpaRepository 사용법
public interface ItemRepository extends JpaRepository<Member, Long> {
}
- JpaRepository 인터페이스를 인터페이스 상속 받고,
- 제네릭에 관리할 <엔티티, 엔티티ID> 명시
그러면 JpaRepository 가 제공하는 기본 CRUD 기능을 모두 사용할 수 있다.
스프링 데이터 JPA가 구현 클래스를 대신 생성
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 가 정한 메서드 이름 생성 규칙을 따라야 한다.
스프링 데이터 JPA가 제공하는 쿼리 메소드 기능
- 조회: 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
...에는 식별하기 위한 내용(설명)이 들어가도 된다.
쿼리 메소드 필터 조건
스프링 데이터 JPA 공식 문서 참고
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.limit-query-result
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는 JPQL 뿐만 아니라 JPA의 네이티브 쿼리 기능도 지원하는데, JPQL 대신에 SQL을 직접 작성할 수 있다.
중요
스프링 데이터 JPA는 JPA를 편리하게 사용하도록 도와주는 도구이다.
따라서 JPA 자체를 잘 이해하는 것이 가장 중요하다.
스프링 데이터 JPA 적용1
설정
스프링 데이터 JPA는 spring-boot-starter-data-jpa 라이브러리를 넣어주면 된다.
build.gradle - 추가
//JPA, 스프링 데이터 JPA 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
JPA를 설정하면서 이미 spring-boot-starter-data-jpa 라이브러리를 넣어주었다.
여기에는 JPA , 하이버네이트, 스프링 데이터 JPA( spring-data-jpa ), 그리고 스프링 JDBC 관련 기능도 모두 포함되어 있다.
SpringDataJpaItemRepository 인터페이스
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
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 기능을 사용할 수 있다. 그러나 스프링 데이터 JPA 도 구현체에서는 결국 JPA 기술을 사용한다.
- 그런데 이름으로 검색하거나, 가격으로 검색하는 기능은 공통으로 제공할 수 있는 기능이 아니다. 따라서 쿼리 메서드 기능을 사용하거나 @Query 를 사용해서 직접 쿼리를 실행해야 한다.
여기서는 데이터를 조건에 따라 4가지로 분류해서 검색한다.
- 모든 데이터 조회 ( JpaRepository.findAll() )
- 이름 조회 ( findByItemNameLike() )
- 가격 조회 ( findByPriceLessThanEqual() )
- 이름 + 가격 조회 ( findByItemNameLikeAndPriceLessThanEqual() , findItems() )
동적 쿼리를 사용하면 좋겠지만, 스프링 데이터 JPA는 동적 쿼리에 약하다.
그래서 2가지 조건이 있을 때의 4가지 상황을 각각 구현했다.
이 문제는 이후에 Querydsl에서 동적 쿼리로 깔끔하게 해결할 수 있다.
참고: 스프링 데이터 JPA도 Example 이라는 기능으로 약간의 동적 쿼리를 지원하지만, 실무에서 사용하기는 기능이 빈약하다. 실무에서 JPQL 동적 쿼리는 Querydsl을 사용하는 것이 좋다.
findByItemNameLikeAndPriceLessThanEqual()
이름과 가격 조건을 검색했을 때 사용하는 쿼리 메서드이다.
실행되는 JPQL : select i from Item i where i.itemName like ? and i.price <= ?
findItems()
메서드 이름으로 쿼리를 실행하는 기능은 다음과 같은 단점이 있다.
1. 조건이 많으면 메서드 이름이 너무 길어진다.
2. 조인 같은 복잡한 조건을 사용할 수 없다.
메서드 이름으로 쿼리를 실행하는 기능은 간단한 경우에는 매우 유용하지만, 복잡해지면 직접 JPQL 쿼리를 작성하는 것이 좋다.
@Query 애노테이션
JPQL 쿼리를 직접 실행할 때 사용
메서드 이름으로 쿼리를 실행할 때는 파라미터를 순서대로 입력하면 되지만, 쿼리를 직접 실행할 때는 파라미터를 명시적으로 바인딩 해야 한다.
파라미터 바인딩은 @Param("itemName") 애노테이션을 사용하고, 애노테이션의 값에 파라미터 이름을 주면 된다.
스프링 데이터 JPA 적용2
의존관계와 구조
ItemService 는 ItemRepository 에 의존하기 때문에 ItemService 에서 SpringDataJpaItemRepository 를 그대로 사용할 수 없다. ( SpringDataJpaItemRepository 는 JpaRepository 를 상속 받았을 뿐 ItemRepository 와는 관련이 없다 )
ItemService 가 SpringDataJpaItemRepository 를 직접 사용하도록 코드를 고치면 되겠지만, ItemService 코드의 변경없이 ItemService 가 ItemRepository 에 대한 의존을 유지하면서 DI를 통해 구현 기술을 변경하고 싶다.
JpaItemRepositoryV2 클래스
JpaItemRepositoryV2 가 ItemRepositoryV1 과 SpringDataJpaItemRepository 사이를 맞추기 위한 어댑터 처럼 사용된다.
SpringDataJpaItemRepository 는 인터페이스이다.
@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.findItems("%" + itemName + "%", maxPrice);
//return repository.findByItemNameLikeAndPriceLessThanEqual("%" + itemName + "%", maxPrice);
} else if (StringUtils.hasText(itemName)){
return repository.findByItemNameLike("%" + itemName + "%");
} else if (maxPrice != null){
return repository.findByPriceLessThanEqual(maxPrice);
} else {
return repository.findAll();
}
}
}
findAll() 에서 동적 쿼리가 아니라 상황에 따라 스프링 데이터 JPA의 메서드를 각각 호출하고 있다. -> 비효율적임
스프링 데이터 JPA는 동적 쿼리 기능에 대한 지원이 매우 약하기 때문에, 이 부분은 Querydsl 을 통해 해결할 수 있다.
클래스 의존 관계

JpaItemRepositoryV2 는 ItemRepository 를 구현하고, SpringDataJpaItemRepository 를 사용한다.'
ItemService 는 JpaItemRepositoryV2 를 통해 스프링 데이터 JPA 기능을 사용할 수 있다.
런타임 객체 의존 관계

itemService -> jpaItemRepositoryV2 -> springDataJpaItemRepository(프록시 객체)
springDataJpaItemRepository 구현체는 런타임에 스프링 생성하여 스프링 컨테이너에 등록해준다.
중간에서 JpaItemRepository 가 어댑터 역할을 해준 덕분에 ItemService 가 사용하는 ItemRepository 인터페이스를 그대로 유지할 수 있고 클라이언트인 ItemService 의 코드를 변경하지 않아도 되는 장점이 있다.
SpringDataJpaConfig 설정 파일
@Configuration
public class SpringDataJpaConfig {
private final SpringDataJpaItemRepository springDataJpaItemRepository;
public SpringDataJpaConfig(SpringDataJpaItemRepository springDataJpaItemRepository) {
this.springDataJpaItemRepository = springDataJpaItemRepository;
}
@Bean
public ItemService itemService(){
return new ItemServiceV1(itemRepository());
}
@Bean
public ItemRepository itemRepository(){
return new JpaItemRepositoryV2(springDataJpaItemRepository);
}
}
SpringDataJpaItemRepository 는 스프링 데이터 JPA가 프록시 기술로 만들어주고 스프링 빈으로도 등록해준다.
ItemServiceApplication - 변경
@Import(SpringDataJpaConfig.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {}
참고: 스프링 예외 변환
스프링 데이터 JPA도 스프링 예외 추상화를 지원한다. 스프링 데이터 JPA가 만들어주는 프록시에서 이미 예외 변환을 처리하기 때문에, @Repository 와 관계없이 예외가 변환된다.
주의 - 하이버네이트 버그
하이버네이트 5.6.6 ~ 5.6.7 을 사용하면 Like 문장을 사용할 때 다음 예외가 발생한다.
java.lang.IllegalArgumentException: Parameter value [\] did not match expected type [java.lang.String (n/a)]
build.gradle에 다음을 추가해서 하이버네이트 버전을 문제가 없는 5.6.5.Final 로 맞추자.
ext["hibernate.version"] = "5.6.5.Final"