본문 바로가기
spring

스프링 DB 2편 - 3

by 오우지 2023. 1. 5.

JPA

 

JPA에서 가장 중요한 부분은 객체와 테이블을 매핑하는 것인데 애노테이션을 이용해서 객체와 테이블을 매핑할 수 있다.

 

 

간단한 내용이니 간략히 보면

@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;
    }
}

I don't know but today also my fucking keyboard cannot type korean. so, I'd like to continue in english.

 

@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);
        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;
        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();
    }
}

This is not Spring Data Jpa. This is pure jpa. contents about spring data jpa is gonna follow after this post.

 

At the beginning of this code. there are some part about EntityManager. 한국어가 써진다. 엔티티 매니저는 내부에 데이터 소스를 가지고 있어서 DB에 접근할 수 있다. 모든 트랜잭션, DB연결 등에 대한 관할을 담당한다.

부트가 아니라면 EntityManagerFactory, JpaTransactionManager, 데이터 소스 등을 모두 설정해줘야 하지만 부트는 이 과정을 자동화해준다.

 

일반적으로 조회는 트랜잭션 없이 가능하듯이 각 메서드에서 select문이 나가는 부분들은 트랜잭션을 안적어줘도 된다. 또한, 수정도 보통 서비스 계층에서 트랜잭션을 건다.

 

마지막 update는 JPQL을 사용했는데 주로 여러 데이터를 복잡한 조건으로 조회할 때 사용한다.

엔티티 객체를 대상으로 하기 때문에 from 다음에 Item 엔티티 객체 이름이 들어간다.

JPQL도 동적 쿼리에서는 결국 어려움이 발생하기 때문에 주로 queryDsl을 사용하게 된다.

 

예외 변환

JPA의 경우 예외가 발생하면 JPA 예외가 발생하게 된다. RuntimeException을 extends한 PersistenceException을 상속한 예외, IllegalStateException, IllegalArgumentException을 발생시킨다. 이걸 어떻게 스프링에서 제공하는 예외 추상화 DataAccessException 하위 예외로 바꿀 수 있을까?

 

이건 @Repository에 답이 있다. 예를 들어 @Repository 없이 쿼리를 잘못 적어서 예외가 나면 IllegalArgumentException이 난다. @Repository를 달면 @Repository가 붙은 클래스는 컴포넌트 스캔의 대상이 되고, 예외 변환 AOP의 적용 대상이 된다.

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

 


Spring Data JPA

여느 Spring Data 시리즈와 마찬가지로 공통적으로 많이 쓰는 save, update, delete 등의 기능들을 공통 인터페이스로 제공하는 라이브러리다. 추가로 쿼리 메서드 기능도 제공한다.

 

중요한건 기존 ItemService 구현체는 ItemRepository에 의존하고 있기 때문에 ItemService에서  코드 변경 없이는SpringDataJpaItemRepository를 바로 사용할 수 없다. 따라서 새로운 리포지토리를 어댑터로 사용해보자.

@Repository
@Transactional
@RequiredArgsConstructor
public class JpaItemRepositoryV2 implements ItemRepository {

    private final SpringDataJpaItemRepository repository;

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

    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        Item findItem = repository.findById(itemId).orElseThrow();
        findItem.setItemName(updateParam.getItemName());
        findItem.setPrice(updateParam.getPrice());
        findItem.setQuantity(updateParam.getQuantity());
    }

    @Override
    public Optional<Item> findById(Long id) {
        return repository.findById(id);
    }

    @Override
    public List<Item> findAll(ItemSearchCond cond) {
        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        if (StringUtils.hasText(itemName) && maxPrice != null) {
//            return repository.findByItemNameLikeAndPriceLessThanEqual(itemName, maxPrice);
            return repository.findItems(itemName, maxPrice);
        } else if (StringUtils.hasText(itemName)) {
            return repository.findByItemNameLike(itemName);
        } else if (maxPrice != null) {
            return repository.findByPriceLessThanEqual(maxPrice);
        } else {
            return repository.findAll();
        }
    }
}

맨 아래 동적 쿼리는 QueryDsl을 이용한 개선이 가능하다.

 

스프링 데이터 JPA도 예외 변환을 지원해주는데 JPA가 만들어주는 프록시에서 이미 예외 변환을 처리하기 때문에 @Repository 없이 예외가 변환된다.

 

 


QueryDsl

- 동적 쿼리를 깔끔하게 사용 가능하다.

- 쿼리 문장에 오타가 있어도 컴파일 시점에 오류를 막을 수 있다.

- 메서드 추출을 통해 코드를 재사용 할 수 있다.

 

 

'spring' 카테고리의 다른 글

스프링 DB 2편 - 2  (0) 2023.01.03
스프링 DB 2편 - 1  (0) 2022.12.21
스프링 DB 1편 - 3  (0) 2022.12.12
스프링 DB 1편 - 2  (0) 2022.11.30
스프링 DB 1편 - 1  (0) 2022.11.02