스프링 DB 2편 - 데이터 접근 활용 기술 (7) - 데이터 접근 기술 - JPA -2
인프런 스프링 DB 2편 - 데이터 접근 활용 기술편을 학습하고 정리한 내용 입니다.
JPA 적용 2 - 리포지토리 분석
JpaItemRepositoryV1 코드를 분석해보자.
save() - 저장
1
2
3
4
5
@Override
public Item save(Item item) {
em.persist(item); // persist : 영구히 저장한다
return item;
}
em.persist(item): JPA에서 객체를 테이블에 저장할 때는 엔티티 매니저가 제공하는persist()메서드 를 사용하면 된다.
JPA가 만들어서 실행한 SQL
1
2
3
4
5
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 결과를 받아서 넣어준다)
1
2
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
update() - 수정
1
2
3
4
5
6
7
8
9
10
@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());
// 업데이트 저장?
// 안해줘도 된다. 더티 채킹 (트랜잭션 끝나는 시점에)
}
JPA가 만들어서 실행한 SQL
1
update item set item_name=?, price=?, quantity=? where id=?
em.update()같은 메서드를 전혀 호출하지 않았다. 그런데 어떻게 UPDATE SQL이 실행되는 것일까?- JPA는 트랜잭션이 커밋되는 시점에, 변경된 엔티티 객체가 있는지 확인한다. 특정 엔티티 객체가 변경된 경우에는 UPDATE SQL을 실행한다.
- JPA가 어떻게 변경된 엔티티 객체를 찾는지 명확하게 이해하려면 영속성 컨텍스트라는 JPA 내부 원리를 이해해야 한다.
- 테스트의 경우 마지막에 트랜잭션이 롤백되기 때문에 JPA는 UPDATE SQL을 실행하지 않는다. 테스트에서UPDATE SQL을 확인하려면
@Commit을 붙이면 확인할 수 있다.
findById() - 단건 조회
1
2
3
4
5
@Override
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
1
2
3
4
5
6
7
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은 별칭이 조금 복잡하다. 조인이 발생하거나 복잡한 조건에서도 문제 없도록 기계적으로 만들다 보니 이런 결과가 나온 듯 하다.
findAll - 목록 조회
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
@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();
}
JPQL
JPA는 JPQL(Java Persistence Query Language)이라는 객체지향 쿼리 언어를 제공한다. 주로 여러 데이터를 복잡한 조건으로 조회할 때 사용한다. SQL이 테이블을 대상으로 한다면, JPQL은 엔티티 객체를 대상으로 SQL을 실행한다 생각하면 된다.
엔티티 객체를 대상으로 하기 때문에 from다음에 Item엔티티 객체 이름이 들어간다. 엔티티 객체와 속성의 대소문자는 구분해야 한다.
결과적으로 JPQL을 실행하면 그 안에 포함된 엔티티 객체의 매핑 정보를 활용해서 SQL을 만들게 된다.
실행된 JPQL
1
2
3
select i from Item i
where i.itemName like concat('%',:itemName,'%')
and i.price <= :maxPrice
이건 JPQL이 나간거고, 실제 SQL로 변환되면
1
2
3
4
5
6
7
8
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)
동적 쿼리를 작성하는 행위는 진짜 힘들다. Querydsl 기술을 사용할 때까지 버티자..
JPA 적용 3 - 예외 변환
JPA의 경우 예외가 발생하면 JPA 예외가 발생하게 된다.
1
2
3
4
5
6
7
8
9
10
@Repository
@Transactional
public class JpaItemRepositoryV1 implements ItemRepository {
private final EntityManager em;
public JpaItemRepositoryV1(EntityManager em) {
this.em = em;
}
}
EntityManager는 순수한 JPA 기술이고, 스프링과는 관계가 없다. 따라서 엔티티 매니저는 예외가 발생하면 JPA 관련 예외를 발생시킨다.- JPA는
PersistenceException과 그 하위 예외를 발생시킨다.- 추가로 JPA는
IllegalStateException,IllegalArgumentException을 발생시킬 수 있다.
- 추가로 JPA는
- 그렇다면 JPA 예외를 스프링 예외 추상화(
DataAccessException)로 어떻게 변환할 수 있을까? - 비밀은 바로
@Repository에 있다.

이러면 서비스 계층이 JPA라는 특정 기술에 종속되어 버리는 것이다.
@Repository의 기능
@Repository가 붙은 클래스는 컴포넌트 스캔의 대상이 된다.@Repository가 붙은 클래스는 예외 변환 AOP의 적용 대상이 된다.- 스프링과 JPA를 함께 사용하는 경우 스프링은 JPA 예외 변환기(
PersistenceExceptionTranslator)를 등록한다. - 예외 변환 AOP 프록시는 JPA 관련 예외가 발생하면 JPA 예외 변환기를 통해 발생한 예외를 스프링 데이터 접근 예외로 변환한다.
- 스프링과 JPA를 함께 사용하는 경우 스프링은 JPA 예외 변환기(

4번 그림이 핵심이다.
결과적으로 리포지토리에 @Repository 애노테이션만 있으면 스프링이 예외 변환을 처리하는 AOP를 만들어준다.

테스트에서 repository를 찍어보면 CGLIB로 프록시로 만들어지는걸 볼 수 있다. 스프링이 예외 변환을 처리하는 AOP를 적용하는 걸 확인해볼 수 있는 것이다.
참고
스프링 부트는PersistenceExceptionTranslationPostProcessor를 자동으로 등록하는데, 여기에서@Repository를 AOP 프록시로 만드는 어드바이저가 등록된다. 복잡한 과정을 거쳐서 실제 예외를 변환하는데, 실제 JPA 예외를 변환하는 코드는EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible()이다.
댓글남기기