이 글은 강의 : 김영한님의 - "[스프링 DB 2편 - 데이터 접근 활용 기술]"을 듣고 정리한 내용입니다. 😁😁
build.gradle
에 라이브러리 의존성을 추가하고 필요한 경우 application.properties
에 로그 설정값을 추가하면 된다.
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
위 코드를 build.gradle에 추가하면 된다. 이 의존성이 추가되면 다음 코드들이 추가된다.
jakarta.persistence-api : JPA 인터페이스
hibernate-core : JPA 구현체인 하이버네이트 라이브러리
spring-data-jpa : 스프링 데이터 JPA 라이브러리
JdbcTemplate 관련 라이브러리
(JPA의 구현체가 하이버네이트이기 때문에 하이버네이트라고 말하면 JPA라고 생각할 것.)
Spring Data JPA의 의존성을 추가해주면 JdbcTemplate 관련 라이브러리들이 알아서 추가된다. 따라서 JdbcTemplate을 위해서 build.gradle에 의존성을 추가해두었다면 그 부분을 삭제해도 무방하다.
# JPA Log
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
# JPA Log
spring.jpa.show-sql=true
위 코드를 application.properties에 추가해서 JPA 로그를 남길 수 있다. 각 명령어는 다음과 같이 동작한다.
라이브러리 설정을 완료했으면 이제 도메인과 리포지토리 영역에 JPA 사용을 위한 설정을 진행해야한다.
JPA를 사용하기 위해서는 기본적으로 사용하고자 하는 도메인(Item 도메인) JPA를 위한 어노테이션을 추가해야한다. JPA에서 사용하는 많은 어노테이션이 있으며 아래는 그 중에 극소수의 어노테이션이다.
@Data
@Entity // JPA가 사용하는 객체라는 뜻. 이게 있어야 JPA가 인식함.
//@Table(name = "item") // 테이블명 = 객체명이면 생략해도 가능함.
public class Item {
// DB에서 ID 값을 넣어줌.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "item_name", length = 10)
private String itemName;
@Column(name = "price")
private Integer price;
// 컬럼명 == 필드명이면 @Column 생략 가능함.
private Integer quantity;
// JPA는 public으로 default 생성자가 필요함. (프록시를 위해서)
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
JPA는 프록시를 이용해서 구현되어있다. 프록시를 원활하게 사용하기 위해서는 기본 생성자가 필요한데, JPA는 기술 스펙으로 반드시 기본 생성자를 요구한다. 따라서 위와 같이 사용 도메인에 기본 생성자를 추가한다.
가장 크게 추가되는 설정값은 다음 두 가지다.
🎈 EntityManager
JPA의 모든 동작은 EnitytManager 객체를 이용해서 실행된다. 따라서 Repository에는 EntityManager를 제공해줘야한다.
EntityManager는 EntityManagerFactory, JpaTransactionManager, DataSource 등 다양한 설정을 해야한다. 스프링부트는 이 과정을 모두 자동화해서 EntityManager를 스프링 빈으로 등록해준다.
🎈 @Transactional
@Slf4j
@Repository
@Transactional // JPA는 트랜잭션을 이용해서 동작한다.
@RequiredArgsConstructor
public class JpaItemRepository implements ItemRepository {
private final EntityManager em;
@Override
public Item save(Item item) {
em.persist(item);
return item;
}
// 더티 체크를 이용한 업데이트.
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
Item item = em.find(Item.class, itemId);
item.setItemName(updateParam.getItemName());
item.setPrice(updateParam.getPrice());
item.setQuantity(updateParam.getQuantity());
}
@Override
public Optional<Item> findById(Long id) {
return Optional.ofNullable(em.find(Item.class, id));
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
String jpql = "select i from Item i";
boolean firstFlag = false;
if (StringUtils.hasText(itemName) ) {
jpql = jpql + " where i.itemName like concat('%', :itemName, '%')";
firstFlag = true;
}
if (maxPrice != null) {
if (firstFlag) {
jpql = jpql + " and i.price <= :maxPrice";
}else{
jpql = jpql + " where 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();
}
}
EntityManager
를 DI 받아야 한다. 이게 있어야 JPA이다.. 여기에 저장하고 조회하고 등등 실행될 것이다. em.persist(item);
insert into item (id, item_name, price, quantity) values (default, ?, ?, ?)
위와 같이 JPA가 쿼리를 생성한다. 쿼리에서 확인해볼 부분은 id에 default 값이 들어간다는 점.
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를 사용할 때는 Update 쿼리를 직접 작성하지 않는다. 대신에 영속성 컨텍스트의 더티체크를 이용해서 실행된다.
🎈 JPA는 트랜잭션 커밋 전, flush()전, jpql 실행 전에 영속성 컨텍스트를 flush한다. flush하는 시점에 더티체크를 이용해서 변경점이 있는 엔티티에는 UPDATE SQL을 실행한다.
🎈 테스트 코드에서는 @Transactional
어노테이션 안에서 commit이 실행되지 않는다. 따라서 아래 두 가지 방법을 이용해서 수동 Commit 한 후에, Update 쿼리를 확인해 볼 수 있다.
JPA에서 단순히 PK를 기준으로 조회하는 것이 아닌, 여러 데이터를 복잡한 조건으로 데이터를 조회하려면 어떻게 하면 될까 ?
JPA는 JPQL(Java Persistence Query Language)라는 객체지향 쿼리 언어를 제공한다.
SQL이 테이블을 대상으로 한다면, JPQL은 엔티티 객체를 대상으로 한다. 따라서 JPQL을 사용할 때, from 절 뒤에 엔티티 객체의 이름이 들어간다. 그리고 모든 필드는 엔티티 객체의 별칭 + 컬럼명으로 접근한다. 아래에서 예시를 확인할 수 있다. (엔티티 객체와 속성의 대소문자는 구분해야 한다!)
// 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 문제점 중 하나는 동적쿼리를 작성하기가 어렵다는 것이다. 예를 들어 검색 조건에 따라서 동적쿼리를 생성하는 경우, 그 때 작성된 쿼리는 아래와 같고, 문제점도 살펴볼 수 있다.
순수 JPA에서 동적 쿼리를 작성할 때는 위 같은 문제점이 있기 때문에 QueryDSL과 함께 사용된다.
@Override
public List<Item> findAll(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
String jpql = "select i from Item i";
boolean firstFlag = false;
if (StringUtils.hasText(itemName) ) {
jpql = jpql + " where i.itemName like concat('%', :itemName, '%')";
firstFlag = true;
}
if (maxPrice != null) {
if (firstFlag) {
jpql = jpql + " and i.price <= :maxPrice";
}else{
jpql = jpql + " where 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();
}
JPA는 EntityManager를 이용해서 DB와 Repository 계층 사이에서 인터페이스한다. 만약 JPA가 동작하다가 문제가 발생하면 EntityManager에서 예외가 발생되게 될 것이다. 이 때, JPA는 스프링과 관련이 없기 때문에 JPA에서 사용하는 예외를 발생시킨다. 이 때 발생되는 예외는 PersistenceException, IllegalSTateException, IllegalArgumentException들이다.
만약 JPA, 정확하게는 EntityManager에서 Exception이 발생하게 된다면 여기서 발생하는 Exception이 서비스 계층까지 바로 다이렉트로 전달되게 될 것이다. 그리고 서비스 계층에서 바라보는 예외는 PersistenceException이고, 이 예외는 JPA와 관련된 예외다. 서비스 계층에서는 이 예외를 처리하기 위해서 PersistenceException을 캐치하는 로직이 들어가야한다. 이렇게 구성되게 된다면 Service 계층은 JPA 기술에 종속되게 된다. 계층별로 기술 종속성이 분리가 되지 않았다는 것이다.
🎈 @Repository를 Repository 계층에 설정해주면 위 문제가 해결된다. @Repository는 아래 두 가지 기능을 가진다.
Component Scan의 대상이 된다.
어노테이션이 달린 클래스를 예외변환 AOP의 적용 대상으로 만든다.
결국 런타임 예외를 잡아주고 스프링 예외로 변환해준다고 이해하면 된다.