3 분 소요


JPA에 대한 자세한 내용은 JPA 카테고리의 내용을 확인.

JPA 적용1 - 개발

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

Item - ORM 매핑

@Data
@Entity
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방식을 사용한다.
    • Ex) MySQL auto increment
  • @Column: 객체의 필드를 테이블의 컬럼과 매핑한다.
    • 객체는 itemName이지만 테이블의 컬럼은 item_name이므로 이렇게 매핑했다.
    • length = 10: JPA의 매핑 정보로 DDL(create table)도 생성할 수 있는데, 그때 컬럼의 길이 값으로 활용된다. (varchar 10)
    • @Column을 생략할 경우 필드의 이름을 테이블 컬럼 이름으로 사용한다. 참고로 스프링 부트와 통합해서 사용하면 필드 이름을 테이블 컬럼 명으로 변경할 때 객체 필드의 카멜 케이스를 테이블 컬럼의 언터스코어로 자동으로 변환해준다.
      • itemName -> item_name
      • 따라서, 위 @Column(name = "item_name", length = 10)코드는 생략해도 된다.
  • JPA는 public 또는 protected의 기본 생성자가 필수이다.

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;
        List<Object> param = new ArrayList<>();
        if (StringUtils.hasText(itemName)) {
            jpql += " i.itemName like concat('%',:itemName,'%')";
            param.add(itemName);
            andFlag = true;
        }

        if (maxPrice != null) {
            if (andFlag) {
                jpql += " and";
            }
            jpql += " i.price <= :maxPrice";
            param.add(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라는 것을 주입받는다.
    • JPA의 모든 동작은 엔티티 매니저를 통해서 이루어진다.
    • 엔티티 매니저는 내부에 데이터소스를 가지고 있고, 데이터베이스에 접근할 수 있다.
  • @Transactional: JPA의 모든 데이터 변경은 트랜잭션 안에서 이루어져야 한다.
    • 일반적으로는 비즈니스 로직을 시작하는 서비스 계층에 트랜잭션을 걸어주는 것이 맞다.


Repository 분석

save() - 저장

public Item save(Item item) {
    em.persist(item);
    return item;
}
  • em.persist(item): JPA에서 객체를 테이블에 저장할 때는 엔티티 매니저가 제공하는 persist() 메서드를 사용하면 된다.

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

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을 실행한다 생각하면 된다.


파라미터

  • JPQL에서 파라미터는 다음과 같이 입력한다.
    • where price <= :maxPrice
  • 파라미터 바인딩
    • query.setParameter("maxPrice", maxPrice)

동적 쿼리 문제

JPA를 사용해도 동적 쿼리 문제가 남아있다. 동적 쿼리는 Querydsl이라는 기술을 활용하면 매우 깔끔하게 사용할 수 있다. 실무에서는 동적 쿼리 문제 때문에, JPA를 사용할 때 Querydsl도 함께 선택하게 된다.


JPA 적용2 - 예외 변환

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

@Repository
@Transactional
public class JpaItemRepositoryV1 implements ItemRepository {

    private final EntityManager em;
    
    @Override
    public Item save(Item item) {
        em.persist(item);
        return item;
    }
}
  • EntityManager는 순수한 JPA 기술이고, 스프링과는 관계가 없다. 따라서, 엔티티 매니저는 예외가 발생하면 JPA 관련 예외를 발생시킨다.
  • JPA는 PersistenceException과 그 하위 예외를 발생시킨다.
    • 추가로 JPA는 IllegalStateException, IllegalArgumentException을 발생시킬 수 있다.
  • 그렇다면 JPA 예외를 스프링 예외 추상화(DataAccessException)로 어떻게 변환할 수 있을까?

예외 변환 전

3

@Repository의 기능

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

예외 변환 후

4

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


<출처 : 인프런 - 스프링 DB 2편 : 데이터 접근 활용 기술(김영한)>

댓글남기기