kimjingyu 2023. 3. 5. 18:19
728x90
  • 스프링: DI 컨테이너를 포함한 애플리케이션 전반의 다양한 기능을 제공
  • JPA: ORM 데이터 접근 기술을 제공
    • JdbcTemplate, MyBatis 같은 SQL Mapper 기술은 SQL을 개발자가 직접 작성해야 한다.
    • JPA를 사용하면 SQL도 JPA가 대신 작성하고 처리해준다.
  • 스프링 데이터 JPA, Querydsl 라는 JPA를 편리하게 사용하도록 도와주는 기술을 함께 사용한다.

설정

  • spring-boot-starter-data-jpa 라이브러리를 사용
    • spring-boot-starter-jdbc 도 함께 포함한다.
  • 다음과 같은 라이브러리가 추가됨
    • hibernate-core: JPA 구현체인 하이버네이트 라이브러리
    • jakarta.persistence-api: JPA Interface
    • spring-data-jpa: 스프링 데이터 JPA 라이브러리
  • application.properties 에 설정 추가
    • logging.level.org.hibernate.SQL=DEBUG : 하이버네이트가 생성하고 실행하는 SQL 확인
    • logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE : SQL에 바인딩되는 파라미터를 확인할 수 있다.
    • spring.jpa.show-sql=true : System.out 콘솔을 통해서 SQL이 출력된다. ( 권장 X )

package hello.itemservice.repository.jpa;

import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.List;
import java.util.Optional;

@Slf4j
@Repository
@Transactional
public class JpaItemRepositoryV1 implements ItemRepository {
    private final EntityManager entityManager;

    public JpaItemRepositoryV1(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    @Override
    public Item save(Item item) {
        entityManager.persist(item);
        return item;
    }

    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        Item foundItem = entityManager.find(Item.class, itemId);
        foundItem.setItemName(updateParam.getItemName());
        foundItem.setPrice(updateParam.getPrice());
        foundItem.setQuantity(updateParam.getQuantity());
    }

    @Override
    public Optional<Item> findById(Long id) {
        Item item = entityManager.find(Item.class, id);
        return Optional.ofNullable(item);
    }

    @Override
    public List<Item> findAll(ItemSearchCond cond) {
        String jpql = "select i from Item i";

        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        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 = entityManager.createQuery(jpql, Item.class);
        if (StringUtils.hasText(itemName)) {
            query.setParameter("itemName", itemName);
        }
        if (maxPrice != null) {
            query.setParameter("maxPrice", maxPrice);
        }
        return query.getResultList();
    }
}
  • EntityManager : JPA의 모든 동작은 엔티티 매니저를 통해서 이루어진다. 내부에 DataSource 를 가지고 있고, 데이터베이스에 접근할 수 있다.
  • @Transactional : JPA의 모든 데이터 변경은 Transaction 안에서 이루어져야 한다.
  • JPA를 설정하려면 EntityManagerFactory, JpaTransactionManager, DataSource 등 다양한 설정을 해야 한다. 스프링 부트는 이 과정을 모두 자동화 해준다. 스프링 부트의 자동 설정은 JpaBaseConfiguration을 참고한다.

save

  • persist(item) : JPA에서 객체를 테이블에 저장할 때, EntityManager가 제공하는 persist 메서드를 사용한다.
insert into item (id, item_name, price, quantity) values (default, ?, ?, ?)
  • JPA가 만들어서 실행한 SQL로 id에 값이 빠져있다.
  • PK key 생성 전략을 IDENTITY로 사용했기 때문이다.
    • 쿼리 실행 이후에 Item 객체의 id 필드에 데이터베이스가 생성한 PK 값이 들어간다.
    • JPA가 Insert SQL 실행 이후에 생성된 ID 결과를 받아서 넣어준다.

update

  • Item foundItem = entityManager.find(Item.class, itemId);
  • foundItem.setItemName(updateParam.getItemName());
2023-03-05 19:26:41.524 DEBUG 8190 --- [nio-8080-exec-6] org.hibernate.SQL                        : update item set item_name=?, price=?, quantity=? where id=?
2023-03-05 19:26:41.529 TRACE 8190 --- [nio-8080-exec-6] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [B]
2023-03-05 19:26:41.529 TRACE 8190 --- [nio-8080-exec-6] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [INTEGER] - [2000]
2023-03-05 19:26:41.530 TRACE 8190 --- [nio-8080-exec-6] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [INTEGER] - [80]
2023-03-05 19:26:41.530 TRACE 8190 --- [nio-8080-exec-6] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [BIGINT] - [18]
  • JPA는 Transaction 이 Commit 되는 시점에 변경된 Entity 객체가 있는지 확인한다. 
  • 특정 Entity 객체가 변경된 경우에는 UPDATE SQL을 실행한다.
    • 어떻게 변경된 객체를 찾는지 이해하려면 영속성 컨텍스트라는 JPA 내부 원리에 대한 이해가 필요하다.
  • 테스트의 경우 마지막에 Transaction Rollback이 되기때문에 JPA는 UPDATE SQL을 실행하지 않는다.
    • @Commit 을 붙여야 확인이 가능하다.

단건 조회

  • JPA에서 객체를 PK를 기준으로 조회할 때는 find 메서드를 사용하고, 조회 타입과 PK 값을 인자로 주면 된다.
  • 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=?
binding parameter [1] as [BIGINT] - [18]

목록 조회

String jpql = "select i from Item i";
//동적 쿼리
TypeQuery<Item> query = entityManager.createQuery(jpql, Item.class);
return query.getResultList();
  • JPQL ( Java Persistence Query Language )
    • JPA는 JPQL이라는 객체지향 쿼리 언어를 제공한다.
    • 주로 여러 데이터를 복잡한 조건으로 조회할 때 사용한다.
    • SQL은 테이블을 대상으로 한다.
    • JPQL은 Entity 객체를 대상으로 SQL을 실행한다.
    • JPQL을 실행하면 그 안에 포함된 Entity 객체의 매핑 정보를 활용해서 SQL을 만들게 된다.
  • 파라미터 입력
    • where price <= :maxPrice
  • 파라미터 바인딩
    • query.setParameter("maxPrice", maxPrice);
  • 동적 쿼리는 Querydsl 라는 기술을 활용한다.

예외 변환

JPA의 경우 예외가 발생하면 JPA 예외가 발생한다.

  • EntityManager는 순수한 JPA 기술이다.
  • JPA는 PersistenceException과 그 하위 예외를 발생시킨다.
  • JPA 예외를 스프링 예외 추상황 ( DataAccessException ) 으로 변환하는 방법
    • @Repository 사용
    • 컴포넌트 스캔의 대상이 되고, 예외 변환 AOP 의 적용 대상이 된다.
    • 스프링과 JPA를 함께 사용하는 경우, 스프링은 JPA 예외 변환기 ( PersistenceExceptionTranslator ) 를 등록한다.
  • 결과적으로 리포지토리에 @Repository 애노테이션만 있으면, 스프링이 예외 변환을 처리하는 AOP를 만들어준다.
    • 스프링 부트는 PersistenceExceptionTranslatorPostProcessor 를 자동으로 등록하는데, 여기에서 @Repository 를 AOP Proxy로 만드는 Advisor가 등록된다.
    • 실제 JPA 예외를 변환하는 코드는 EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible() 이다.
728x90