본문 바로가기
인프런/스프링 DB 2편 - 데이터 접근 활용 기술

[인프런] 스프링 DB 2편 - 데이터 접근 기술 활용 / 5. JPA

by hxxyeoniii 2024. 8. 4.

JPA 설정

스프링이 DI 컨테이너를 포함한 애플리케이션 전반의 다양한 기능을 제공한다면, JPA는 ORM 데이터 접근 기술을 제공

 

(+) 생산성 증가

(+) SQL 중심의 개발에서 객체 중심의 개발

(+) 애플리케이션과 DB 사이에 JPA가 존재한다고 보면됨 : 1차 캐시 지원

 

 

build.gradle 의존관계 추가 및 제거

spring-boot-starter-data-jpa 라이브러리를 사용하면 JPA와 스프링 데이터 JPA를 스프링 부트와 통합하고, 설정도 간단히 할 수 있음

-> spring-boot-starter-data-jpa가 spring-boot-starter-jdb도 함께 포함하므로 주석처리

-> 참고로, mybatis-spring-boot-starter도 spring-boot-starter-jdbc를 포함함

 

이렇게 설정할 시 다음과 같은 3개의 라이브러리가 추가된다.

1) hibernate-core : JPA 구현체인 하이버네이트 라이브러리

2) jakarta.persistence-api : JPA 인터페이스

3) spring-data-jpa : 스프링 데이터 JPA 라이브러리

 

 

application.properties 추가

-> org.hibernate.SQL=DEBUG : 하이버네이트가 생성하고 실행하는 SQL 확인 가능

-> org.hibernate.type.descriptor.sql.BasicBinder=TRACE : SQL에 바인딩 되는 파라미터 확인 가능

 

 

* 스프링 부트 3.0이상을 사용하면 하이버네이트 6버전이 사용되는데, 로그 방식이 달라지므로 다음과 같이 설정해야 함

logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.orm.jdbc.bind=TRACE

JPA 적용 

JPA에서 가장 중요한 부분은 객체와 테이블을 매핑하는 것

 

Item.java : Item 객체와 테이블 매핑

-> @Entity : JPA가 사용하는 객체라는 뜻으로, 이 애노테이션으로 JPA가 인식함

-> @Id : 테이블의 PK와 해당 필드 매핑

-> @GeneratedValue(strategy = GenerationType.IDENTITY) : PK 생성 값을 DB에서 생성하는 IDENTITY 방식을 사용

-> @Column : 객체의 필드를 테이블 컬럼과 매핑

-> JPA는 public 또는 protected의 기본 생성자가 필수!

 

 

JpaItemRepositoryV1.java

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 javax.persistence.EntityManager;
import javax.transaction.Transactional;
import java.util.List;
import java.util.Optional;


@Slf4j
@Repository
@Transactional
public class JpaItemRepository implements ItemRepository {

    private final EntityManager em;

    public JpaItemRepository(EntityManager em) {
        this.em = em;
    }

    @Override
    public Item save(Item item) {
        em.persist(item); // 매핑 정보를 가지고 insert 쿼리를 가지고 저장해줌
        return item;
    }

    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        Item findItem = em.find(Item.class, itemId);

        // JPA가 내부에 스냅샷을 떠놓고 트랜잭션이 커밋되는 시점에 해당 데이터들에 대한
        // update 쿼리를 만들고 DB에 날림
        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"; // i는 Item 엔티티 자체
        
        List<Item> result = em.createQuery(jpql, Item.class).getResultList();
        
        return result;
    }
}

-> private final EntityManager

   : 생성자를 보면 스프링을 통해 엔티티 매니저를 주입 받는다.

   : JPA의 모든 동작은 엔티티 매니저를 통해 이루어진다.

   : 엔티티 매니저는 내부에 데이터 소스를 가지고 있고, 데이터베이스에 접근할 수 있다.

-> @Transactional 

   : JPA의 모든 데이터 변경은 트랜잭션 안에서 이루어져야 한다(조회는 트랜잭션 없이도 가능)

   : 변경의 경우, 일반적으로 서비스 계층에서 트랜잭션을 시작하기 때문에 문제가 없다.

   : 일반적으로는 비지니스 로직을 시작하는 서비스 계층에 트랜잭션을 걸어주는 것이 맞음, 현재 예제에는 복잡한 비지니스 로직이 없으므로 리포지토리에 트랜잭션을 걸어주었다.


 

JPA 리포지토리 분석

1. 저장 : save()

em.persist() : JPA에서 객체를 테이블에 저장할 때는 엔티티 매니저가 제공하는 persist() 메서드를 사용

-> JPA가 만들어 실행한 SQL에 id가 빠져있는 것은, PK 키 생성 전략을 IDENTITY로 사용했기 때문(DB가 생성한 PK값이 들어가게 됨)

 

 

2. 수정 : udpate()

-> em.update() 같은 메서드가 없는데 어떻게 UPDATE SQL이 실행되는가?

-> JPA는 트랜잭션이 커밋되는 시점에 변경된 엔티티가 있는지 확인하고 특정 엔티티 객체가 변경된 경우 UPDATE SQL을 실행한다.

 

 

3. 단건 조회 : findByID() 

-> 별칭이 조금 복잡한 것으로 확인 : 조인이 발생하거나 복잡한 조건에서도 문제 없도록 기계적으로 만들다보니 이런 것으로 추측

 

 

4. 목록조회 : findAll() - 동적 쿼리

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(Java Persistence Query Language)

- JPA는 JPQL이라는 객체지향 쿼리 언어 제공

- 주로 여러 데이터를 복잡한 조건으로 조회할 때 사용

- SQL이 테이블을 대상으로 한다면, JPQL은 엔티티 객체를 대상으로 SQL을 실행한다고 생각하면 됨

- JPQL을 실행하면 그 안에 포함된 엔티티 객체의 매핑 정보를 활용해 SQL을 만들게 됨

 

 

=> JPA를 사용해도 동적 쿼리 문제는 남아 있음, 실무에서는 동적 쿼리 때문에 JPA 사용 시 Querydsl도 함께 선택


JPA 예외 변환

JPA의 경우, 예외가 발생하면 JPA 예외가 발생하게 됨

 

- EntityManager는 순수 JPA 기술이고, 스프링과는 관계가 없음 -> 엔티티 메니저는 예외가 발생하면 JPA 관련 예외를 발생시킴

- JPA는 PersistenceException과 그 하위 예외를 발생시킴

- 어떻게 JPA 예외를 스프링 예외 추상화로 변환할 수 있을까? -> @Repository 기능 사용!!

 

 

예외 변환 전

예외 변환 후

 

@Repository의 기능

1) @Repository가 붙은 클래스는 컴포넌트 스캔의 대상이 됨

2) @Repository가 붙은 클래스는 예외 변환 AOP의 적용 대상이 됨

   -> 스프링과 JPA를 함께 사용하는 경우 스프링은 JPA 예외 변환기(PersistenceExceptionTranslater)를 등록한다.

   -> 예외 변환 AOP 프록시는 JPA 관련 예외가 발생하면 JPA 예외 변환기를 통해 발생한 예외를 스프링 데이터 접근 예외로 변환한다.

 

 

=> 리포지토리에 @Repository 애노테이션이 있으면 스프링이 예외 변환을 처리하는 AOP를 만들어준다.

=> 스프링 부트는 PersistenceExceptionTranslationPostProcessor를 자동으로 등록하는데, 여기서 @Repository를 AOP 프록시로 만드는 어드바이저가 등록된다.